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 😁 ', + ); + } + }); + }); +}