diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index c3f75c5c9c..21883362b1 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -183,7 +183,7 @@ class TextNode extends Node { super(children: children ?? LinkedList(), attributes: attributes ?? {}); TextNode.empty() - : _delta = Delta([TextInsert(' ')]), + : _delta = Delta([TextInsert('')]), super( type: 'text', children: LinkedList(), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart index 5c02955d7b..317d1a6bdf 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/checkbox_text.dart @@ -77,7 +77,7 @@ class _CheckboxNodeWidgetState extends State debugPrint('[Checkbox] onTap...'); TransactionBuilder(widget.editorState) ..updateNode(widget.textNode, { - 'checkbox': !check, + StyleKey.checkbox: !check, }) ..commit(); }, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart index 22dd892746..32715d8556 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/flowy_rich_text.dart @@ -35,7 +35,7 @@ class FlowyRichText extends StatefulWidget { this.cursorHeight, this.cursorWidth = 2.0, this.textSpanDecorator, - this.placeholderText = 'Type \'/\' for commands', + this.placeholderText = ' ', this.placeholderTextSpanDecorator, required this.textNode, required this.editorState, @@ -138,11 +138,16 @@ class _FlowyRichTextState extends State with Selectable { } Widget _buildRichText(BuildContext context) { - return Stack( - children: [ - _buildPlaceholderText(context), - _buildSingleRichText(context), - ], + return MouseRegion( + cursor: SystemMouseCursors.text, + child: widget.textNode.toRawString().isEmpty + ? Stack( + children: [ + _buildPlaceholderText(context), + _buildSingleRichText(context), + ], + ) + : _buildSingleRichText(context), ); } 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 ba3d883c77..78f7bb76fa 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 @@ -13,7 +13,7 @@ import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_hand import 'package:flowy_editor/service/internal_key_event_handlers/copy_paste_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_handler.dart'; -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/enter_without_shift_in_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'; @@ -39,7 +39,7 @@ List defaultKeyEventHandler = [ flowyDeleteNodesHandler, arrowKeysHandler, copyPasteKeysHandler, - enterInEdgeOfTextNodeHandler, + enterWithoutShiftInTextNodesHandler, updateTextStyleByCommandXHandler, whiteSpaceHandler, ]; 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 861449c0ab..e44f0002a0 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 @@ -46,8 +46,14 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) { if (textNode.previous is TextNode) { final previous = textNode.previous as TextNode; transactionBuilder + ..mergeText(previous, textNode) ..deleteNode(textNode) - ..mergeText(previous, textNode); + ..afterSelection = Selection.collapsed( + Position( + path: previous.path, + offset: previous.toRawString().length, + ), + ); break; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart deleted file mode 100644 index 9ea754e162..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'dart:collection'; - -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/document/text_delta.dart'; -import 'package:flowy_editor/extensions/node_extensions.dart'; -import 'package:flowy_editor/extensions/path_extensions.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'; - -FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.enter) { - return KeyEventResult.ignored; - } - - final nodes = editorState.service.selectionService.currentSelectedNodes; - final selection = editorState.service.selectionService.currentSelection.value; - if (selection == null || - nodes.length != 1 || - nodes.first is! TextNode || - !selection.isCollapsed) { - return KeyEventResult.ignored; - } - - final textNode = nodes.first as TextNode; - if (textNode.selectable!.end() == selection.end) { - if (textNode.subtype != null && textNode.delta.length == 0) { - TransactionBuilder(editorState) - ..deleteNode(textNode) - ..insertNode( - textNode.path, - textNode.copyWith( - children: LinkedList(), - delta: Delta([TextInsert('')]), - attributes: {}, - ), - ) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path, - offset: 0, - ), - ) - ..commit(); - } else { - final needCopyAttributes = StyleKey.globalStyleKeys - .where((key) => key != StyleKey.heading) - .contains(textNode.subtype); - TransactionBuilder(editorState) - ..insertNode( - textNode.path.next, - textNode.copyWith( - children: LinkedList(), - delta: Delta([TextInsert('')]), - attributes: needCopyAttributes ? textNode.attributes : {}, - ), - ) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path.next, - offset: 0, - ), - ) - ..commit(); - } - - return KeyEventResult.handled; - } else if (textNode.selectable!.start() == selection.start) { - TransactionBuilder(editorState) - ..insertNode( - textNode.path, - textNode.copyWith( - children: LinkedList(), - delta: Delta([TextInsert('')]), - attributes: {}, - ), - ) - ..afterSelection = Selection.collapsed( - Position( - path: textNode.path.next, - offset: 0, - ), - ) - ..commit(); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; -}; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart new file mode 100644 index 0000000000..cf71830386 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -0,0 +1,106 @@ +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/extensions/path_extensions.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'; + +/// Handle some cases where enter is pressed and shift is not pressed. +/// +/// 1. Multiple selection and the selected nodes are [TextNode] +/// 1.1 delete the nodes expect for the first and the last, +/// and delete the text in the first and the last node by case. +/// 2. Single selection and the selected node is [TextNode] +/// 2.1 split the node into two nodes with style +/// 2.2 or insert a empty text node before. +FlowyKeyEventHandler enterWithoutShiftInTextNodesHandler = + (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.enter || event.isShiftPressed) { + return KeyEventResult.ignored; + } + + final nodes = editorState.service.selectionService.currentSelectedNodes; + final textNodes = nodes.whereType().toList(growable: false); + final selection = editorState.service.selectionService.currentSelection.value; + + if (selection == null || nodes.length != textNodes.length) { + return KeyEventResult.ignored; + } + + // Multiple selection + if (!selection.isSingle) { + final length = textNodes.length; + final List subTextNodes = + length >= 3 ? textNodes.sublist(1, textNodes.length - 2) : []; + final afterSelection = Selection.collapsed( + Position(path: textNodes.first.path.next, offset: 0), + ); + TransactionBuilder(editorState) + ..deleteText( + textNodes.first, + selection.start.offset, + textNodes.first.toRawString().length, + ) + ..deleteNodes(subTextNodes) + ..deleteText( + textNodes.last, + 0, + selection.end.offset, + ) + ..afterSelection = afterSelection + ..commit(); + return KeyEventResult.handled; + } + + // Single selection and the selected node is [TextNode] + if (textNodes.length != 1) { + return KeyEventResult.ignored; + } + + final textNode = textNodes.first; + + // If selection is collapsed and position.start.offset == 0, + // insert a empty text node before. + if (selection.isCollapsed && selection.start.offset == 0) { + final afterSelection = Selection.collapsed( + Position(path: textNode.path.next, offset: 0), + ); + TransactionBuilder(editorState) + ..insertNode( + textNode.path, + TextNode.empty(), + ) + ..afterSelection = afterSelection + ..commit(); + return KeyEventResult.handled; + } + + // Otherwise, + // split the node into two nodes with style + final needCopyAttributes = StyleKey.globalStyleKeys + .where((key) => key != StyleKey.heading) + .contains(textNode.subtype); + final afterSelection = Selection.collapsed( + Position(path: textNode.path.next, offset: 0), + ); + TransactionBuilder(editorState) + ..insertNode( + textNode.path.next, + textNode.copyWith( + attributes: needCopyAttributes ? textNode.attributes : {}, + delta: textNode.delta.slice(selection.end.offset), + ), + ) + ..deleteText( + textNode, + selection.start.offset, + textNode.toRawString().length - selection.start.offset, + ) + ..afterSelection = afterSelection + ..commit(); + return KeyEventResult.handled; +};