From 6230d0ad9fbc74781ce4660435b52354eb4c162c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 28 Sep 2022 11:21:42 +0800 Subject: [PATCH 1/6] feat: add commands and update checkbox logic --- .../src/commands/format_built_in_text.dart | 62 +++++++++++++++++++ .../lib/src/commands/format_text.dart | 34 ++++++++++ .../src/render/rich_text/checkbox_text.dart | 17 +++-- .../src/render/rich_text/flowy_rich_text.dart | 13 ++++ .../format_rich_text_style.dart | 12 ++-- 5 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart new file mode 100644 index 0000000000..30e5fcf17e --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart @@ -0,0 +1,62 @@ +import 'package:appflowy_editor/src/commands/format_text.dart'; +import 'package:appflowy_editor/src/document/attributes.dart'; +import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; + +Future formatBuiltInTextAttributes( + EditorState editorState, + String key, + Attributes attributes, { + Path? path, + TextNode? textNode, +}) async { + if (BuiltInAttributeKey.globalStyleKeys.contains(key)) { + assert(!(path != null && textNode != null)); + assert(!(path == null && textNode == null)); + + TextNode formattedTextNode; + if (textNode != null) { + formattedTextNode = textNode; + } else if (path != null) { + formattedTextNode = editorState.document.nodeAtPath(path) as TextNode; + } else { + throw Exception('path and textNode cannot be null at the same time'); + } + // remove all the existing style + final newAttributes = formattedTextNode.attributes + ..removeWhere((key, value) { + if (BuiltInAttributeKey.globalStyleKeys.contains(key)) { + return true; + } + return false; + }) + ..addAll(attributes) + ..addAll({ + BuiltInAttributeKey.subtype: key, + }); + return updateTextNodeAttributes( + editorState, + newAttributes, + textNode: textNode, + ); + } +} + +Future formatTextToCheckbox( + EditorState editorState, + bool check, { + Path? path, + TextNode? textNode, +}) async { + return formatBuiltInTextAttributes( + editorState, + BuiltInAttributeKey.checkbox, + { + BuiltInAttributeKey.checkbox: check, + }, + path: path, + textNode: textNode, + ); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart new file mode 100644 index 0000000000..41eb8c16e6 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart @@ -0,0 +1,34 @@ +import 'package:appflowy_editor/src/document/attributes.dart'; +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:flutter/widgets.dart'; + +Future updateTextNodeAttributes( + EditorState editorState, + Attributes attributes, { + Path? path, + TextNode? textNode, +}) async { + assert(!(path != null && textNode != null)); + assert(!(path == null && textNode == null)); + + TextNode formattedTextNode; + if (textNode != null) { + formattedTextNode = textNode; + } else if (path != null) { + formattedTextNode = editorState.document.nodeAtPath(path) as TextNode; + } else { + throw Exception('path and textNode cannot be null at the same time'); + } + + TransactionBuilder(editorState) + ..updateNode(formattedTextNode, attributes) + ..commit(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + print('AAAAAAAAAAAAAA'); + return; + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index 2ca7531d2b..e41f3b4891 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -1,15 +1,8 @@ -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; -import 'package:appflowy_editor/src/document/node.dart'; -import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/commands/format_built_in_text.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart'; -import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart'; -import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart'; -import 'package:appflowy_editor/src/render/selection/selectable.dart'; -import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; -import 'package:appflowy_editor/src/service/render_plugin_service.dart'; -import 'package:appflowy_editor/src/extensions/attributes_extension.dart'; import 'package:appflowy_editor/src/extensions/text_style_extension.dart'; import 'package:flutter/material.dart'; @@ -82,7 +75,11 @@ class _CheckboxNodeWidgetState extends State name: check ? 'check' : 'uncheck', ), onTap: () { - formatCheckbox(widget.editorState, !check); + formatTextToCheckbox( + widget.editorState, + !check, + textNode: widget.textNode, + ); }, ), Flexible( diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 7dba4852ed..99a6d08918 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -18,6 +18,8 @@ import 'package:appflowy_editor/src/extensions/attributes_extension.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +const _kRichTextDebugMode = false; + typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); class FlowyRichText extends StatefulWidget { @@ -261,6 +263,17 @@ class _FlowyRichTextState extends State with SelectableMixin { ), ); } + if (_kRichTextDebugMode) { + textSpans.add( + TextSpan( + text: '${widget.textNode.path}', + style: const TextStyle( + backgroundColor: Colors.red, + fontSize: 16.0, + ), + ), + ); + } return TextSpan( children: textSpans, ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index 23ddc75a69..053d9e542a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart @@ -103,13 +103,17 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) { final builder = TransactionBuilder(editorState); for (final textNode in textNodes) { + var newAttributes = {...textNode.attributes}; + for (final globalStyleKey in BuiltInAttributeKey.globalStyleKeys) { + if (newAttributes.keys.contains(globalStyleKey)) { + newAttributes[globalStyleKey] = null; + } + } + newAttributes.addAll(attributes); builder ..updateNode( textNode, - Attributes.fromIterable( - BuiltInAttributeKey.globalStyleKeys, - value: (_) => null, - )..addAll(attributes), + newAttributes, ) ..afterSelection = Selection.collapsed( Position( From ab0131c19ccfab1709553fcd0d8c766b4c0065ef Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 28 Sep 2022 11:49:31 +0800 Subject: [PATCH 2/6] feat: disable apply operation when setting editable = false --- .../packages/appflowy_editor/lib/src/editor_state.dart | 5 +++++ .../appflowy_editor/lib/src/service/editor_service.dart | 2 ++ 2 files changed, 7 insertions(+) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart index 4f2ca39a84..4aeb7ab599 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart @@ -72,6 +72,8 @@ class EditorState { // TODO: only for testing. bool disableSealTimer = false; + bool editable = true; + Selection? get cursorSelection { return _cursorSelection; } @@ -112,6 +114,9 @@ class EditorState { /// should record the transaction in undo/redo stack. apply(Transaction transaction, [ApplyOptions options = const ApplyOptions()]) { + if (!editable) { + return; + } // TODO: validate the transation. for (final op in transaction.operations) { _applyOperation(op); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart index 7174290b9c..3d9599383d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart @@ -72,6 +72,7 @@ class _AppFlowyEditorState extends State { editorState.selectionMenuItems = widget.selectionMenuItems; editorState.editorStyle = widget.editorStyle; editorState.service.renderPluginService = _createRenderPlugin(); + editorState.editable = widget.editable; } @override @@ -84,6 +85,7 @@ class _AppFlowyEditorState extends State { } editorState.editorStyle = widget.editorStyle; + editorState.editable = widget.editable; services = null; } From 99cb2430f724b6188d2a357ed28d0b36c5149d8a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 28 Sep 2022 23:01:22 +0800 Subject: [PATCH 3/6] fix: could not insert link on the Web --- .../src/commands/format_built_in_text.dart | 44 ++++++--- .../lib/src/commands/format_text.dart | 95 ++++++++++++++++--- .../lib/src/document/selection.dart | 4 + .../src/render/rich_text/checkbox_text.dart | 4 +- .../lib/src/render/toolbar/toolbar_item.dart | 9 +- 5 files changed, 124 insertions(+), 32 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart index 30e5fcf17e..e9fe907e7d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart @@ -3,29 +3,25 @@ import 'package:appflowy_editor/src/document/attributes.dart'; import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/editor_state.dart'; Future formatBuiltInTextAttributes( EditorState editorState, String key, Attributes attributes, { + Selection? selection, Path? path, TextNode? textNode, }) async { + final result = getTextNodeToBeFormatted( + editorState, + path: path, + textNode: textNode, + ); if (BuiltInAttributeKey.globalStyleKeys.contains(key)) { - assert(!(path != null && textNode != null)); - assert(!(path == null && textNode == null)); - - TextNode formattedTextNode; - if (textNode != null) { - formattedTextNode = textNode; - } else if (path != null) { - formattedTextNode = editorState.document.nodeAtPath(path) as TextNode; - } else { - throw Exception('path and textNode cannot be null at the same time'); - } // remove all the existing style - final newAttributes = formattedTextNode.attributes + final newAttributes = result.attributes ..removeWhere((key, value) { if (BuiltInAttributeKey.globalStyleKeys.contains(key)) { return true; @@ -41,6 +37,13 @@ Future formatBuiltInTextAttributes( newAttributes, textNode: textNode, ); + } else if (BuiltInAttributeKey.partialStyleKeys.contains(key)) { + return updateTextNodeDeltaAttributes( + editorState, + selection, + attributes, + textNode: textNode, + ); } } @@ -60,3 +63,20 @@ Future formatTextToCheckbox( textNode: textNode, ); } + +Future formatLinkInText( + EditorState editorState, + String? link, { + Path? path, + TextNode? textNode, +}) async { + return formatBuiltInTextAttributes( + editorState, + BuiltInAttributeKey.href, + { + BuiltInAttributeKey.href: link, + }, + path: path, + textNode: textNode, + ); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart index 41eb8c16e6..ad6a9bbfc0 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart @@ -1,6 +1,9 @@ +import 'dart:async'; + import 'package:appflowy_editor/src/document/attributes.dart'; import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:flutter/widgets.dart'; @@ -11,24 +14,90 @@ Future updateTextNodeAttributes( Path? path, TextNode? textNode, }) async { - assert(!(path != null && textNode != null)); - assert(!(path == null && textNode == null)); + final result = getTextNodeToBeFormatted( + editorState, + path: path, + textNode: textNode, + ); - TextNode formattedTextNode; - if (textNode != null) { - formattedTextNode = textNode; - } else if (path != null) { - formattedTextNode = editorState.document.nodeAtPath(path) as TextNode; - } else { - throw Exception('path and textNode cannot be null at the same time'); - } + final completer = Completer(); TransactionBuilder(editorState) - ..updateNode(formattedTextNode, attributes) + ..updateNode(result, attributes) ..commit(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - print('AAAAAAAAAAAAAA'); - return; + completer.complete(); }); + + return completer.future; +} + +Future updateTextNodeDeltaAttributes( + EditorState editorState, + Selection? selection, + Attributes attributes, { + Path? path, + TextNode? textNode, +}) { + final result = getTextNodeToBeFormatted( + editorState, + path: path, + textNode: textNode, + ); + final newSelection = _getSelection(editorState, selection: selection); + + final completer = Completer(); + + TransactionBuilder(editorState) + ..formatText( + result, + newSelection.startIndex, + newSelection.length, + attributes, + ) + ..commit(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + completer.complete(); + }); + + return completer.future; +} + +// get formatted [TextNode] +TextNode getTextNodeToBeFormatted( + EditorState editorState, { + Path? path, + TextNode? textNode, +}) { + assert(!(path != null && textNode != null)); + assert(!(path == null && textNode == null)); + + TextNode result; + if (textNode != null) { + result = textNode; + } else if (path != null) { + result = editorState.document.nodeAtPath(path) as TextNode; + } else { + throw Exception('path and textNode cannot be null at the same time'); + } + return result; +} + +Selection _getSelection( + EditorState editorState, { + Selection? selection, +}) { + final currentSelection = + editorState.service.selectionService.currentSelection.value; + Selection result; + if (selection != null) { + result = selection; + } else if (currentSelection != null) { + result = currentSelection; + } else { + throw Exception('path and textNode cannot be null at the same time'); + } + return result; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart index ea451b46dd..99248dc167 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart @@ -53,6 +53,10 @@ class Selection { Selection get reversed => copyWith(start: end, end: start); + int get startIndex => normalize.start.offset; + int get endIndex => normalize.end.offset; + int get length => endIndex - startIndex; + Selection collapse({bool atStart = false}) { if (atStart) { return Selection(start: start, end: start); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index e41f3b4891..10b17d6b36 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -74,8 +74,8 @@ class _CheckboxNodeWidgetState extends State padding: iconPadding, name: check ? 'check' : 'uncheck', ), - onTap: () { - formatTextToCheckbox( + onTap: () async { + await formatTextToCheckbox( widget.editorState, !check, textNode: widget.textNode, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 68bb5023ca..6407f81cb3 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/commands/format_built_in_text.dart'; import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; @@ -345,11 +346,8 @@ void showLinkMenu( onOpenLink: () async { await safeLaunchUrl(linkText); }, - onSubmitted: (text) { - TransactionBuilder(editorState) - ..formatText( - textNode, index, length, {BuiltInAttributeKey.href: text}) - ..commit(); + onSubmitted: (text) async { + await formatLinkInText(editorState, text, textNode: textNode); _dismissLinkMenu(); }, onCopyLink: () { @@ -377,6 +375,7 @@ void showLinkMenu( Overlay.of(context)?.insert(_linkMenuOverlay!); editorState.service.scrollService?.disable(); + editorState.service.keyboardService?.disable(); editorState.service.selectionService.currentSelection .addListener(_dismissLinkMenu); } From 6ec93d49c2b0b90f16b1b1bddcbc6ade71f20e82 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 28 Sep 2022 23:03:02 +0800 Subject: [PATCH 4/6] feat: set link menu as auto focus --- .../appflowy_editor/lib/src/render/link_menu/link_menu.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 13396a33c4..3a1785391b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -31,6 +31,7 @@ class _LinkMenuState extends State { void initState() { super.initState(); _textEditingController.text = widget.linkText ?? ''; + _focusNode.requestFocus(); _focusNode.addListener(_onFocusChange); } From 0a49a182808d25f2749d14b27911af7a66d464ff Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Wed, 28 Sep 2022 23:36:44 +0800 Subject: [PATCH 5/6] fix: could not insert space sometimes on the web --- .../lib/src/commands/edit_text.dart | 34 ++++++++++++++ .../src/commands/format_built_in_text.dart | 1 + .../lib/src/commands/format_text.dart | 40 +---------------- .../lib/src/commands/text_command_infra.dart | 43 ++++++++++++++++++ .../space_on_web_handler.dart | 21 +++++++++ .../built_in_shortcut_events.dart | 12 +++++ .../space_on_web_handler_test.dart | 45 +++++++++++++++++++ 7 files changed, 158 insertions(+), 38 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart create mode 100644 frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart new file mode 100644 index 0000000000..2e1310ca2c --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/edit_text.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:appflowy_editor/src/commands/text_command_infra.dart'; +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; +import 'package:appflowy_editor/src/operation/transaction_builder.dart'; +import 'package:flutter/widgets.dart'; + +Future insertContextInText( + EditorState editorState, + int index, + String content, { + Path? path, + TextNode? textNode, +}) async { + final result = getTextNodeToBeFormatted( + editorState, + path: path, + textNode: textNode, + ); + + final completer = Completer(); + + TransactionBuilder(editorState) + ..insertText(result, index, content) + ..commit(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + completer.complete(); + }); + + return completer.future; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart index e9fe907e7d..dcce054351 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_built_in_text.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/src/commands/format_text.dart'; +import 'package:appflowy_editor/src/commands/text_command_infra.dart'; import 'package:appflowy_editor/src/document/attributes.dart'; import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; import 'package:appflowy_editor/src/document/node.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart index ad6a9bbfc0..0ec9e7b61a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/format_text.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:appflowy_editor/src/commands/text_command_infra.dart'; import 'package:appflowy_editor/src/document/attributes.dart'; import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/path.dart'; @@ -45,7 +46,7 @@ Future updateTextNodeDeltaAttributes( path: path, textNode: textNode, ); - final newSelection = _getSelection(editorState, selection: selection); + final newSelection = getSelection(editorState, selection: selection); final completer = Completer(); @@ -64,40 +65,3 @@ Future updateTextNodeDeltaAttributes( return completer.future; } - -// get formatted [TextNode] -TextNode getTextNodeToBeFormatted( - EditorState editorState, { - Path? path, - TextNode? textNode, -}) { - assert(!(path != null && textNode != null)); - assert(!(path == null && textNode == null)); - - TextNode result; - if (textNode != null) { - result = textNode; - } else if (path != null) { - result = editorState.document.nodeAtPath(path) as TextNode; - } else { - throw Exception('path and textNode cannot be null at the same time'); - } - return result; -} - -Selection _getSelection( - EditorState editorState, { - Selection? selection, -}) { - final currentSelection = - editorState.service.selectionService.currentSelection.value; - Selection result; - if (selection != null) { - result = selection; - } else if (currentSelection != null) { - result = currentSelection; - } else { - throw Exception('path and textNode cannot be null at the same time'); - } - return result; -} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart new file mode 100644 index 0000000000..d54a84a3e0 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/commands/text_command_infra.dart @@ -0,0 +1,43 @@ +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/path.dart'; +import 'package:appflowy_editor/src/document/selection.dart'; +import 'package:appflowy_editor/src/editor_state.dart'; + +// get formatted [TextNode] +TextNode getTextNodeToBeFormatted( + EditorState editorState, { + Path? path, + TextNode? textNode, +}) { + final currentSelection = + editorState.service.selectionService.currentSelection.value; + TextNode result; + if (textNode != null) { + result = textNode; + } else if (path != null) { + result = editorState.document.nodeAtPath(path) as TextNode; + } else if (currentSelection != null && currentSelection.isCollapsed) { + result = editorState.document.nodeAtPath(currentSelection.start.path) + as TextNode; + } else { + throw Exception('path and textNode cannot be null at the same time'); + } + return result; +} + +Selection getSelection( + EditorState editorState, { + Selection? selection, +}) { + final currentSelection = + editorState.service.selectionService.currentSelection.value; + Selection result; + if (selection != null) { + result = selection; + } else if (currentSelection != null) { + result = currentSelection; + } else { + throw Exception('path and textNode cannot be null at the same time'); + } + return result; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart new file mode 100644 index 0000000000..6d942135ba --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/space_on_web_handler.dart @@ -0,0 +1,21 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/commands/edit_text.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +ShortcutEventHandler spaceOnWebHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType() + .toList(growable: false); + if (selection == null || + !selection.isCollapsed || + !kIsWeb || + textNodes.length != 1) { + return KeyEventResult.ignored; + } + + insertContextInText(editorState, selection.startIndex, ' '); + + return KeyEventResult.handled; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index 38eb9ee7c5..ca614354a0 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart @@ -9,9 +9,11 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_und import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/format_style_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/space_on_web_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/tab_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart'; import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart'; +import 'package:flutter/foundation.dart'; // List builtInShortcutEvents = [ @@ -249,4 +251,14 @@ List builtInShortcutEvents = [ command: 'tab', handler: tabHandler, ), + // https://github.com/flutter/flutter/issues/104944 + // Workaround: Using space editing on the web platform often results in errors, + // so adding a shortcut event to handle the space input instead of using the + // `input_service`. + if (kIsWeb) + ShortcutEvent( + key: 'Space on the Web', + command: 'space', + handler: spaceOnWebHandler, + ), ]; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart new file mode 100644 index 0000000000..fbbe016d30 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/space_on_web_handler_test.dart @@ -0,0 +1,45 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import '../../infra/test_editor.dart'; + +void main() async { + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('space_on_web_handler.dart', () { + testWidgets('Presses space key on web', (tester) async { + if (!kIsWeb) return; + const count = 10; + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor; + for (var i = 0; i < count; i++) { + editor.insertTextNode(text); + } + await editor.startTesting(); + + for (var i = 0; i < count; i++) { + await editor.updateSelection( + Selection.single(path: [i], startOffset: 1), + ); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect( + (editor.nodeAtPath([i]) as TextNode).toRawString(), + 'W elcome to Appflowy 😁', + ); + } + for (var i = 0; i < count; i++) { + await editor.updateSelection( + Selection.single(path: [i], startOffset: text.length + 1), + ); + await editor.pressLogicKey(LogicalKeyboardKey.space); + expect( + (editor.nodeAtPath([i]) as TextNode).toRawString(), + 'W elcome to Appflowy 😁 ', + ); + } + }); + }); +} From 8d39dac1450fea85c819e7bdecac6370ad6beed1 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 29 Sep 2022 11:33:27 +0800 Subject: [PATCH 6/6] chore: disable IME support for the Web platform --- .../appflowy_editor/lib/src/service/input_service.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index 7f1a4718f5..d6a3420099 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -297,7 +297,11 @@ class _AppFlowyInputState extends State _updateCaretPosition(textNodes.first, selection); } } else { - // close(); + // https://github.com/flutter/flutter/issues/104944 + // Disable IME for the Web. + if (kIsWeb) { + close(); + } } }