From b52f618b1a9beb768745b526b255cc466ba0d64b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 26 Aug 2022 14:06:11 +0800 Subject: [PATCH 1/9] test: add more test cases for image --- .../appflowy_editor/example/lib/main.dart | 5 +- .../src/render/image/image_node_builder.dart | 1 + .../src/render/image/image_node_widget.dart | 50 ++++++++++++- ...xt_handler.dart => backspace_handler.dart} | 16 +++- .../default_key_event_handlers.dart | 2 +- .../lib/src/service/toolbar_service.dart | 2 +- .../test/infra/test_editor.dart | 2 +- .../render/image/image_node_widget_test.dart | 11 +++ ..._test.dart => backspace_handler_test.dart} | 75 ++++++++++++++++++- 9 files changed, 152 insertions(+), 12 deletions(-) rename frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/{delete_text_handler.dart => backspace_handler.dart} (92%) rename frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/{delete_text_handler_test.dart => backspace_handler_test.dart} (83%) 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 e72739e246..0184138fb5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -45,6 +45,7 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( + extendBodyBehindAppBar: true, body: Container( alignment: Alignment.topCenter, child: _buildEditor(context), @@ -92,8 +93,8 @@ class _MyHomePageState extends State { ..handler = (message) { debugPrint(message); }; - return Container( - padding: const EdgeInsets.all(20), + return SizedBox( + width: MediaQuery.of(context).size.width, child: AppFlowyEditor( editorState: _editorState, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart index 796e96c250..ad3cf19d53 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_builder.dart @@ -17,6 +17,7 @@ class ImageNodeBuilder extends NodeWidgetBuilder { } return ImageNodeWidget( key: context.node.key, + node: context.node, src: src, width: width, alignment: _textToAlignment(align), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index 2cc0916b66..ca52a14db1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -1,10 +1,17 @@ +import 'dart:math'; + +import 'package:appflowy_editor/src/document/node.dart'; +import 'package:appflowy_editor/src/document/position.dart'; +import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; +import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; class ImageNodeWidget extends StatefulWidget { const ImageNodeWidget({ Key? key, + required this.node, required this.src, this.width, required this.alignment, @@ -14,6 +21,7 @@ class ImageNodeWidget extends StatefulWidget { required this.onResize, }) : super(key: key); + final Node node; final String src; final double? width; final Alignment alignment; @@ -26,7 +34,7 @@ class ImageNodeWidget extends StatefulWidget { State createState() => _ImageNodeWidgetState(); } -class _ImageNodeWidgetState extends State { +class _ImageNodeWidgetState extends State with Selectable { double? _imageWidth; double _initial = 0; double _distance = 0; @@ -42,7 +50,8 @@ class _ImageNodeWidgetState extends State { _imageWidth = widget.width; _imageStreamListener = ImageStreamListener( (image, _) { - _imageWidth = image.image.width.toDouble(); + _imageWidth = + min(defaultMaxTextNodeWidth, image.image.width.toDouble()); }, ); } @@ -64,6 +73,43 @@ class _ImageNodeWidgetState extends State { ); } + @override + Position start() { + return Position(path: widget.node.path, offset: 0); + } + + @override + Position end() { + return Position(path: widget.node.path, offset: 1); + } + + @override + Position getPositionInOffset(Offset start) { + return end(); + } + + @override + Rect? getCursorRectInPosition(Position position) { + return null; + } + + @override + List getRectsInSelection(Selection selection) { + final renderBox = context.findRenderObject() as RenderBox; + return [Offset.zero & renderBox.size]; + } + + @override + Selection getSelectionInRange(Offset start, Offset end) { + return Selection(start: this.start(), end: this.end()); + } + + @override + Offset localToGlobal(Offset offset) { + final renderBox = context.findRenderObject() as RenderBox; + return renderBox.localToGlobal(offset); + } + Widget _buildNetworkImage(BuildContext context) { return Align( alignment: widget.alignment, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart similarity index 92% rename from frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart rename to frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index d2a3d51e64..ebe1eb3bb8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/delete_text_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -11,10 +11,16 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { var nodes = editorState.service.selectionService.currentSelectedNodes; nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false); selection = selection.isBackward ? selection : selection.reversed; - // make sure all nodes is [TextNode]. final textNodes = nodes.whereType().toList(); + final nonTextNodes = + nodes.where((node) => node is! TextNode).toList(growable: false); final transactionBuilder = TransactionBuilder(editorState); + + if (nonTextNodes.isNotEmpty) { + transactionBuilder.deleteNodes(nonTextNodes); + } + if (textNodes.length == 1) { final textNode = textNodes.first; final index = textNode.delta.prevRunePosition(selection.start.offset); @@ -68,7 +74,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } } } else { - _deleteNodes(transactionBuilder, textNodes, selection); + if (textNodes.isNotEmpty) { + _deleteTextNodes(transactionBuilder, textNodes, selection); + } } if (transactionBuilder.operations.isNotEmpty) { @@ -121,7 +129,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { } } } else { - _deleteNodes(transactionBuilder, textNodes, selection); + _deleteTextNodes(transactionBuilder, textNodes, selection); } transactionBuilder.commit(); @@ -129,7 +137,7 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) { return KeyEventResult.handled; } -void _deleteNodes(TransactionBuilder transactionBuilder, +void _deleteTextNodes(TransactionBuilder transactionBuilder, List textNodes, Selection selection) { final first = textNodes.first; final last = textNodes.last; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart index 468eda4e98..a8cbdee3ab 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart @@ -1,6 +1,6 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart'; -import 'package:appflowy_editor/src/service/internal_key_event_handlers/delete_text_handler.dart'; +import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart'; 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 e26a186387..8dba7dcb8e 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 @@ -90,7 +90,7 @@ class _FlowyToolbarState extends State .where((item) => item.validator(widget.editorState)) .toList(growable: false) ..sort((a, b) => a.type.compareTo(b.type)); - if (items.isEmpty) { + if (filterItems.isEmpty) { return []; } final List dividedItems = [filterItems.first]; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart index a815d91875..e3a5a7d0c5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart @@ -80,7 +80,7 @@ class EditorWidgetTester { } else { _editorState.service.selectionService.updateSelection(selection); } - await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 200)); expect(_editorState.service.selectionService.currentSelection.value, selection); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart index d2f774d33f..a566b7ec07 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_widget_test.dart @@ -1,3 +1,6 @@ +import 'dart:collection'; + +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -20,6 +23,14 @@ void main() async { final widget = ImageNodeWidget( src: src, + node: Node( + type: 'image', + children: LinkedList(), + attributes: { + 'image_src': src, + 'align': 'center', + }, + ), alignment: Alignment.center, onCopy: () { onCopyHit = true; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart similarity index 83% rename from frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart rename to frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index 1e7bf4e842..76843c4300 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/delete_text_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -1,7 +1,9 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/image/image_node_widget.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; import '../../infra/test_editor.dart'; void main() async { @@ -9,7 +11,7 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); }); - group('delete_text_handler.dart', () { + group('backspace_handler.dart', () { testWidgets('Presses backspace key in empty document', (tester) async { // Before // @@ -167,6 +169,77 @@ void main() async { testWidgets('Presses delete key in styled text (quote)', (tester) async { await _deleteStyledTextByDelete(tester, StyleKey.quote); }); + + // Before + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // [Image] + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + // After + // + // Welcome to Appflowy 😁 + // Welcome to Appflowy 😁 + // + testWidgets('Deletes the image surrounded by text', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertImageNode(src) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 5); + expect(find.byType(ImageNodeWidget), findsOneWidget); + + await editor.updateSelection( + Selection( + start: Position(path: [1], offset: 0), + end: Position(path: [3], offset: text.length), + ), + ); + + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 3); + expect(find.byType(ImageNodeWidget), findsNothing); + expect( + editor.documentSelection, + Selection.single(path: [1], startOffset: 0), + ); + }); + }); + + testWidgets('Deletes the first image', (tester) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; + final editor = tester.editor + ..insertImageNode(src) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(ImageNodeWidget), findsOneWidget); + + await editor.updateSelection( + Selection( + start: Position(path: [0], offset: 0), + end: Position(path: [0], offset: 1), + ), + ); + + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 2); + expect(find.byType(ImageNodeWidget), findsNothing); + }); + }); } Future _deleteStyledTextByBackspace( From 01328442a02b39ffe8cfc28cdae23b676b3d4c10 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 26 Aug 2022 16:00:20 +0800 Subject: [PATCH 2/9] fix: could not delete the image when the selection is multiple --- .../src/render/image/image_node_widget.dart | 6 +- .../backspace_handler.dart | 3 + .../backspace_handler_test.dart | 94 ++++++++++++++----- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index ca52a14db1..316202b1c7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -101,7 +101,11 @@ class _ImageNodeWidgetState extends State with Selectable { @override Selection getSelectionInRange(Offset start, Offset end) { - return Selection(start: this.start(), end: this.end()); + if (start <= end) { + return Selection(start: this.start(), end: this.end()); + } else { + return Selection(start: this.end(), end: this.start()); + } } @override diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart index ebe1eb3bb8..0eeaf654de 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart @@ -80,6 +80,9 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) { } if (transactionBuilder.operations.isNotEmpty) { + if (nonTextNodes.isNotEmpty) { + transactionBuilder.afterSelection = Selection.collapsed(selection.start); + } transactionBuilder.commit(); } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart index 76843c4300..1976ec3250 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart @@ -215,30 +215,82 @@ void main() async { }); }); - testWidgets('Deletes the first image', (tester) async { - mockNetworkImagesFor(() async { - const text = 'Welcome to Appflowy 😁'; - const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; - final editor = tester.editor - ..insertImageNode(src) - ..insertTextNode(text) - ..insertTextNode(text); - await editor.startTesting(); + testWidgets('Deletes the first image, and selection is backward', + (tester) async { + await _deleteFirstImage(tester, true); + }); - expect(editor.documentLength, 3); - expect(find.byType(ImageNodeWidget), findsOneWidget); + testWidgets('Deletes the first image, and selection is not backward', + (tester) async { + await _deleteFirstImage(tester, false); + }); - await editor.updateSelection( - Selection( - start: Position(path: [0], offset: 0), - end: Position(path: [0], offset: 1), - ), - ); + testWidgets('Deletes the last image and selection is backward', + (tester) async { + await _deleteLastImage(tester, true); + }); - await editor.pressLogicKey(LogicalKeyboardKey.backspace); - expect(editor.documentLength, 2); - expect(find.byType(ImageNodeWidget), findsNothing); - }); + testWidgets('Deletes the last image and selection is not backward', + (tester) async { + await _deleteLastImage(tester, false); + }); +} + +Future _deleteFirstImage(WidgetTester tester, bool isBackward) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; + final editor = tester.editor + ..insertImageNode(src) + ..insertTextNode(text) + ..insertTextNode(text); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(ImageNodeWidget), findsOneWidget); + + final start = Position(path: [0], offset: 0); + final end = Position(path: [1], offset: 1); + await editor.updateSelection( + Selection( + start: isBackward ? start : end, + end: isBackward ? end : start, + ), + ); + + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 2); + expect(find.byType(ImageNodeWidget), findsNothing); + expect(editor.documentSelection, Selection.collapsed(start)); + }); +} + +Future _deleteLastImage(WidgetTester tester, bool isBackward) async { + mockNetworkImagesFor(() async { + const text = 'Welcome to Appflowy 😁'; + const src = 'https://s1.ax1x.com/2022/08/26/v2sSbR.jpg'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode(text) + ..insertImageNode(src); + await editor.startTesting(); + + expect(editor.documentLength, 3); + expect(find.byType(ImageNodeWidget), findsOneWidget); + + final start = Position(path: [1], offset: 0); + final end = Position(path: [2], offset: 1); + await editor.updateSelection( + Selection( + start: isBackward ? start : end, + end: isBackward ? end : start, + ), + ); + + await editor.pressLogicKey(LogicalKeyboardKey.backspace); + expect(editor.documentLength, 2); + expect(find.byType(ImageNodeWidget), findsNothing); + expect(editor.documentSelection, Selection.collapsed(start)); }); } From 3686351592b1b8a707544e3739d213b2d6433452 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 10:08:44 +0800 Subject: [PATCH 3/9] fix: #918 could not update the link sometimes --- .../lib/src/render/rich_text/flowy_rich_text.dart | 13 ++++++++++++- .../test/render/rich_text/checkbox_text_test.dart | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 39f484c23f..884a8bbe12 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -42,7 +42,7 @@ class FlowyRichText extends StatefulWidget { } class _FlowyRichTextState extends State with Selectable { - final _textKey = GlobalKey(); + var _textKey = GlobalKey(); final _placeholderTextKey = GlobalKey(); final _lineHeight = 1.5; @@ -53,6 +53,17 @@ class _FlowyRichTextState extends State with Selectable { RenderParagraph get _placeholderRenderParagraph => _placeholderTextKey.currentContext?.findRenderObject() as RenderParagraph; + @override + void didUpdateWidget(covariant FlowyRichText oldWidget) { + super.didUpdateWidget(oldWidget); + + // https://github.com/flutter/flutter/issues/110342 + if (_textKey.currentWidget is RichText) { + // Force refresh the RichText widget. + _textKey = GlobalKey(); + } + } + @override Widget build(BuildContext context) { return _buildRichText(context); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart index f039c227d9..afd89ddee9 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart @@ -10,8 +10,8 @@ void main() async { TestWidgetsFlutterBinding.ensureInitialized(); }); - group('delete_text_handler.dart', () { - testWidgets('Presses backspace key in empty document', (tester) async { + group('checkbox_text_handler.dart', () { + testWidgets('Click checkbox icon', (tester) async { // Before // // [BIUS]Welcome to Appflowy 😁[BIUS] From cde48926e2f9aab0fead3bcab95b546ee3f11244 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 10:25:56 +0800 Subject: [PATCH 4/9] feat: add copy link to link menu --- .../appflowy_editor/assets/images/copy.svg | 4 + .../extensions/url_launcher_extension.dart | 14 ++++ .../src/operation/transaction_builder.dart | 14 ++-- .../lib/src/render/link_menu/link_menu.dart | 19 +++-- .../src/render/rich_text/flowy_rich_text.dart | 80 +++++++++---------- .../lib/src/render/toolbar/toolbar_item.dart | 20 +++-- .../lib/src/service/input_service.dart | 4 - .../test/render/link_menu/link_menu_test.dart | 1 + 8 files changed, 95 insertions(+), 61 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg new file mode 100644 index 0000000000..101cf34205 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart new file mode 100644 index 0000000000..1c0ea30c82 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/url_launcher_extension.dart @@ -0,0 +1,14 @@ +import 'package:url_launcher/url_launcher_string.dart'; + +Future safeLaunchUrl(String? href) async { + if (href == null) { + return Future.value(false); + } + final uri = Uri.parse(href); + // url_launcher cannot open a link without scheme. + final newHref = (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); + if (await canLaunchUrlString(newHref)) { + await launchUrlString(newHref); + } + return Future.value(true); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart index 12c13bf2e5..1390b23918 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/operation/transaction_builder.dart @@ -115,17 +115,18 @@ class TransactionBuilder { /// Inserts content at a specified index. /// Optionally, you may specify formatting attributes that are applied to the inserted string. /// By default, the formatting attributes before the insert position will be used. - insertText(TextNode node, int index, String content, - {Attributes? attributes, Attributes? removedAttributes}) { + insertText( + TextNode node, + int index, + String content, { + Attributes? attributes, + }) { var newAttributes = attributes; if (index != 0 && attributes == null) { newAttributes = node.delta.slice(max(index - 1, 0), index).first.attributes; if (newAttributes != null) { newAttributes = Attributes.from(newAttributes); - if (removedAttributes != null) { - newAttributes.addAll(removedAttributes); - } } } textEdit( @@ -138,7 +139,8 @@ class TransactionBuilder { ), ); afterSelection = Selection.collapsed( - Position(path: node.path, offset: index + content.length)); + Position(path: node.path, offset: index + content.length), + ); } /// Assigns formatting attributes to a range of text. diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index a33adf3b8c..07e1b947eb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -6,12 +6,14 @@ class LinkMenu extends StatefulWidget { Key? key, this.linkText, required this.onSubmitted, + required this.onOpenLink, required this.onCopyLink, required this.onRemoveLink, }) : super(key: key); final String? linkText; final void Function(String text) onSubmitted; + final VoidCallback onOpenLink; final VoidCallback onCopyLink; final VoidCallback onRemoveLink; @@ -26,15 +28,12 @@ class _LinkMenuState extends State { @override void initState() { super.initState(); - _textEditingController.text = widget.linkText ?? ''; - _focusNode.requestFocus(); } @override void dispose() { - _focusNode.dispose(); - + _textEditingController.dispose(); super.dispose(); } @@ -67,6 +66,12 @@ class _LinkMenuState extends State { if (widget.linkText != null) ...[ _buildIconButton( iconName: 'link', + text: 'Open link', + onPressed: widget.onOpenLink, + ), + _buildIconButton( + iconName: 'copy', + color: Colors.black, text: 'Copy link', onPressed: widget.onCopyLink, ), @@ -126,11 +131,15 @@ class _LinkMenuState extends State { Widget _buildIconButton({ required String iconName, + Color? color, required String text, required VoidCallback onPressed, }) { return TextButton.icon( - icon: FlowySvg(name: iconName), + icon: FlowySvg( + name: iconName, + color: color, + ), style: TextButton.styleFrom( minimumSize: const Size.fromHeight(40), padding: EdgeInsets.zero, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 884a8bbe12..517f7dd4b8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:ui'; +import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -13,7 +15,6 @@ import 'package:appflowy_editor/src/document/text_delta.dart'; import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; -import 'package:url_launcher/url_launcher_string.dart'; typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan); @@ -204,53 +205,23 @@ class _FlowyRichTextState extends State with Selectable { var offset = 0; return TextSpan( children: widget.textNode.delta.whereType().map((insert) { - GestureRecognizer? gestureDetector; + GestureRecognizer? gestureRecognizer; if (insert.attributes?[StyleKey.href] != null) { - final startOffset = offset; - Timer? timer; - var tapCount = 0; - gestureDetector = TapGestureRecognizer() - ..onTap = () async { - // implement a simple double tap logic - tapCount += 1; - timer?.cancel(); - - if (tapCount == 2) { - tapCount = 0; - final href = insert.attributes![StyleKey.href]; - final uri = Uri.parse(href); - // url_launcher cannot open a link without scheme. - final newHref = - (uri.scheme.isNotEmpty ? href : 'http://$href').trim(); - if (await canLaunchUrlString(newHref)) { - await launchUrlString(newHref); - } - return; - } - - timer = Timer(const Duration(milliseconds: 200), () { - tapCount = 0; - // update selection - final selection = Selection.single( - path: widget.textNode.path, - startOffset: startOffset, - endOffset: startOffset + insert.length, - ); - widget.editorState.service.selectionService - .updateSelection(selection); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - widget.editorState.service.toolbarService - ?.triggerHandler('appflowy.toolbar.link'); - }); - }); - }; + gestureRecognizer = _buildTapHrefGestureRecognizer( + insert.attributes![StyleKey.href], + Selection.single( + path: widget.textNode.path, + startOffset: offset, + endOffset: offset + insert.length, + ), + ); } offset += insert.length; final textSpan = RichTextStyle( attributes: insert.attributes ?? {}, text: insert.content, height: _lineHeight, - gestureRecognizer: gestureDetector, + gestureRecognizer: gestureRecognizer, ).toTextSpan(); return textSpan; }).toList(growable: false), @@ -266,4 +237,31 @@ class _FlowyRichTextState extends State with Selectable { height: _lineHeight, ).toTextSpan() ]); + + GestureRecognizer _buildTapHrefGestureRecognizer( + String href, Selection selection) { + Timer? timer; + var tapCount = 0; + final tapGestureRecognizer = TapGestureRecognizer() + ..onTap = () async { + // implement a simple double tap logic + tapCount += 1; + timer?.cancel(); + + if (tapCount == 2) { + tapCount = 0; + safeLaunchUrl(href); + return; + } + + timer = Timer(const Duration(milliseconds: 200), () { + tapCount = 0; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + showLinkMenu(context, widget.editorState, + customSelection: selection); + }); + }); + }; + return tapGestureRecognizer; + } } 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 107ae23b6f..979f86cdd1 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 @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; @@ -132,7 +133,7 @@ List defaultToolbarItems = [ tooltipsMessage: 'Link', icon: const FlowySvg(name: 'toolbar/link'), validator: _onlyShowInSingleTextSelection, - handler: (editorState, context) => _showLinkMenu(editorState, context), + handler: (editorState, context) => showLinkMenu(context, editorState), ), ToolbarItem( id: 'appflowy.toolbar.highlight', @@ -157,7 +158,11 @@ ToolbarShowValidator _showInTextSelection = (editorState) { OverlayEntry? _linkMenuOverlay; EditorState? _editorState; -void _showLinkMenu(EditorState editorState, BuildContext context) { +void showLinkMenu( + BuildContext context, + EditorState editorState, { + Selection? customSelection, +}) { final rects = editorState.service.selectionService.selectionRects; var maxBottom = 0.0; late Rect matchRect; @@ -173,8 +178,11 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { // Since the link menu will only show in single text selection, // We get the text node directly instead of judging details again. - final selection = - editorState.service.selectionService.currentSelection.value!; + final selection = customSelection ?? + editorState.service.selectionService.currentSelection.value; + if (selection == null) { + return; + } final index = selection.isBackward ? selection.start.offset : selection.end.offset; final length = (selection.start.offset - selection.end.offset).abs(); @@ -191,6 +199,9 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { child: Material( child: LinkMenu( linkText: linkText, + onOpenLink: () async { + await safeLaunchUrl(linkText); + }, onSubmitted: (text) { TransactionBuilder(editorState) ..formatText(node, index, length, {StyleKey.href: text}) @@ -214,7 +225,6 @@ void _showLinkMenu(EditorState editorState, BuildContext context) { Overlay.of(context)?.insert(_linkMenuOverlay!); editorState.service.scrollService?.disable(); - editorState.service.keyboardService?.disable(); editorState.service.selectionService.currentSelection .addListener(_dismissLinkMenu); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart index 96f0777544..a92fae1b95 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart @@ -1,5 +1,4 @@ import 'package:appflowy_editor/src/infra/log.dart'; -import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -150,9 +149,6 @@ class _AppFlowyInputState extends State textNode, delta.insertionOffset, delta.textInserted, - removedAttributes: { - StyleKey.href: null, - }, ) ..commit(); } else { diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart index 7b4541033b..5b102b9ec1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart @@ -12,6 +12,7 @@ void main() async { const link = 'appflowy.io'; var submittedText = ''; final linkMenu = LinkMenu( + onOpenLink: () {}, onCopyLink: () {}, onRemoveLink: () {}, onSubmitted: (text) { From c07af9007cf18e651d28d6622c5ad20c3cc9da69 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 11:40:34 +0800 Subject: [PATCH 5/9] fix: Should not add a new line below after pressing enter at the front of the first line of text. --- .../lib/src/document/state_tree.dart | 15 +++++++++---- ...thout_shift_in_text_node_handler_test.dart | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart index a17b2fbf98..a4a9869df5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/document/state_tree.dart @@ -62,10 +62,17 @@ class StateTree { } return false; } - for (var i = 0; i < nodes.length; i++) { - final node = nodes[i]; - insertedNode!.insertAfter(node); - insertedNode = node; + if (path.last <= 0) { + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + insertedNode.insertBefore(node); + } + } else { + for (var i = 0; i < nodes.length; i++) { + final node = nodes[i]; + insertedNode!.insertAfter(node); + insertedNode = node; + } } return true; } 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 ee21dfa455..5bfe1ada67 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 @@ -116,6 +116,27 @@ void main() async { (tester) async { _testMultipleSelection(tester, false); }); + + testWidgets('Presses enter key in the first line', (tester) async { + // Before + // + // Welcome to Appflowy 😁 + // + // After + // + // [Empty Line] + // Welcome to Appflowy 😁 + // + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor..insertTextNode(text); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0), + ); + await editor.pressLogicKey(LogicalKeyboardKey.enter); + expect(editor.documentLength, 2); + expect((editor.nodeAtPath([1]) as TextNode).toRawString(), text); + }); }); } From dd9cac9c1d4a681d6fdb859d0bc199fe7c12c189 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 12:02:47 +0800 Subject: [PATCH 6/9] feat: highlight selection when tap on the link menu --- .../lib/src/render/editor/editor_entry.dart | 43 ++++++++++--------- .../lib/src/render/link_menu/link_menu.dart | 8 ++++ .../src/render/rich_text/flowy_rich_text.dart | 7 ++- .../lib/src/render/toolbar/toolbar_item.dart | 29 ++++++++++--- .../test/render/link_menu/link_menu_test.dart | 1 + 5 files changed, 59 insertions(+), 29 deletions(-) diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart index e71dc7c79b..d14d44613f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart @@ -32,26 +32,29 @@ class EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: node.children - .map( - (child) => - editorState.service.renderPluginService.buildPluginWidget( - child is TextNode - ? NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ) - : NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ), - ), - ) - .toList(), + return Container( + color: Colors.red.withOpacity(0.1), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: node.children + .map( + (child) => + editorState.service.renderPluginService.buildPluginWidget( + child is TextNode + ? NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ) + : NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ), + ), + ) + .toList(), + ), ); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart index 07e1b947eb..13396a33c4 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart @@ -9,6 +9,7 @@ class LinkMenu extends StatefulWidget { required this.onOpenLink, required this.onCopyLink, required this.onRemoveLink, + required this.onFocusChange, }) : super(key: key); final String? linkText; @@ -16,6 +17,7 @@ class LinkMenu extends StatefulWidget { final VoidCallback onOpenLink; final VoidCallback onCopyLink; final VoidCallback onRemoveLink; + final void Function(bool value) onFocusChange; @override State createState() => _LinkMenuState(); @@ -29,11 +31,13 @@ class _LinkMenuState extends State { void initState() { super.initState(); _textEditingController.text = widget.linkText ?? ''; + _focusNode.addListener(_onFocusChange); } @override void dispose() { _textEditingController.dispose(); + _focusNode.removeListener(_onFocusChange); super.dispose(); } @@ -157,4 +161,8 @@ class _LinkMenuState extends State { onPressed: onPressed, ); } + + void _onFocusChange() { + widget.onFocusChange(_focusNode.hasFocus); + } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 517f7dd4b8..473f29eaa7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -257,8 +257,11 @@ class _FlowyRichTextState extends State with Selectable { timer = Timer(const Duration(milliseconds: 200), () { tapCount = 0; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - showLinkMenu(context, widget.editorState, - customSelection: selection); + showLinkMenu( + context, + widget.editorState, + customSelection: selection, + ); }); }); }; 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 979f86cdd1..34b38c0444 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 @@ -158,6 +158,7 @@ ToolbarShowValidator _showInTextSelection = (editorState) { OverlayEntry? _linkMenuOverlay; EditorState? _editorState; +bool _changeSelectionInner = false; void showLinkMenu( BuildContext context, EditorState editorState, { @@ -180,17 +181,17 @@ void showLinkMenu( // We get the text node directly instead of judging details again. final selection = customSelection ?? editorState.service.selectionService.currentSelection.value; - if (selection == null) { + final node = editorState.service.selectionService.currentSelectedNodes; + if (selection == null || node.isEmpty || node.first is! TextNode) { return; } final index = selection.isBackward ? selection.start.offset : selection.end.offset; final length = (selection.start.offset - selection.end.offset).abs(); - final node = editorState.service.selectionService.currentSelectedNodes.first - as TextNode; + final textNode = node.first as TextNode; String? linkText; - if (node.allSatisfyLinkInSelection(selection)) { - linkText = node.getAttributeInSelection(selection, StyleKey.href); + if (textNode.allSatisfyLinkInSelection(selection)) { + linkText = textNode.getAttributeInSelection(selection, StyleKey.href); } _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( @@ -204,7 +205,7 @@ void showLinkMenu( }, onSubmitted: (text) { TransactionBuilder(editorState) - ..formatText(node, index, length, {StyleKey.href: text}) + ..formatText(textNode, index, length, {StyleKey.href: text}) ..commit(); _dismissLinkMenu(); }, @@ -214,10 +215,17 @@ void showLinkMenu( }, onRemoveLink: () { TransactionBuilder(editorState) - ..formatText(node, index, length, {StyleKey.href: null}) + ..formatText(textNode, index, length, {StyleKey.href: null}) ..commit(); _dismissLinkMenu(); }, + onFocusChange: (value) { + if (value && customSelection != null) { + _changeSelectionInner = true; + editorState.service.selectionService + .updateSelection(customSelection); + } + }, ), ), ); @@ -230,6 +238,13 @@ void showLinkMenu( } void _dismissLinkMenu() { + if (_editorState?.service.selectionService.currentSelection.value == null) { + return; + } + if (_changeSelectionInner) { + _changeSelectionInner = false; + return; + } _linkMenuOverlay?.remove(); _linkMenuOverlay = null; diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart index 5b102b9ec1..cef16a1cec 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/link_menu/link_menu_test.dart @@ -15,6 +15,7 @@ void main() async { onOpenLink: () {}, onCopyLink: () {}, onRemoveLink: () {}, + onFocusChange: (value) {}, onSubmitted: (text) { submittedText = text; }, From 42866e105702a91007f77afd6de5140c788013ad Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 12:10:57 +0800 Subject: [PATCH 7/9] fix: The cursor will not disappear after clicking in an area outside the editor. --- .../lib/src/service/keyboard_service.dart | 4 ++++ .../lib/src/service/selection_service.dart | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) 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 1867574993..dee3f42725 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 @@ -129,6 +129,10 @@ class _AppFlowyKeyboardState extends State void _onFocusChange(bool value) { Log.keyboard.debug('on keyboard event focus change $value'); + isFocus = value; + if (!value) { + widget.editorState.service.selectionService.clearCursor(); + } } KeyEventResult _onKey(FocusNode node, RawKeyEvent event) { diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart index c5e351059c..6f6897596f 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart @@ -57,6 +57,9 @@ abstract class AppFlowySelectionService { /// Clears the selection area, cursor area and the popup list area. void clearSelection(); + /// Clears the cursor area. + void clearCursor(); + /// Returns the [Node]s in [Selection]. List getNodesInSelection(Selection selection); @@ -205,16 +208,23 @@ class _AppFlowySelectionState extends State currentSelectedNodes = []; currentSelection.value = null; + clearCursor(); // clear selection areas _selectionAreas ..forEach((overlay) => overlay.remove()) ..clear(); // clear cursor areas + + // hide toolbar + editorState.service.toolbarService?.hide(); + } + + @override + void clearCursor() { + // clear cursor areas _cursorAreas ..forEach((overlay) => overlay.remove()) ..clear(); - // hide toolbar - editorState.service.toolbarService?.hide(); } @override From e567158cee02da9199cda2e6194dddc27a7c0d4a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 13:41:20 +0800 Subject: [PATCH 8/9] fix: The click area of the linked text is too large. --- .../appflowy_editor/example/lib/main.dart | 9 ++-- .../lib/src/render/editor/editor_entry.dart | 43 +++++++++---------- .../render/rich_text/bulleted_list_text.dart | 8 ++-- .../src/render/rich_text/checkbox_text.dart | 2 +- .../src/render/rich_text/flowy_rich_text.dart | 4 +- .../src/render/rich_text/heading_text.dart | 4 +- .../render/rich_text/number_list_text.dart | 2 +- .../lib/src/render/rich_text/quoted_text.dart | 2 +- .../lib/src/render/rich_text/rich_text.dart | 4 +- .../lib/src/render/toolbar/toolbar_item.dart | 6 +++ 10 files changed, 46 insertions(+), 38 deletions(-) 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 0184138fb5..8f6bf64c30 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -46,9 +46,12 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, - body: Container( - alignment: Alignment.topCenter, - child: _buildEditor(context), + body: Center( + child: Container( + width: 780, + alignment: Alignment.topCenter, + child: _buildEditor(context), + ), ), floatingActionButton: _buildExpandableFab(), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart index d14d44613f..4167ca1b38 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/editor/editor_entry.dart @@ -32,29 +32,26 @@ class EditorNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - color: Colors.red.withOpacity(0.1), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: node.children - .map( - (child) => - editorState.service.renderPluginService.buildPluginWidget( - child is TextNode - ? NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ) - : NodeWidgetContext( - context: context, - node: child, - editorState: editorState, - ), - ), - ) - .toList(), - ), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: node.children + .map( + (child) => + editorState.service.renderPluginService.buildPluginWidget( + child is TextNode + ? NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ) + : NodeWidgetContext( + context: context, + node: child, + editorState: editorState, + ), + ), + ) + .toList(), ); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index 7d69ff459f..5408f862d8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart @@ -56,8 +56,8 @@ class _BulletedListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return SizedBox( - width: defaultMaxTextNodeWidth, + return Container( + constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), child: Padding( padding: EdgeInsets.only(bottom: defaultLinePadding), child: Row( @@ -70,14 +70,14 @@ class _BulletedListTextNodeWidgetState extends State padding: EdgeInsets.only(right: _iconRightPadding), name: 'point', ), - Expanded( + Flexible( child: FlowyRichText( key: _richTextKey, placeholderText: 'List', textNode: widget.textNode, editorState: widget.editorState, ), - ), + ) ], ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index 0255a84049..bfda4e3f73 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -86,7 +86,7 @@ class _CheckboxNodeWidgetState extends State ..commit(); }, ), - Expanded( + Flexible( child: FlowyRichText( key: _richTextKey, placeholderText: 'To-do', diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart index 473f29eaa7..3489c2bb52 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart @@ -194,7 +194,9 @@ class _FlowyRichTextState extends State with Selectable { return RichText( key: _textKey, textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, applyHeightToLastDescent: false), + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), text: widget.textSpanDecorator != null ? widget.textSpanDecorator!(textSpan) : textSpan, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart index 050b330f8b..7b94783f03 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart @@ -63,8 +63,8 @@ class _HeadingTextNodeWidgetState extends State top: _topPadding, bottom: defaultLinePadding, ), - child: SizedBox( - width: defaultMaxTextNodeWidth, + child: Container( + constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), child: FlowyRichText( key: _richTextKey, placeholderText: 'Heading', diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index c1062e1c3c..a4d72bb011 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -70,7 +70,7 @@ class _NumberListTextNodeWidgetState extends State padding: EdgeInsets.only(right: _iconRightPadding), number: widget.textNode.attributes.number, ), - Expanded( + Flexible( child: FlowyRichText( key: _richTextKey, placeholderText: 'List', diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index 78c6653904..04ae379799 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -69,7 +69,7 @@ class _QuotedTextNodeWidgetState extends State padding: EdgeInsets.only(right: _iconRightPadding), name: 'quote', ), - Expanded( + Flexible( child: FlowyRichText( key: _richTextKey, placeholderText: 'Quote', diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart index d8dcfb91f6..5fe65db4b7 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart @@ -52,8 +52,8 @@ class _RichTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return SizedBox( - width: defaultMaxTextNodeWidth, + return Container( + constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), child: Padding( padding: EdgeInsets.only(bottom: defaultLinePadding), child: FlowyRichText( 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 34b38c0444..9a1b2f1c02 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 @@ -238,6 +238,12 @@ void showLinkMenu( } void _dismissLinkMenu() { + // workaround: SelectionService has been released after hot reload. + final isSelectionDisposed = + _editorState?.service.selectionServiceKey.currentState == null; + if (isSelectionDisposed) { + return; + } if (_editorState?.service.selectionService.currentSelection.value == null) { return; } From 3f38e246ea294f6f21f8e8ef917e0fbc118c2c21 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 29 Aug 2022 15:56:33 +0800 Subject: [PATCH 9/9] feat: support customizing editor edges --- .../appflowy_editor/example/lib/main.dart | 9 +-- .../appflowy_editor/lib/appflowy_editor.dart | 1 + .../appflowy_editor/lib/src/editor_state.dart | 4 ++ .../src/render/image/image_node_widget.dart | 18 +++--- .../render/rich_text/bulleted_list_text.dart | 43 ++++++------- .../src/render/rich_text/checkbox_text.dart | 63 +++++++++---------- .../src/render/rich_text/heading_text.dart | 17 +++-- .../render/rich_text/number_list_text.dart | 39 ++++++------ .../lib/src/render/rich_text/quoted_text.dart | 43 ++++++------- .../lib/src/render/rich_text/rich_text.dart | 15 ++--- .../src/render/rich_text/rich_text_style.dart | 1 - .../lib/src/render/style/editor_style.dart | 20 ++++++ .../lib/src/service/editor_service.dart | 47 ++++++++------ .../render/image/image_node_builder_test.dart | 9 +-- 14 files changed, 172 insertions(+), 157 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart 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 8f6bf64c30..4bd1cb1972 100644 --- a/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart @@ -46,13 +46,7 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( extendBodyBehindAppBar: true, - body: Center( - child: Container( - width: 780, - alignment: Alignment.topCenter, - child: _buildEditor(context), - ), - ), + body: _buildEditor(context), floatingActionButton: _buildExpandableFab(), ); } @@ -100,6 +94,7 @@ class _MyHomePageState extends State { width: MediaQuery.of(context).size.width, child: AppFlowyEditor( editorState: _editorState, + editorStyle: const EditorStyle.defaultStyle(), ), ); } else { 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 14826ff713..12b3a29252 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart @@ -2,6 +2,7 @@ library appflowy_editor; export 'src/infra/log.dart'; +export 'src/render/style/editor_style.dart'; export 'src/document/node.dart'; export 'src/document/path.dart'; export 'src/document/position.dart'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart index 396b428baf..2750af07a6 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:appflowy_editor/src/infra/log.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; +import 'package:appflowy_editor/src/render/style/editor_style.dart'; import 'package:appflowy_editor/src/service/service.dart'; import 'package:flutter/material.dart'; @@ -58,6 +59,9 @@ class EditorState { /// Stores the selection menu items. List selectionMenuItems = []; + /// Stores the editor style. + EditorStyle editorStyle = const EditorStyle.defaultStyle(); + final UndoManager undoManager = UndoManager(); Selection? _cursorSelection; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart index 316202b1c7..a65df11541 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/image/image_node_widget.dart @@ -1,10 +1,8 @@ -import 'dart:math'; - +import 'package:appflowy_editor/src/extensions/object_extensions.dart'; import 'package:appflowy_editor/src/document/node.dart'; import 'package:appflowy_editor/src/document/position.dart'; import 'package:appflowy_editor/src/document/selection.dart'; import 'package:appflowy_editor/src/infra/flowy_svg.dart'; -import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/selection/selectable.dart'; import 'package:flutter/material.dart'; @@ -35,6 +33,8 @@ class ImageNodeWidget extends StatefulWidget { } class _ImageNodeWidgetState extends State with Selectable { + final _imageKey = GlobalKey(); + double? _imageWidth; double _initial = 0; double _distance = 0; @@ -50,8 +50,11 @@ class _ImageNodeWidgetState extends State with Selectable { _imageWidth = widget.width; _imageStreamListener = ImageStreamListener( (image, _) { - _imageWidth = - min(defaultMaxTextNodeWidth, image.image.width.toDouble()); + _imageWidth = _imageKey.currentContext + ?.findRenderObject() + ?.unwrapOrNull() + ?.size + .width; }, ); } @@ -65,9 +68,8 @@ class _ImageNodeWidgetState extends State with Selectable { @override Widget build(BuildContext context) { // only support network image. - return Container( - width: defaultMaxTextNodeWidth, + key: _imageKey, padding: const EdgeInsets.only(top: 8, bottom: 8), child: _buildNetworkImage(context), ); @@ -137,7 +139,7 @@ class _ImageNodeWidgetState extends State with Selectable { loadingBuilder: (context, child, loadingProgress) => loadingProgress == null ? child : _buildLoading(context), errorBuilder: (context, error, stackTrace) { - _imageWidth ??= defaultMaxTextNodeWidth; + // _imageWidth ??= defaultMaxTextNodeWidth; return _buildError(context); }, ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart index 5408f862d8..7f0f0363f8 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart @@ -56,30 +56,27 @@ class _BulletedListTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Container( - constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - key: iconKey, - width: _iconWidth, - height: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - name: 'point', + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + width: _iconWidth, + height: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + name: 'point', + ), + Flexible( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'List', + textNode: widget.textNode, + editorState: widget.editorState, ), - Flexible( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'List', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ) - ], - ), + ) + ], ), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart index bfda4e3f73..ed6748a43e 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart @@ -63,41 +63,38 @@ class _CheckboxNodeWidgetState extends State Widget _buildWithSingle(BuildContext context) { final check = widget.textNode.attributes.check; - return SizedBox( - width: defaultMaxTextNodeWidth, - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - key: iconKey, - child: FlowySvg( - width: _iconWidth, - height: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - name: check ? 'check' : 'uncheck', - ), - onTap: () { - TransactionBuilder(widget.editorState) - ..updateNode(widget.textNode, { - StyleKey.checkbox: !check, - }) - ..commit(); - }, + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + key: iconKey, + child: FlowySvg( + width: _iconWidth, + height: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + name: check ? 'check' : 'uncheck', ), - Flexible( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'To-do', - textNode: widget.textNode, - textSpanDecorator: _textSpanDecorator, - placeholderTextSpanDecorator: _textSpanDecorator, - editorState: widget.editorState, - ), + onTap: () { + TransactionBuilder(widget.editorState) + ..updateNode(widget.textNode, { + StyleKey.checkbox: !check, + }) + ..commit(); + }, + ), + Flexible( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'To-do', + textNode: widget.textNode, + textSpanDecorator: _textSpanDecorator, + placeholderTextSpanDecorator: _textSpanDecorator, + editorState: widget.editorState, ), - ], - ), + ), + ], ), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart index 7b94783f03..fff25dd2d5 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart @@ -63,16 +63,13 @@ class _HeadingTextNodeWidgetState extends State top: _topPadding, bottom: defaultLinePadding, ), - child: Container( - constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Heading', - placeholderTextSpanDecorator: _placeholderTextSpanDecorator, - textSpanDecorator: _textSpanDecorator, - textNode: widget.textNode, - editorState: widget.editorState, - ), + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Heading', + placeholderTextSpanDecorator: _placeholderTextSpanDecorator, + textSpanDecorator: _textSpanDecorator, + textNode: widget.textNode, + editorState: widget.editorState, ), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart index a4d72bb011..36cf91bdce 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart @@ -58,28 +58,25 @@ class _NumberListTextNodeWidgetState extends State Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: defaultLinePadding), - child: SizedBox( - width: defaultMaxTextNodeWidth, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowySvg( - key: iconKey, - width: _iconWidth, - height: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - number: widget.textNode.attributes.number, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowySvg( + key: iconKey, + width: _iconWidth, + height: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + number: widget.textNode.attributes.number, + ), + Flexible( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'List', + textNode: widget.textNode, + editorState: widget.editorState, ), - Flexible( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'List', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ], - ), + ), + ], )); } } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart index 04ae379799..9c2366d1cb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart @@ -55,30 +55,27 @@ class _QuotedTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return SizedBox( - width: defaultMaxTextNodeWidth, - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - FlowySvg( - key: iconKey, - width: _iconWidth, - padding: EdgeInsets.only(right: _iconRightPadding), - name: 'quote', + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FlowySvg( + key: iconKey, + width: _iconWidth, + padding: EdgeInsets.only(right: _iconRightPadding), + name: 'quote', + ), + Flexible( + child: FlowyRichText( + key: _richTextKey, + placeholderText: 'Quote', + textNode: widget.textNode, + editorState: widget.editorState, ), - Flexible( - child: FlowyRichText( - key: _richTextKey, - placeholderText: 'Quote', - textNode: widget.textNode, - editorState: widget.editorState, - ), - ), - ], - ), + ), + ], ), ), ); diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart index 5fe65db4b7..b9a3e2f314 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart @@ -52,15 +52,12 @@ class _RichTextNodeWidgetState extends State @override Widget build(BuildContext context) { - return Container( - constraints: BoxConstraints(maxWidth: defaultMaxTextNodeWidth), - child: Padding( - padding: EdgeInsets.only(bottom: defaultLinePadding), - child: FlowyRichText( - key: _richTextKey, - textNode: widget.textNode, - editorState: widget.editorState, - ), + return Padding( + padding: EdgeInsets.only(bottom: defaultLinePadding), + child: FlowyRichText( + key: _richTextKey, + textNode: widget.textNode, + editorState: widget.editorState, ), ); } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart index efcdd3790f..6270127610 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart @@ -61,7 +61,6 @@ class StyleKey { } // TODO: customize -double defaultMaxTextNodeWidth = 780.0; double defaultLinePadding = 8.0; double baseFontSize = 16.0; String defaultHighlightColor = '0x6000BCF0'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart new file mode 100644 index 0000000000..e691ea689e --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +/// Editor style configuration +class EditorStyle { + const EditorStyle({ + required this.padding, + }); + + const EditorStyle.defaultStyle() + : padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0); + + /// The margin of the document context from the editor. + final EdgeInsets padding; + + EditorStyle copyWith({EdgeInsets? padding}) { + return EditorStyle( + padding: padding ?? this.padding, + ); + } +} 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 2781471b46..3a8d75560b 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 @@ -1,5 +1,6 @@ import 'package:appflowy_editor/src/render/image/image_node_builder.dart'; import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart'; +import 'package:appflowy_editor/src/render/style/editor_style.dart'; import 'package:appflowy_editor/src/service/internal_key_event_handlers/default_key_event_handlers.dart'; import 'package:flutter/material.dart'; @@ -36,6 +37,7 @@ class AppFlowyEditor extends StatefulWidget { this.customBuilders = const {}, this.keyEventHandlers = const [], this.selectionMenuItems = const [], + this.editorStyle = const EditorStyle.defaultStyle(), }) : super(key: key); final EditorState editorState; @@ -48,6 +50,8 @@ class AppFlowyEditor extends StatefulWidget { final List selectionMenuItems; + final EditorStyle editorStyle; + @override State createState() => _AppFlowyEditorState(); } @@ -60,6 +64,7 @@ class _AppFlowyEditorState extends State { super.initState(); editorState.selectionMenuItems = widget.selectionMenuItems; + editorState.editorStyle = widget.editorStyle; editorState.service.renderPluginService = _createRenderPlugin(); } @@ -68,6 +73,8 @@ class _AppFlowyEditorState extends State { super.didUpdateWidget(oldWidget); if (editorState.service != oldWidget.editorState.service) { + editorState.selectionMenuItems = widget.selectionMenuItems; + editorState.editorStyle = widget.editorStyle; editorState.service.renderPluginService = _createRenderPlugin(); } } @@ -76,27 +83,31 @@ class _AppFlowyEditorState extends State { Widget build(BuildContext context) { return AppFlowyScroll( key: editorState.service.scrollServiceKey, - child: AppFlowySelection( - key: editorState.service.selectionServiceKey, - editorState: editorState, - child: AppFlowyInput( - key: editorState.service.inputServiceKey, + child: Padding( + padding: widget.editorStyle.padding, + child: AppFlowySelection( + key: editorState.service.selectionServiceKey, editorState: editorState, - child: AppFlowyKeyboard( - key: editorState.service.keyboardServiceKey, - handlers: [ - ...defaultKeyEventHandlers, - ...widget.keyEventHandlers, - ], + child: AppFlowyInput( + key: editorState.service.inputServiceKey, editorState: editorState, - child: FlowyToolbar( - key: editorState.service.toolbarServiceKey, + child: AppFlowyKeyboard( + key: editorState.service.keyboardServiceKey, + handlers: [ + ...defaultKeyEventHandlers, + ...widget.keyEventHandlers, + ], editorState: editorState, - child: editorState.service.renderPluginService.buildPluginWidget( - NodeWidgetContext( - context: context, - node: editorState.document.root, - editorState: editorState, + child: FlowyToolbar( + key: editorState.service.toolbarServiceKey, + editorState: editorState, + child: + editorState.service.renderPluginService.buildPluginWidget( + NodeWidgetContext( + context: context, + node: editorState.document.root, + editorState: editorState, + ), ), ), ), diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart index 9121fa1868..a9732d8a20 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/image/image_node_builder_test.dart @@ -49,9 +49,10 @@ void main() async { final editorRect = tester.getRect(editorFinder); final leftImageRect = tester.getRect(imageFinder.at(0)); - expect(leftImageRect.left, editorRect.left); + expect(leftImageRect.left, editor.editorState.editorStyle.padding.left); final rightImageRect = tester.getRect(imageFinder.at(2)); - expect(rightImageRect.right, editorRect.right); + expect(rightImageRect.right, + editorRect.right - editor.editorState.editorStyle.padding.right); final centerImageRect = tester.getRect(imageFinder.at(1)); expect(centerImageRect.left, (leftImageRect.left + rightImageRect.left) / 2.0); @@ -73,8 +74,8 @@ void main() async { leftImage.onAlign(Alignment.centerRight); await tester.pump(const Duration(milliseconds: 100)); expect( - tester.getRect(imageFinder.at(0)).left, - rightImageRect.left, + tester.getRect(imageFinder.at(0)).right, + rightImageRect.right, ); }); });