From adb05d4c49fe2f518e5554cc7d6c2fbe3b01670d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 10 Oct 2023 11:38:40 +0800 Subject: [PATCH] feat: support customizing error block (#524) * feat: support customzing error block * feat: support slash menu in toggle list * fix: enter in heading block --- .../insert_newline_in_type_command.dart | 1 - .../heading_character_shortcut.dart | 31 +++++ .../standard_block_components.dart | 1 + .../renderer/block_component_service.dart | 23 ++-- .../slash_command.dart | 4 +- test/customer/custom_error_block_test.dart | 130 ++++++++++++++++++ 6 files changed, 177 insertions(+), 13 deletions(-) create mode 100644 test/customer/custom_error_block_test.dart diff --git a/lib/src/editor/block_component/base_component/insert_newline_in_type_command.dart b/lib/src/editor/block_component/base_component/insert_newline_in_type_command.dart index 6f83dd85c..c3dc30c5b 100644 --- a/lib/src/editor/block_component/base_component/insert_newline_in_type_command.dart +++ b/lib/src/editor/block_component/base_component/insert_newline_in_type_command.dart @@ -19,7 +19,6 @@ Future insertNewLineInType( if (selection.startIndex == 0 && delta.isEmpty) { // clear the style - return KeyEventResult.ignored != convertToParagraphCommand.execute(editorState); } diff --git a/lib/src/editor/block_component/heading_block_component/heading_character_shortcut.dart b/lib/src/editor/block_component/heading_block_component/heading_character_shortcut.dart index 95580cf49..f4bbe7baf 100644 --- a/lib/src/editor/block_component/heading_block_component/heading_character_shortcut.dart +++ b/lib/src/editor/block_component/heading_block_component/heading_character_shortcut.dart @@ -30,3 +30,34 @@ CharacterShortcutEvent formatSignToHeading = CharacterShortcutEvent( }, ), ); + +/// Insert a new block after the heading block. +/// +/// - support +/// - desktop +/// - web +/// - mobile +/// +CharacterShortcutEvent insertNewLineAfterHeading = CharacterShortcutEvent( + key: 'insert new block after heading', + character: '\n', + handler: (editorState) async { + final selection = editorState.selection; + if (selection == null || + !selection.isCollapsed || + selection.startIndex != 0) { + return false; + } + final node = editorState.getNodeAtPath(selection.end.path); + if (node == null || node.type != HeadingBlockKeys.type) { + return false; + } + final transaction = editorState.transaction; + transaction.insertNode(selection.start.path, paragraphNode()); + transaction.afterSelection = Selection.collapsed( + Position(path: selection.start.path.next, offset: 0), + ); + await editorState.apply(transaction); + return true; + }, +); diff --git a/lib/src/editor/block_component/standard_block_components.dart b/lib/src/editor/block_component/standard_block_components.dart index b3165e2c5..0fb339661 100644 --- a/lib/src/editor/block_component/standard_block_components.dart +++ b/lib/src/editor/block_component/standard_block_components.dart @@ -53,6 +53,7 @@ final List standardCharacterShortcutEvents = [ insertNewLineAfterBulletedList, insertNewLineAfterTodoList, insertNewLineAfterNumberedList, + insertNewLineAfterHeading, insertNewLine, // bulleted list diff --git a/lib/src/editor/editor_component/service/renderer/block_component_service.dart b/lib/src/editor/editor_component/service/renderer/block_component_service.dart index e8ab706bd..fefaa86dd 100644 --- a/lib/src/editor/editor_component/service/renderer/block_component_service.dart +++ b/lib/src/editor/editor_component/service/renderer/block_component_service.dart @@ -1,6 +1,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; +const errorBlockComponentBuilderKey = 'errorBlockComponentBuilderKey'; + typedef BlockActionBuilder = Widget Function( BlockComponentContext blockComponentContext, BlockComponentActionState state, @@ -121,17 +123,18 @@ class BlockComponentRenderer extends BlockComponentRendererService { header: header, footer: footer, ); + final errorBuilder = _builders[errorBlockComponentBuilderKey]; final builder = blockComponentBuilder(node.type); - if (builder == null) { - assert(false, 'no builder for node type(${node.type})'); - return _buildPlaceHolderWidget(blockComponentContext); - } - if (!builder.validate(node)) { - assert( - false, - 'node(${node.type}) is invalid, attributes: ${node.attributes}, children: ${node.children}', - ); - return _buildPlaceHolderWidget(blockComponentContext); + if (builder == null || !builder.validate(node)) { + if (errorBuilder != null) { + return BlockComponentContainer( + node: node, + configuration: errorBuilder.configuration, + builder: (_) => errorBuilder.build(blockComponentContext), + ); + } else { + return _buildPlaceHolderWidget(blockComponentContext); + } } return BlockComponentContainer( diff --git a/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/slash_command.dart b/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/slash_command.dart index 5d2b41924..24180aad2 100644 --- a/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/slash_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/slash_command.dart @@ -34,14 +34,14 @@ CharacterShortcutEvent customSlashCommand( ); } -final supportSlashMenuNodeWhiteList = [ +final Set supportSlashMenuNodeWhiteList = { ParagraphBlockKeys.type, HeadingBlockKeys.type, TodoListBlockKeys.type, BulletedListBlockKeys.type, NumberedListBlockKeys.type, QuoteBlockKeys.type, -]; +}; SelectionMenuService? _selectionMenuService; Future _showSlashMenu( diff --git a/test/customer/custom_error_block_test.dart b/test/customer/custom_error_block_test.dart new file mode 100644 index 000000000..9b2e69330 --- /dev/null +++ b/test/customer/custom_error_block_test.dart @@ -0,0 +1,130 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + await AppFlowyEditorLocalizations.load( + const Locale.fromSubtags(languageCode: 'en'), + ); + + testWidgets('custom error block', (tester) async { + final editorState = EditorState.blank(withInitialText: false); + editorState.document.insert( + [0], + [ + Node( + type: 'not_exist', + attributes: { + 'text': 'line 1', + }, + ), + Node( + type: 'heading', + attributes: {}, + ), + ], + ); + final widget = ErrorEditor( + editorState: editorState, + ); + await tester.pumpWidget(widget); + await tester.pumpAndSettle(); + + expect(find.byType(ErrorBlockComponentWidget), findsNWidgets(2)); + }); +} + +class ErrorEditor extends StatelessWidget { + const ErrorEditor({ + super.key, + required this.editorState, + }); + + final EditorState editorState; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Container( + width: 1000, + decoration: BoxDecoration( + border: Border.all(color: Colors.blue), + ), + child: AppFlowyEditor( + editorState: editorState, + blockComponentBuilders: { + ...standardBlockComponentBuilderMap, + errorBlockComponentBuilderKey: + ErrorBlockComponentBuilder(), + }, + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class ErrorBlockComponentBuilder extends BlockComponentBuilder { + ErrorBlockComponentBuilder({ + super.configuration, + }); + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + return ErrorBlockComponentWidget( + key: node.key, + node: node, + configuration: configuration, + showActions: showActions(node), + actionBuilder: (context, state) => actionBuilder( + blockComponentContext, + state, + ), + ); + } + + @override + bool validate(Node node) => true; +} + +class ErrorBlockComponentWidget extends BlockComponentStatefulWidget { + const ErrorBlockComponentWidget({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + }); + + @override + State createState() => + _DividerBlockComponentWidgetState(); +} + +class _DividerBlockComponentWidgetState extends State + with BlockComponentConfigurable { + @override + BlockComponentConfiguration get configuration => widget.configuration; + + @override + Node get node => widget.node; + + @override + Widget build(BuildContext context) { + return Container( + color: Colors.red, + child: const Text('error'), + ); + } +}