diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 5c088b46d6..ba3d883c77 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -16,6 +16,7 @@ import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_han import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/whitespace_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/render_plugin_service.dart'; import 'package:flowy_editor/service/scroll_service.dart'; @@ -40,6 +41,7 @@ List defaultKeyEventHandler = [ copyPasteKeysHandler, enterInEdgeOfTextNodeHandler, updateTextStyleByCommandXHandler, + whiteSpaceHandler, ]; class FlowyEditor extends StatefulWidget { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart index 5f59f4266d..861449c0ab 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_text_handler.dart @@ -1,8 +1,9 @@ -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; + // Handle delete text. FlowyKeyEventHandler deleteTextHandler = (editorState, event) { if (event.logicalKey != LogicalKeyboardKey.backspace) { @@ -28,9 +29,16 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { if (index < 0) { // 1. style if (textNode.subtype != null) { - transactionBuilder.updateNode(textNode, { - 'subtype': null, - }); + transactionBuilder + ..updateNode(textNode, { + 'subtype': null, + }) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: 0, + ), + ); } else { // 2. non-style // find previous text node. diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart index 68bcdca7a6..f824b87234 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/slash_handler.dart @@ -72,16 +72,22 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { final rect = selectable.getCursorRectInPosition(selection.start); final offset = selectable.localToGlobal(rect.topLeft); - if (!selection.isCollapsed) { - TransactionBuilder(editorState) - ..deleteText( - textNode, - selection.start.offset, - selection.end.offset - selection.start.offset, - ) - ..commit(); - } + TransactionBuilder(editorState) + ..replaceText(textNode, selection.start.offset, + selection.end.offset - selection.start.offset, '/') + ..commit(); + + _editorState = editorState; + WidgetsBinding.instance.addPostFrameCallback((_) { + showPopupList(context, editorState, offset); + }); + + return KeyEventResult.handled; +}; + +void showPopupList( + BuildContext context, EditorState editorState, Offset offset) { _popupListOverlay?.remove(); _popupListOverlay = OverlayEntry( builder: (context) => Positioned( @@ -97,16 +103,12 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { Overlay.of(context)?.insert(_popupListOverlay!); editorState.service.selectionService.currentSelection - .removeListener(clearPopupListOverlay); + .removeListener(clearPopupList); editorState.service.selectionService.currentSelection - .addListener(clearPopupListOverlay); - // editorState.service.keyboardService?.disable(); - _editorState = editorState; + .addListener(clearPopupList); +} - return KeyEventResult.handled; -}; - -void clearPopupListOverlay() { +void clearPopupList() { _popupListOverlay?.remove(); _popupListOverlay = null; @@ -215,7 +217,7 @@ class _PopupListWidgetState extends State { } if (event.logicalKey == LogicalKeyboardKey.escape) { - clearPopupListOverlay(); + clearPopupList(); return KeyEventResult.handled; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/whitespace_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/whitespace_handler.dart new file mode 100644 index 0000000000..b3642cc1a1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/whitespace_handler.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; +import 'package:flowy_editor/service/keyboard_service.dart'; + +const _bulletedListSymbols = ['*', '-']; +const _checkboxListSymbols = ['[x]', '-[x]']; +const _unCheckboxListSymbols = ['[]', '-[]']; + +FlowyKeyEventHandler whiteSpaceHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.space) { + return KeyEventResult.ignored; + } + + /// Process markdown input style. + /// + /// like, #, *, -, 1., -[], + + final selection = editorState.service.selectionService.currentSelection.value; + if (selection == null || !selection.isCollapsed) { + return KeyEventResult.ignored; + } + + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final textNode = textNodes.first; + final text = textNode.toRawString(); + if ((_checkboxListSymbols + _unCheckboxListSymbols).any(text.startsWith)) { + return _toCheckboxList(editorState, textNode); + } else if (_bulletedListSymbols.any(text.startsWith)) { + return _toBulletedList(editorState, textNode); + } else if (_countOfSign(text) != 0) { + return _toHeadingStyle(editorState, textNode); + } + + return KeyEventResult.ignored; +}; + +KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) { + if (textNode.subtype == StyleKey.bulletedList) { + return KeyEventResult.ignored; + } + TransactionBuilder(editorState) + ..deleteText(textNode, 0, 1) + ..updateNode(textNode, { + StyleKey.subtype: StyleKey.bulletedList, + }) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: 0, + ), + ) + ..commit(); + return KeyEventResult.handled; +} + +KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) { + if (textNode.subtype == StyleKey.checkbox) { + return KeyEventResult.ignored; + } + final String symbol; + bool check = false; + final symbols = List.from(_checkboxListSymbols) + ..retainWhere(textNode.toRawString().startsWith); + if (symbols.isNotEmpty) { + symbol = symbols.first; + check = true; + } else { + symbol = (List.from(_unCheckboxListSymbols) + ..retainWhere(textNode.toRawString().startsWith)) + .first; + check = false; + } + + TransactionBuilder(editorState) + ..deleteText(textNode, 0, symbol.length) + ..updateNode(textNode, { + StyleKey.subtype: StyleKey.checkbox, + StyleKey.checkbox: check, + }) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: 0, + ), + ) + ..commit(); + return KeyEventResult.handled; +} + +KeyEventResult _toHeadingStyle(EditorState editorState, TextNode textNode) { + final x = _countOfSign(textNode.toRawString()); + final hX = 'h$x'; + if (textNode.attributes.heading == hX) { + return KeyEventResult.ignored; + } + TransactionBuilder(editorState) + ..deleteText(textNode, 0, x) + ..updateNode(textNode, { + StyleKey.subtype: StyleKey.heading, + StyleKey.heading: hX, + }) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path, + offset: 0, + ), + ) + ..commit(); + return KeyEventResult.handled; +} + +int _countOfSign(String text) { + for (var i = 6; i >= 0; i--) { + if (text.startsWith('#' * i)) { + return i; + } + } + return 0; +}