From 49fb0470ab70a89c8a1104278be96c3322cd250d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 26 Sep 2022 16:54:13 +0800 Subject: [PATCH 1/8] fix: should not notify the parent node when the subtype is not changed --- .../packages/appflowy_editor/lib/src/document/node.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart index 81e87399b1..63e6525754 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart @@ -93,12 +93,14 @@ class Node extends ChangeNotifier with LinkedListEntry { } void updateAttributes(Attributes attributes) { - bool shouldNotifyParent = _attributes['subtype'] != attributes['subtype']; - + final oldAttributes = {..._attributes}; _attributes = composeAttributes(_attributes, attributes) ?? {}; + // Notifies the new attributes // if attributes contains 'subtype', should notify parent to rebuild node // else, just notify current node. + bool shouldNotifyParent = + _attributes['subtype'] != oldAttributes['subtype']; shouldNotifyParent ? parent?.notifyListeners() : notifyListeners(); } From ec97735e947217e9d1fda9abb134fbe825cf492d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 26 Sep 2022 16:55:06 +0800 Subject: [PATCH 2/8] fix: prevent to copy the style in some cases when pressing the enter key in the front of the text --- .../enter_without_shift_in_text_node_handler.dart | 9 ++++++++- .../lib/src/service/keyboard_service.dart | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart index a981470f6e..c31b8c3699 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart @@ -117,12 +117,17 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = makeFollowingNodesIncremental(editorState, insertPath, afterSelection, beginNum: prevNumber); } else { + bool needCopyAttributes = ![ + BuiltInAttributeKey.heading, + BuiltInAttributeKey.quote, + ].contains(subtype); TransactionBuilder(editorState) ..insertNode( textNode.path, textNode.copyWith( children: LinkedList(), delta: Delta(), + attributes: needCopyAttributes ? null : {}, ), ) ..afterSelection = afterSelection @@ -173,7 +178,9 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler = Attributes _attributesFromPreviousLine(TextNode textNode) { final prevAttributes = textNode.attributes; final subType = textNode.subtype; - if (subType == null || subType == BuiltInAttributeKey.heading) { + if (subType == null || + subType == BuiltInAttributeKey.heading || + subType == BuiltInAttributeKey.quote) { return {}; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart index 5259872b95..d5154bc2b5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/keyboard_service.dart @@ -124,6 +124,8 @@ class _AppFlowyKeyboardState extends State final result = shortcutEvent.handler(widget.editorState, event); if (result == KeyEventResult.handled) { return KeyEventResult.handled; + } else if (result == KeyEventResult.skipRemainingHandlers) { + return KeyEventResult.skipRemainingHandlers; } continue; } From 2d32e02dba110e6a207b7fd13465a13f0cc582d8 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 26 Sep 2022 16:55:32 +0800 Subject: [PATCH 3/8] feat: tab for 4 spaces --- .../src/service/internal_key_event_handlers/tab_handler.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart index 0eb36fff17..d0d594d41e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart @@ -16,6 +16,9 @@ ShortcutEventHandler tabHandler = (editorState, event) { if (textNode.subtype != BuiltInAttributeKey.bulletedList || previous == null || previous.subtype != BuiltInAttributeKey.bulletedList) { + TransactionBuilder(editorState) + ..insertText(textNode, selection.end.offset, ' ' * 4) + ..commit(); return KeyEventResult.handled; } From ff9cf905fae7c89832fc79eda85c1b7a1d2da087 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 26 Sep 2022 16:55:49 +0800 Subject: [PATCH 4/8] feat: implement simple code block --- .../example/assets/example.json | 5 + .../appflowy_editor/example/lib/main.dart | 9 + .../lib/plugin/code_block_node_widget.dart | 275 ++++++++++++++++++ .../appflowy_editor/example/pubspec.yaml | 2 + .../appflowy_editor/lib/appflowy_editor.dart | 4 + .../lib/src/service/editor_service.dart | 2 +- 6 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index 2d441d3367..d7fc310573 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -25,6 +25,11 @@ } ] }, + { + "type": "text", + "attributes": { "subtype": "code_block", "theme": "vs", "language": "dart" }, + "delta": [{ "insert": "final x = 0;\nprint($x);" }] + }, { "type": "text", "delta": [] }, { "type": "text", diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart index fd5ccdeff3..744359052f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:example/plugin/code_block_node_widget.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -116,9 +117,17 @@ class _MyHomePageState extends State { editorState: _editorState!, editorStyle: _editorStyle, editable: true, + customBuilders: { + 'text/code_block': CodeBlockNodeWidgetBuilder(), + }, shortcutEvents: [ + enterInCodeBlock, + ignoreKeysInCodeBlock, underscoreToItalic, ], + selectionMenuItems: [ + codeBlockItem, + ], ), ); } else { diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart new file mode 100644 index 0000000000..b36cbbd8ac --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart @@ -0,0 +1,275 @@ +import 'dart:collection'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:highlight/highlight.dart' as highlight; +import 'package:highlight/languages/all.dart'; + +ShortcutEvent enterInCodeBlock = ShortcutEvent( + key: 'Enter in code block', + command: 'enter', + handler: _enterInCodeBlockHandler, +); + +ShortcutEvent ignoreKeysInCodeBlock = ShortcutEvent( + key: 'White space in code block', + command: 'space,slash,shift+underscore', + handler: _ignorekHandler, +); + +ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) { + final selection = editorState.service.selectionService.currentSelection.value; + final nodes = editorState.service.selectionService.currentSelectedNodes; + final codeBlockNode = + nodes.whereType().where((node) => node.id == 'text/code_block'); + if (codeBlockNode.length != 1 || selection == null) { + return KeyEventResult.ignored; + } + if (selection.isCollapsed) { + TransactionBuilder(editorState) + ..insertText(codeBlockNode.first, selection.end.offset, '\n') + ..commit(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; +}; + +ShortcutEventHandler _ignorekHandler = (editorState, event) { + final nodes = editorState.service.selectionService.currentSelectedNodes; + final codeBlockNodes = + nodes.whereType().where((node) => node.id == 'text/code_block'); + if (codeBlockNodes.length == 1) { + return KeyEventResult.skipRemainingHandlers; + } + return KeyEventResult.ignored; +}; + +SelectionMenuItem codeBlockItem = SelectionMenuItem( + name: 'Code Block', + icon: const Icon(Icons.abc), + keywords: ['code block'], + handler: (editorState, _, __) { + final selection = + editorState.service.selectionService.currentSelection.value; + final textNodes = editorState.service.selectionService.currentSelectedNodes + .whereType(); + if (selection == null || textNodes.isEmpty) { + return; + } + if (textNodes.first.toRawString().isEmpty) { + TransactionBuilder(editorState) + ..updateNode(textNodes.first, { + 'subtype': 'code_block', + 'theme': 'vs', + 'language': null, + }) + ..afterSelection = selection + ..commit(); + } else { + TransactionBuilder(editorState) + ..insertNode( + selection.end.path.next, + TextNode( + type: 'text', + children: LinkedList(), + attributes: { + 'subtype': 'code_block', + 'theme': 'vs', + 'language': null, + }, + delta: Delta()..insert('\n'), + ), + ) + ..afterSelection = selection + ..commit(); + } + }, +); + +class CodeBlockNodeWidgetBuilder extends NodeWidgetBuilder { + @override + Widget build(NodeWidgetContext context) { + return _CodeBlockNodeWidge( + key: context.node.key, + textNode: context.node, + editorState: context.editorState, + ); + } + + @override + NodeValidator get nodeValidator => (node) { + return node is TextNode && node.attributes['theme'] is String; + }; +} + +class _CodeBlockNodeWidge extends StatefulWidget { + const _CodeBlockNodeWidge({ + Key? key, + required this.textNode, + required this.editorState, + }) : super(key: key); + + final TextNode textNode; + final EditorState editorState; + + @override + State<_CodeBlockNodeWidge> createState() => __CodeBlockNodeWidgeState(); +} + +class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> + with SelectableMixin, DefaultSelectable { + final _richTextKey = GlobalKey(debugLabel: 'code_block_text'); + final _padding = const EdgeInsets.only(left: 20, top: 20, bottom: 20); + String? get _language => widget.textNode.attributes['language'] as String?; + + @override + SelectableMixin get forward => + _richTextKey.currentState as SelectableMixin; + + @override + GlobalKey>? get iconKey => null; + + @override + Offset get baseOffset => super.baseOffset + _padding.topLeft; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _buildCodeBlock(context), + _buildSwitchCodeButton(context), + ], + ); + } + + Widget _buildCodeBlock(BuildContext context) { + final result = highlight.highlight.parse( + widget.textNode.toRawString(), + language: _language, + autoDetection: _language == null, + ); + final code = result.nodes; + final codeTextSpan = _convert(code!); + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + color: Colors.grey.withOpacity(0.1), + ), + padding: _padding, + width: MediaQuery.of(context).size.width, + child: FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, + textSpanDecorator: (textSpan) => TextSpan( + style: widget.editorState.editorStyle.textStyle.defaultTextStyle, + children: codeTextSpan, + ), + ), + ); + } + + Widget _buildSwitchCodeButton(BuildContext context) { + return Positioned( + top: -5, + right: 0, + child: DropdownButton( + value: _language, + onChanged: (value) { + TransactionBuilder(widget.editorState) + ..updateNode(widget.textNode, { + 'language': value, + }) + ..commit(); + }, + items: allLanguages.keys.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle(fontSize: 12.0), + ), + ); + }).toList(growable: false), + ), + ); + } + + // Copy from flutter.highlight package. + // https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart + List _convert(List nodes) { + List spans = []; + var currentSpans = spans; + List> stack = []; + + _traverse(highlight.Node node) { + if (node.value != null) { + currentSpans.add(node.className == null + ? TextSpan(text: node.value) + : TextSpan( + text: node.value, + style: _builtInCodeBlockTheme[node.className!])); + } else if (node.children != null) { + List tmp = []; + currentSpans.add(TextSpan( + children: tmp, style: _builtInCodeBlockTheme[node.className!])); + stack.add(currentSpans); + currentSpans = tmp; + + for (var n in node.children!) { + _traverse(n); + if (n == node.children!.last) { + currentSpans = stack.isEmpty ? spans : stack.removeLast(); + } + } + } + } + + for (var node in nodes) { + _traverse(node); + } + + return spans; + } +} + +const _builtInCodeBlockTheme = { + 'root': + TextStyle(backgroundColor: Color(0xffffffff), color: Color(0xff000000)), + 'comment': TextStyle(color: Color(0xff007400)), + 'quote': TextStyle(color: Color(0xff007400)), + 'tag': TextStyle(color: Color(0xffaa0d91)), + 'attribute': TextStyle(color: Color(0xffaa0d91)), + 'keyword': TextStyle(color: Color(0xffaa0d91)), + 'selector-tag': TextStyle(color: Color(0xffaa0d91)), + 'literal': TextStyle(color: Color(0xffaa0d91)), + 'name': TextStyle(color: Color(0xffaa0d91)), + 'variable': TextStyle(color: Color(0xff3F6E74)), + 'template-variable': TextStyle(color: Color(0xff3F6E74)), + 'code': TextStyle(color: Color(0xffc41a16)), + 'string': TextStyle(color: Color(0xffc41a16)), + 'meta-string': TextStyle(color: Color(0xffc41a16)), + 'regexp': TextStyle(color: Color(0xff0E0EFF)), + 'link': TextStyle(color: Color(0xff0E0EFF)), + 'title': TextStyle(color: Color(0xff1c00cf)), + 'symbol': TextStyle(color: Color(0xff1c00cf)), + 'bullet': TextStyle(color: Color(0xff1c00cf)), + 'number': TextStyle(color: Color(0xff1c00cf)), + 'section': TextStyle(color: Color(0xff643820)), + 'meta': TextStyle(color: Color(0xff643820)), + 'type': TextStyle(color: Color(0xff5c2699)), + 'built_in': TextStyle(color: Color(0xff5c2699)), + 'builtin-name': TextStyle(color: Color(0xff5c2699)), + 'params': TextStyle(color: Color(0xff5c2699)), + 'attr': TextStyle(color: Color(0xff836C28)), + 'subst': TextStyle(color: Color(0xff000000)), + 'formula': TextStyle( + backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic), + 'addition': TextStyle(backgroundColor: Color(0xffbaeeba)), + 'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)), + 'selector-id': TextStyle(color: Color(0xff9b703f)), + 'selector-class': TextStyle(color: Color(0xff9b703f)), + 'doctag': TextStyle(fontWeight: FontWeight.bold), + 'strong': TextStyle(fontWeight: FontWeight.bold), + 'emphasis': TextStyle(fontStyle: FontStyle.italic), +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 3c3f51632e..432941656d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: sdk: flutter file_picker: ^5.0.1 universal_html: ^2.0.8 + highlight: ^0.7.0 + flutter_highlight: ^0.7.0 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart index 0c9f447145..b594262e95 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart @@ -28,4 +28,8 @@ export 'src/service/shortcut_event/keybinding.dart'; export 'src/service/shortcut_event/shortcut_event.dart'; export 'src/service/shortcut_event/shortcut_event_handler.dart'; export 'src/extensions/attributes_extension.dart'; +export 'src/extensions/path_extensions.dart'; +export 'src/render/rich_text/default_selectable.dart'; +export 'src/render/rich_text/flowy_rich_text.dart'; +export 'src/render/selection_menu/selection_menu_widget.dart'; export 'src/l10n/l10n.dart'; 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 2655717c1d..7174290b9c 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 @@ -118,8 +118,8 @@ class _AppFlowyEditorState extends State { key: editorState.service.keyboardServiceKey, editable: widget.editable, shortcutEvents: [ - ...builtInShortcutEvents, ...widget.shortcutEvents, + ...builtInShortcutEvents, ], editorState: editorState, child: FlowyToolbar( From 5421c156c3a7c6712aef50bcc2656ab46b4b1ca6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 26 Sep 2022 18:13:45 +0800 Subject: [PATCH 5/8] fix: tab & enter test --- .../tab_handler.dart | 10 ++++--- ...thout_shift_in_text_node_handler_test.dart | 27 ++++++++++++++----- .../tab_handler_test.dart | 20 ++++++++------ 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart index d0d594d41e..0291fc34a5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/tab_handler.dart @@ -13,15 +13,19 @@ ShortcutEventHandler tabHandler = (editorState, event) { final textNode = textNodes.first; final previous = textNode.previous; - if (textNode.subtype != BuiltInAttributeKey.bulletedList || - previous == null || - previous.subtype != BuiltInAttributeKey.bulletedList) { + + if (textNode.subtype != BuiltInAttributeKey.bulletedList) { TransactionBuilder(editorState) ..insertText(textNode, selection.end.offset, ' ' * 4) ..commit(); return KeyEventResult.handled; } + if (previous == null || + previous.subtype != BuiltInAttributeKey.bulletedList) { + return KeyEventResult.ignored; + } + final path = previous.path + [previous.children.length]; final afterSelection = Selection( start: selection.start.copyWith(path: path), diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart index cb2d10ea2f..916541025d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart @@ -2,7 +2,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../infra/test_editor.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; void main() async { setUpAll(() { @@ -171,13 +170,27 @@ Future _testStyleNeedToBeCopy(WidgetTester tester, String style) async { LogicalKeyboardKey.enter, ); expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0)); - expect(editor.nodeAtPath([4])?.subtype, style); - await editor.pressLogicKey( - LogicalKeyboardKey.enter, - ); - expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0)); - expect(editor.nodeAtPath([4])?.subtype, null); + if ([BuiltInAttributeKey.heading, BuiltInAttributeKey.quote] + .contains(style)) { + expect(editor.nodeAtPath([4])?.subtype, null); + + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + expect( + editor.documentSelection, Selection.single(path: [5], startOffset: 0)); + expect(editor.nodeAtPath([5])?.subtype, null); + } else { + expect(editor.nodeAtPath([4])?.subtype, style); + + await editor.pressLogicKey( + LogicalKeyboardKey.enter, + ); + expect( + editor.documentSelection, Selection.single(path: [4], startOffset: 0)); + expect(editor.nodeAtPath([4])?.subtype, null); + } } Future _testMultipleSelection( diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart index 1374869deb..641282c55f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/tab_handler_test.dart @@ -15,23 +15,24 @@ void main() async { ..insertTextNode(text) ..insertTextNode(text); await editor.startTesting(); - final document = editor.document; var selection = Selection.single(path: [0], startOffset: 0); await editor.updateSelection(selection); await editor.pressLogicKey(LogicalKeyboardKey.tab); - // nothing happens - expect(editor.documentSelection, selection); - expect(editor.document.toJson(), document.toJson()); + expect( + editor.documentSelection, + Selection.single(path: [0], startOffset: 4), + ); selection = Selection.single(path: [1], startOffset: 0); await editor.updateSelection(selection); await editor.pressLogicKey(LogicalKeyboardKey.tab); - // nothing happens - expect(editor.documentSelection, selection); - expect(editor.document.toJson(), document.toJson()); + expect( + editor.documentSelection, + Selection.single(path: [1], startOffset: 4), + ); }); testWidgets('press tab in bulleted list', (tester) async { @@ -63,7 +64,10 @@ void main() async { await editor.pressLogicKey(LogicalKeyboardKey.tab); // nothing happens - expect(editor.documentSelection, selection); + expect( + editor.documentSelection, + Selection.single(path: [0], startOffset: 0), + ); expect(editor.document.toJson(), document.toJson()); // Before From 31ba12d289e8c737b8a340a11168b7af528ddeb3 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 27 Sep 2022 11:14:58 +0800 Subject: [PATCH 6/8] fix: disable built-in toolbar items for non-built-in widget --- .../lib/src/render/toolbar/toolbar_item.dart | 26 ++++++++++++------- .../arrow_keys_handler.dart | 8 +++--- .../lib/src/service/toolbar_service.dart | 12 ++++----- 3 files changed, 26 insertions(+), 20 deletions(-) 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 02eaddc68d..68bb5023ca 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 @@ -8,7 +8,6 @@ import 'package:appflowy_editor/src/service/default_text_operations/format_rich_ import 'package:flutter/material.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; -import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; typedef ToolbarItemEventHandler = void Function( EditorState editorState, BuildContext context); @@ -120,7 +119,7 @@ List defaultToolbarItems = [ name: 'toolbar/bold', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.bold, @@ -136,7 +135,7 @@ List defaultToolbarItems = [ name: 'toolbar/italic', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.italic, @@ -152,7 +151,7 @@ List defaultToolbarItems = [ name: 'toolbar/underline', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.underline, @@ -168,7 +167,7 @@ List defaultToolbarItems = [ name: 'toolbar/strikethrough', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.strikethrough, @@ -184,7 +183,7 @@ List defaultToolbarItems = [ name: 'toolbar/code', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.code, @@ -248,7 +247,7 @@ List defaultToolbarItems = [ name: 'toolbar/highlight', color: isHighlight ? Colors.lightBlue : null, ), - validator: _showInTextSelection, + validator: _showInBuiltInTextSelection, highlightCallback: (editorState) => _allSatisfy( editorState, BuiltInAttributeKey.backgroundColor, @@ -262,13 +261,22 @@ List defaultToolbarItems = [ ]; ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) { + final result = _showInBuiltInTextSelection(editorState); + if (!result) { + return false; + } final nodes = editorState.service.selectionService.currentSelectedNodes; return (nodes.length == 1 && nodes.first is TextNode); }; -ToolbarItemValidator _showInTextSelection = (editorState) { +ToolbarItemValidator _showInBuiltInTextSelection = (editorState) { final nodes = editorState.service.selectionService.currentSelectedNodes - .whereType(); + .whereType() + .where( + (textNode) => + BuiltInAttributeKey.globalStyleKeys.contains(textNode.subtype) || + textNode.subtype == null, + ); return nodes.isNotEmpty; }; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index c04dafe986..010cbb5840 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -341,12 +341,12 @@ Position? _goUp(EditorState editorState) { final rect = rects.reduce( (current, next) => current.bottom >= next.bottom ? current : next, ); - offset = rect.topRight.translate(0, -rect.height); + offset = rect.topRight.translate(0, -rect.height * 1.3); } else { final rect = rects.reduce( (current, next) => current.top <= next.top ? current : next, ); - offset = rect.topLeft.translate(0, -rect.height); + offset = rect.topLeft.translate(0, -rect.height * 1.3); } return editorState.service.selectionService.getPositionInOffset(offset); } @@ -362,12 +362,12 @@ Position? _goDown(EditorState editorState) { final rect = rects.reduce( (current, next) => current.bottom >= next.bottom ? current : next, ); - offset = rect.bottomRight.translate(0, rect.height); + offset = rect.bottomRight.translate(0, rect.height * 1.3); } else { final rect = rects.reduce( (current, next) => current.top <= next.top ? current : next, ); - offset = rect.bottomLeft.translate(0, rect.height); + offset = rect.bottomLeft.translate(0, rect.height * 1.3); } return editorState.service.selectionService.getPositionInOffset(offset); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart index 6575dced25..3d3def574e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart @@ -38,14 +38,17 @@ class _FlowyToolbarState extends State @override void showInOffset(Offset offset, LayerLink layerLink) { hide(); - + final items = _filterItems(defaultToolbarItems); + if (items.isEmpty) { + return; + } _toolbarOverlay = OverlayEntry( builder: (context) => ToolbarWidget( key: _toolbarWidgetKey, editorState: widget.editorState, layerLink: layerLink, offset: offset, - items: _filterItems(defaultToolbarItems), + items: items, ), ); Overlay.of(context)?.insert(_toolbarOverlay!); @@ -102,9 +105,4 @@ class _FlowyToolbarState extends State } return dividedItems; } - - // List _highlightItems( - // List items, - // Selection selection, - // ) {} } From 86a6f5827d045bfde2b50f8cabf67892316a0ca4 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 27 Sep 2022 14:24:59 +0800 Subject: [PATCH 7/8] chore: revert the example.json --- .../packages/appflowy_editor/example/assets/example.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json index d7fc310573..2d441d3367 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json +++ b/frontend/app_flowy/packages/appflowy_editor/example/assets/example.json @@ -25,11 +25,6 @@ } ] }, - { - "type": "text", - "attributes": { "subtype": "code_block", "theme": "vs", "language": "dart" }, - "delta": [{ "insert": "final x = 0;\nprint($x);" }] - }, { "type": "text", "delta": [] }, { "type": "text", From af877913d7e892ab741d35f98d1130cc78c8aed9 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 27 Sep 2022 14:36:40 +0800 Subject: [PATCH 8/8] feat: add auto detect language and remove the unused package --- .../example/lib/plugin/code_block_node_widget.dart | 4 +++- .../packages/appflowy_editor/example/pubspec.yaml | 1 - .../internal_key_event_handlers/arrow_keys_handler.dart | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart index b36cbbd8ac..3949073756 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/code_block_node_widget.dart @@ -121,6 +121,7 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> final _richTextKey = GlobalKey(debugLabel: 'code_block_text'); final _padding = const EdgeInsets.only(left: 20, top: 20, bottom: 20); String? get _language => widget.textNode.attributes['language'] as String?; + String? _detectLanguage; @override SelectableMixin get forward => @@ -148,6 +149,7 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> language: _language, autoDetection: _language == null, ); + _detectLanguage = _language ?? result.language; final code = result.nodes; final codeTextSpan = _convert(code!); return Container( @@ -174,7 +176,7 @@ class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge> top: -5, right: 0, child: DropdownButton( - value: _language, + value: _detectLanguage, onChanged: (value) { TransactionBuilder(widget.editorState) ..updateNode(widget.textNode, { diff --git a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml index 432941656d..9f7b4e805b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/example/pubspec.yaml @@ -44,7 +44,6 @@ dependencies: file_picker: ^5.0.1 universal_html: ^2.0.8 highlight: ^0.7.0 - flutter_highlight: ^0.7.0 dev_dependencies: flutter_test: diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart index 010cbb5840..c04dafe986 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -341,12 +341,12 @@ Position? _goUp(EditorState editorState) { final rect = rects.reduce( (current, next) => current.bottom >= next.bottom ? current : next, ); - offset = rect.topRight.translate(0, -rect.height * 1.3); + offset = rect.topRight.translate(0, -rect.height); } else { final rect = rects.reduce( (current, next) => current.top <= next.top ? current : next, ); - offset = rect.topLeft.translate(0, -rect.height * 1.3); + offset = rect.topLeft.translate(0, -rect.height); } return editorState.service.selectionService.getPositionInOffset(offset); } @@ -362,12 +362,12 @@ Position? _goDown(EditorState editorState) { final rect = rects.reduce( (current, next) => current.bottom >= next.bottom ? current : next, ); - offset = rect.bottomRight.translate(0, rect.height * 1.3); + offset = rect.bottomRight.translate(0, rect.height); } else { final rect = rects.reduce( (current, next) => current.top <= next.top ? current : next, ); - offset = rect.bottomLeft.translate(0, rect.height * 1.3); + offset = rect.bottomLeft.translate(0, rect.height); } return editorState.service.selectionService.getPositionInOffset(offset); }