diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json index 9f1d278e16..c69237f24f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/example.json @@ -144,7 +144,7 @@ } ], "attributes": { - "subtype": "bullet-list" + "subtype": "bulleted-list" } }, { @@ -155,7 +155,7 @@ } ], "attributes": { - "subtype": "bullet-list" + "subtype": "bulleted-list" } }, { @@ -170,7 +170,7 @@ } ], "attributes": { - "subtype": "bullet-list" + "subtype": "bulleted-list" } }, { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 158e33bbb1..1a68f38ead 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -116,13 +116,16 @@ class _MyHomePageState extends State { _editorState = EditorState( document: document, ); - return FlowyEditor( - key: editorKey, - editorState: _editorState, - keyEventHandlers: const [], - customBuilders: { - 'image': ImageNodeBuilder(), - }, + return Container( + padding: const EdgeInsets.only(left: 20, right: 20), + child: FlowyEditor( + key: editorKey, + editorState: _editorState, + keyEventHandlers: const [], + customBuilders: { + 'image': ImageNodeBuilder(), + }, + ), // shortcuts: [ // // TODO: this won't work, just a example for now. // { diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 25d432b759..6a01fb6430 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,6 +1,23 @@ import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; +/// 1. define your custom type in example.json +/// For example I need to define an image plugin, then I define type equals +/// "image", and add "image_src" into "attributes". +/// { +/// "type": "image", +/// "attributes", { "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" } +/// } +/// 2. create a class extends [NodeWidgetBuilder] +/// 3. override the function `Widget build(NodeWidgetContext context)` +/// and return a widget to render. The returned widget should be +/// a StatefulWidget and mixin with [Selectable]. +/// +/// 4. override the getter `nodeValidator` +/// to verify the data structure in [Node]. +/// 5. register the plugin with `type` to `flowy_editor` in `main.dart`. +/// 6. Congratulations! + class ImageNodeBuilder extends NodeWidgetBuilder { @override Widget build(NodeWidgetContext context) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart index 6e845420ef..4e1f39775f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/attributes.dart @@ -26,7 +26,7 @@ Attributes? composeAttributes(Attributes? a, Attributes? b) { a ??= {}; b ??= {}; final Attributes attributes = {}; - attributes.addAll(b); + attributes.addAll(Map.from(b)..removeWhere((_, value) => value == null)); for (final entry in a.entries) { if (!b.containsKey(entry.key)) { 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 bdd6da444d..3a7ad36456 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -89,7 +89,11 @@ class Node extends ChangeNotifier with LinkedListEntry { this.attributes['subtype'] != attributes['subtype']; for (final attribute in attributes.entries) { - this.attributes[attribute.key] = attribute.value; + if (attribute.value == null) { + this.attributes.remove(attribute.key); + } else { + this.attributes[attribute.key] = attribute.value; + } } // Notify the new attributes // if attributes contains 'subtype', should notify parent to rebuild node @@ -178,7 +182,7 @@ class TextNode extends Node { }) : _delta = delta; TextNode.empty() - : _delta = Delta([TextInsert('')]), + : _delta = Delta([TextInsert(' ')]), super( type: 'text', children: LinkedList(), @@ -201,6 +205,19 @@ class TextNode extends Node { return map; } + TextNode copyWith({ + String? type, + LinkedList? children, + Attributes? attributes, + Delta? delta, + }) => + TextNode( + type: type ?? this.type, + children: children ?? this.children, + attributes: attributes ?? this.attributes, + delta: delta ?? this.delta, + ); + // TODO: It's unneccesry to compute everytime. String toRawString() => _delta.operations.whereType().map((op) => op.content).join(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index 277b742604..92a05fc880 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -60,7 +60,12 @@ class EditorState { for (final op in transaction.operations) { _applyOperation(op); } - updateCursorSelection(transaction.afterSelection); + // updateCursorSelection(transaction.afterSelection); + + // FIXME: don't use delay + Future.delayed(const Duration(milliseconds: 16), () { + updateCursorSelection(transaction.afterSelection); + }); if (options.recordUndo) { final undoItem = undoManager.getUndoHistoryItem(); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index e1d76c81fe..88e0c00890 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -1,18 +1,18 @@ import 'dart:collection'; -import 'package:flowy_editor/editor_state.dart'; + +import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/position.dart'; -import 'package:flowy_editor/document/text_delta.dart'; -import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/selection.dart'; - -import './operation.dart'; -import './transaction.dart'; +import 'package:flowy_editor/document/text_delta.dart'; +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/operation/transaction.dart'; /// A [TransactionBuilder] is used to build the transaction from the state. /// It will save make a snapshot of the cursor selection state automatically. -/// The cursor can be resoted if the transaction is undo. +/// The cursor can be resorted if the transaction is undo. class TransactionBuilder { final List operations = []; @@ -70,7 +70,7 @@ class TransactionBuilder { } textEdit(TextNode node, Delta Function() f) { - beforeSelection = state.service.selectionService.currentSelection; + beforeSelection = state.cursorSelection; final path = node.path; final delta = f(); 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 4d52e41867..ba2c5b8712 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 @@ -21,7 +21,7 @@ class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder { @override NodeValidator get nodeValidator => ((node) { - return node.attributes.containsKey(StyleKey.check); + return node.attributes.containsKey(StyleKey.checkbox); }); } 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 70834184cc..f302fcaba8 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 @@ -74,7 +74,7 @@ class _FlowyRichTextState extends State with Selectable { _renderParagraph.getOffsetForCaret(textPosition, Rect.zero); final cursorHeight = widget.cursorHeight ?? _renderParagraph.getFullHeightForCaret(textPosition) ?? - 5.0; // default height + 18.0; // default height return Rect.fromLTWH( cursorOffset.dx - (widget.cursorWidth / 2), cursorOffset.dy, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart index 26d4275774..cc4f6038ac 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/rich_text/rich_text_style.dart @@ -25,26 +25,49 @@ class StyleKey { static String font = 'font'; static String href = 'href'; - static String quote = 'quote'; - static String list = 'list'; - static String number = 'number'; - static String todo = 'todo'; - static String code = 'code'; - static String subtype = 'subtype'; - static String check = 'checkbox'; static String heading = 'heading'; + static String h1 = 'h1'; + static String h2 = 'h2'; + static String h3 = 'h3'; + static String h4 = 'h4'; + static String h5 = 'h5'; + static String h6 = 'h6'; + + static String bulletedList = 'bulleted-list'; + static String numberList = 'number-list'; + + static String quote = 'quote'; + static String checkbox = 'checkbox'; + static String code = 'code'; + static String number = 'number'; + + static List partialStyleKeys = [ + StyleKey.bold, + StyleKey.italic, + StyleKey.underline, + StyleKey.strikethrough, + ]; + + static List globalStyleKeys = [ + StyleKey.heading, + StyleKey.checkbox, + StyleKey.bulletedList, + StyleKey.numberList, + StyleKey.quote, + StyleKey.code, + ]; } double baseFontSize = 16.0; // TODO: customize. Map headingToFontSize = { - 'h1': baseFontSize + 15, - 'h2': baseFontSize + 12, - 'h3': baseFontSize + 9, - 'h4': baseFontSize + 6, - 'h5': baseFontSize + 3, - 'h6': baseFontSize, + StyleKey.h1: baseFontSize + 15, + StyleKey.h2: baseFontSize + 12, + StyleKey.h3: baseFontSize + 9, + StyleKey.h4: baseFontSize + 6, + StyleKey.h5: baseFontSize + 3, + StyleKey.h6: baseFontSize, }; extension NodeAttributesExtensions on Attributes { @@ -73,13 +96,6 @@ extension NodeAttributesExtensions on Attributes { return null; } - String? get list { - if (containsKey(StyleKey.list) && this[StyleKey.list] is String) { - return this[StyleKey.list]; - } - return null; - } - int? get number { if (containsKey(StyleKey.number) && this[StyleKey.number] is int) { return this[StyleKey.number]; @@ -87,13 +103,6 @@ extension NodeAttributesExtensions on Attributes { return null; } - bool get todo { - if (containsKey(StyleKey.todo) && this[StyleKey.todo] is bool) { - return this[StyleKey.todo]; - } - return false; - } - bool get code { if (containsKey(StyleKey.code) && this[StyleKey.code] == true) { return this[StyleKey.code]; @@ -102,8 +111,8 @@ extension NodeAttributesExtensions on Attributes { } bool get check { - if (containsKey(StyleKey.check) && this[StyleKey.check] is bool) { - return this[StyleKey.check]; + if (containsKey(StyleKey.checkbox) && this[StyleKey.checkbox] is bool) { + return this[StyleKey.checkbox]; } return false; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart index 7266929962..91659e1d1f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/toolbar_widget.dart @@ -1,43 +1,39 @@ -import 'package:flowy_editor/editor_state.dart'; -import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; -typedef ToolbarEventHandler = void Function( - EditorState editorState, String eventName); +import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/infra/flowy_svg.dart'; +import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; -typedef ToolbarEventHandlers = List>; -ToolbarEventHandlers defaultToolbarEventHandlers = [ - { - 'bold': ((editorState, eventName) {}), - 'italic': ((editorState, eventName) {}), - 'strikethrough': ((editorState, eventName) {}), - 'underline': ((editorState, eventName) {}), - 'quote': ((editorState, eventName) {}), - 'number_list': ((editorState, eventName) {}), - 'bulleted_list': ((editorState, eventName) {}), - } -]; +typedef ToolbarEventHandler = void Function(EditorState editorState); -ToolbarEventHandlers defaultListToolbarEventHandlers = [ - { - 'h1': ((editorState, eventName) {}), - }, - { - 'h2': ((editorState, eventName) {}), - }, - { - 'h3': ((editorState, eventName) {}), - }, - { - 'bulleted_list': ((editorState, eventName) {}), - }, - { - 'quote': ((editorState, eventName) {}), - } +typedef ToolbarEventHandlers = Map; + +ToolbarEventHandlers defaultToolbarEventHandlers = { + 'bold': (editorState) => formatBold(editorState), + 'italic': (editorState) => formatItalic(editorState), + 'strikethrough': (editorState) => formatStrikethrough(editorState), + 'underline': (editorState) => formatUnderline(editorState), + 'quote': (editorState) => formatQuote(editorState), + 'number_list': (editorState) {}, + 'bulleted_list': (editorState) => formatBulletedList(editorState), + 'Text': (editorState) => formatText(editorState), + 'H1': (editorState) => formatHeading(editorState, StyleKey.h1), + 'H2': (editorState) => formatHeading(editorState, StyleKey.h2), + 'H3': (editorState) => formatHeading(editorState, StyleKey.h3), +}; + +List defaultListToolbarEventNames = [ + 'Text', + 'H1', + 'H2', + 'H3', + // 'B-List', + // 'N-List', ]; class ToolbarWidget extends StatefulWidget { - ToolbarWidget({ + const ToolbarWidget({ Key? key, required this.editorState, required this.layerLink, @@ -137,7 +133,7 @@ class _ToolbarWidgetState extends State { preferBelow: false, message: name, child: GestureDetector( - onTap: onTap ?? () => debugPrint('toolbar tap $name'), + onTap: onTap ?? () => _onTap(name), child: SizedBox.fromSize( size: width != null ? Size(width, toolbarHeight) @@ -154,9 +150,7 @@ class _ToolbarWidgetState extends State { void _onTapListToolbar(BuildContext context) { // TODO: implement more detailed UI. - final items = defaultListToolbarEventHandlers - .map((handler) => handler.keys.first) - .toList(growable: false); + final items = defaultListToolbarEventNames; final renderBox = _listToolbarKey.currentContext?.findRenderObject() as RenderBox; final offset = renderBox @@ -198,7 +192,7 @@ class _ToolbarWidgetState extends State { ), ), onTap: () { - debugPrint('tap on $index'); + _onTap(items[index]); }, ); }), @@ -210,6 +204,14 @@ class _ToolbarWidgetState extends State { Overlay.of(context)?.insert(_listToolbarOverlay!); } + void _onTap(String eventName) { + if (defaultToolbarEventHandlers.containsKey(eventName)) { + defaultToolbarEventHandlers[eventName]!(widget.editorState); + return; + } + assert(false, 'Could not find the event handler for $eventName'); + } + void _onSelectionChange() { _listToolbarOverlay?.remove(); _listToolbarOverlay = null; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart index 5622785d45..79e7bfe077 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/default_text_operations/format_rich_text_style.dart @@ -1,9 +1,103 @@ +import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/editor_state.dart'; +import 'package:flowy_editor/extensions/text_node_extensions.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; +import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; -bool formatRichTextStyle( - EditorState editorState, Map attributes) { +void formatText(EditorState editorState) { + formatTextNodes(editorState, {}); +} + +void formatHeading(EditorState editorState, String heading) { + formatTextNodes(editorState, { + StyleKey.subtype: StyleKey.heading, + StyleKey.heading: heading, + }); +} + +void formatQuote(EditorState editorState) { + formatTextNodes(editorState, { + StyleKey.subtype: StyleKey.quote, + }); +} + +void formatCheckbox(EditorState editorState) { + formatTextNodes(editorState, { + StyleKey.subtype: StyleKey.checkbox, + StyleKey.checkbox: false, + }); +} + +void formatBulletedList(EditorState editorState) { + formatTextNodes(editorState, { + StyleKey.subtype: StyleKey.bulletedList, + }); +} + +bool formatTextNodes(EditorState editorState, Attributes attributes) { + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final textNodes = nodes.whereType().toList(); + + if (textNodes.isEmpty) { + return false; + } + + final builder = TransactionBuilder(editorState); + + for (final textNode in textNodes) { + builder.updateNode( + textNode, + Attributes.fromIterable( + StyleKey.globalStyleKeys, + value: (_) => null, + )..addAll(attributes), + ); + } + + builder.commit(); + return true; +} + +bool formatBold(EditorState editorState) { + return formatRichTextPartialStyle(editorState, StyleKey.bold); +} + +bool formatItalic(EditorState editorState) { + return formatRichTextPartialStyle(editorState, StyleKey.italic); +} + +bool formatUnderline(EditorState editorState) { + return formatRichTextPartialStyle(editorState, StyleKey.underline); +} + +bool formatStrikethrough(EditorState editorState) { + return formatRichTextPartialStyle(editorState, StyleKey.strikethrough); +} + +bool formatRichTextPartialStyle(EditorState editorState, String styleKey) { + final selection = editorState.service.selectionService.currentSelection; + final nodes = editorState.service.selectionService.currentSelectedNodes.value; + final textNodes = nodes.whereType().toList(growable: false); + + if (selection == null || textNodes.isEmpty) { + return false; + } + + bool value = !textNodes.allSatisfyInSelection(styleKey, selection); + Attributes attributes = { + styleKey: value, + }; + if (styleKey == StyleKey.underline && value) { + attributes[StyleKey.strikethrough] = null; + } else if (styleKey == StyleKey.strikethrough && value) { + attributes[StyleKey.underline] = null; + } + + return formatRichTextStyle(editorState, attributes); +} + +bool formatRichTextStyle(EditorState editorState, Attributes attributes) { final selection = editorState.service.selectionService.currentSelection; final nodes = editorState.service.selectionService.currentSelectedNodes.value; final textNodes = nodes.whereType().toList(); @@ -17,29 +111,38 @@ bool formatRichTextStyle( // 1. All nodes are text nodes. // 2. The first node is not TextNode. // 3. The last node is not TextNode. - for (var i = 0; i < textNodes.length; i++) { - final textNode = textNodes[i]; - if (i == 0 && textNode == nodes.first) { - builder.formatText( - textNode, - selection.start.offset, - textNode.toRawString().length - selection.start.offset, - attributes, - ); - } else if (i == textNodes.length - 1 && textNode == nodes.last) { - builder.formatText( - textNode, - 0, - selection.end.offset, - attributes, - ); - } else { - builder.formatText( - textNode, - 0, - textNode.toRawString().length, - attributes, - ); + if (nodes.length == textNodes.length && textNodes.length == 1) { + builder.formatText( + textNodes.first, + selection.start.offset, + selection.end.offset - selection.start.offset, + attributes, + ); + } else { + for (var i = 0; i < textNodes.length; i++) { + final textNode = textNodes[i]; + if (i == 0 && textNode == nodes.first) { + builder.formatText( + textNode, + selection.start.offset, + textNode.toRawString().length - selection.start.offset, + attributes, + ); + } else if (i == textNodes.length - 1 && textNode == nodes.last) { + builder.formatText( + textNode, + 0, + selection.end.offset, + attributes, + ); + } else { + builder.formatText( + textNode, + 0, + textNode.toRawString().length, + attributes, + ); + } } } 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 6d21699625..b62fe1bb15 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 @@ -25,7 +25,7 @@ NodeWidgetBuilders defaultBuilders = { 'text': RichTextNodeWidgetBuilder(), 'text/checkbox': CheckboxNodeWidgetBuilder(), 'text/heading': HeadingTextNodeWidgetBuilder(), - 'text/bullet-list': BulletedListTextNodeWidgetBuilder(), + 'text/bulleted-list': BulletedListTextNodeWidgetBuilder(), 'text/number-list': NumberListTextNodeWidgetBuilder(), 'text/quote': QuotedTextNodeWidgetBuilder(), }; 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 index d1e89d393e..ccdfcad5dc 100644 --- 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 @@ -1,12 +1,17 @@ +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/operation/transaction_builder.dart'; -import 'package:flowy_editor/service/keyboard_service.dart'; -import 'package:flowy_editor/extensions/path_extensions.dart'; +import 'package:flowy_editor/document/text_delta.dart'; import 'package:flowy_editor/extensions/node_extensions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.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) { @@ -23,20 +28,63 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) { } final textNode = nodes.first as TextNode; - if (textNode.selectable!.end() == selection.end) { - TransactionBuilder(editorState) - ..insertNode( - textNode.path.next, - TextNode.empty(), - ) - ..commit(); + 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.empty(), + textNode.copyWith( + children: LinkedList(), + delta: Delta([TextInsert('')]), + attributes: {}, + ), + ) + ..afterSelection = Selection.collapsed( + Position( + path: textNode.path.next, + offset: 0, + ), ) ..commit(); return KeyEventResult.handled; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart index 6e4b742785..b062480cf2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/extensions/text_node_extensions.dart'; -import 'package:flowy_editor/render/rich_text/rich_text_style.dart'; import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; @@ -23,9 +21,7 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) { // bold case 'B': case 'b': - formatRichTextStyle(editorState, { - StyleKey.bold: !textNodes.allSatisfyBoldInSelection(selection), - }); + formatBold(editorState); return KeyEventResult.handled; default: break; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 0695dd5e90..59632773e5 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -233,6 +233,9 @@ class _FlowySelectionState extends State @override void dispose() { + clearSelection(); + WidgetsBinding.instance.removeObserver(this); + super.dispose(); } @@ -455,7 +458,7 @@ class _FlowySelectionState extends State ..forEach((overlay) => overlay.remove()) ..clear(); // clear toolbar - editorState.service.toolbarService.hide(); + editorState.service.toolbarService?.hide(); } void _updateSelection(Selection selection) { @@ -526,7 +529,7 @@ class _FlowySelectionState extends State if (topmostRect != null && layerLink != null) { editorState.service.toolbarService - .showInOffset(topmostRect.topLeft, layerLink); + ?.showInOffset(topmostRect.topLeft, layerLink); } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 829ad2bde1..937a16044a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -23,9 +23,11 @@ class FlowyService { // toolbar service final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service'); - ToolbarService get toolbarService { - assert(toolbarServiceKey.currentState != null && - toolbarServiceKey.currentState is ToolbarService); - return toolbarServiceKey.currentState! as ToolbarService; + ToolbarService? get toolbarService { + if (toolbarServiceKey.currentState != null && + toolbarServiceKey.currentState is ToolbarService) { + return toolbarServiceKey.currentState! as ToolbarService; + } + return null; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart index b8b8f95e46..feb293aad4 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/toolbar_service.dart @@ -35,7 +35,7 @@ class _FlowyToolbarState extends State with ToolbarService { editorState: widget.editorState, layerLink: layerLink, offset: offset.translate(0, -37.0), - handlers: const [], + handlers: const {}, ), ); Overlay.of(context)?.insert(_toolbarOverlay!); diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index 7507cb65bf..339807cea4 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -8,6 +8,7 @@ import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/state_tree.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); group('transform path', () { test('transform path changed', () { expect(transformPath([0, 1], [0, 1]), [0, 2]); @@ -87,7 +88,7 @@ void main() { "path": [0], "nodes": [item1.toJson()], } - ], + ] }); }); test("delete", () {