From 3eaa31c68c65bd99b4be67259226bb93d1cb26bb Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 30 Aug 2022 14:28:17 +0800 Subject: [PATCH 1/2] feat: #931 highlight the status of the currently selected style in toolbar --- .../extensions/editor_state_extensions.dart | 8 + .../src/extensions/text_node_extensions.dart | 90 +++++++---- .../src/render/rich_text/rich_text_style.dart | 2 + .../lib/src/render/toolbar/toolbar_item.dart | 147 +++++++++++++++--- .../render/toolbar/toolbar_item_widget.dart | 5 +- .../src/render/toolbar/toolbar_widget.dart | 1 + .../format_rich_text_style.dart | 6 +- .../lib/src/service/toolbar_service.dart | 5 + .../toolbar/toolbar_item_widget_test.dart | 18 ++- ..._text_style_by_command_x_handler_test.dart | 16 +- .../test/service/toolbar_service_test.dart | 12 +- 11 files changed, 240 insertions(+), 70 deletions(-) create mode 100644 frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/editor_state_extensions.dart diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/editor_state_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/editor_state_extensions.dart new file mode 100644 index 0000000000..56b0c7726f --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/editor_state_extensions.dart @@ -0,0 +1,8 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; + +extension EditorStateExtensions on EditorState { + List get selectedTextNodes => + service.selectionService.currentSelectedNodes + .whereType() + .toList(growable: false); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index 119cbae8d2..9fd85fdb76 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -29,56 +29,63 @@ extension TextNodeExtension on TextNode { } bool allSatisfyLinkInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.href, selection, (value) { + allSatisfyInSelection(selection, StyleKey.href, (value) { return value != null; }); bool allSatisfyBoldInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.bold, selection, (value) { + allSatisfyInSelection(selection, StyleKey.bold, (value) { return value == true; }); bool allSatisfyItalicInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.italic, selection, (value) { + allSatisfyInSelection(selection, StyleKey.italic, (value) { return value == true; }); bool allSatisfyUnderlineInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.underline, selection, (value) { + allSatisfyInSelection(selection, StyleKey.underline, (value) { return value == true; }); bool allSatisfyStrikethroughInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.strikethrough, selection, (value) { + allSatisfyInSelection(selection, StyleKey.strikethrough, (value) { return value == true; }); bool allSatisfyInSelection( - String styleKey, Selection selection, + String styleKey, bool Function(dynamic value) test, ) { - final ops = delta.whereType(); - final startOffset = - selection.isBackward ? selection.start.offset : selection.end.offset; - final endOffset = - selection.isBackward ? selection.end.offset : selection.start.offset; - var start = 0; - for (final op in ops) { - if (start >= endOffset) { - break; + if (StyleKey.globalStyleKeys.contains(styleKey)) { + if (attributes.containsKey(styleKey)) { + return test(attributes[styleKey]); } - final length = op.length; - if (start < endOffset && start + length > startOffset) { - if (op.attributes == null || - !op.attributes!.containsKey(styleKey) || - !test(op.attributes![styleKey])) { - return false; + } else if (StyleKey.partialStyleKeys.contains(styleKey)) { + final ops = delta.whereType(); + final startOffset = + selection.isBackward ? selection.start.offset : selection.end.offset; + final endOffset = + selection.isBackward ? selection.end.offset : selection.start.offset; + var start = 0; + for (final op in ops) { + if (start >= endOffset) { + break; } + final length = op.length; + if (start < endOffset && start + length > startOffset) { + if (op.attributes == null || + !op.attributes!.containsKey(styleKey) || + !test(op.attributes![styleKey])) { + return false; + } + } + start += length; } - start += length; + return true; } - return true; + return false; } bool allNotSatisfyInSelection( @@ -111,29 +118,44 @@ extension TextNodeExtension on TextNode { } extension TextNodesExtension on List { - bool allSatisfyBoldInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.bold, selection, true); + bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection( + selection, + StyleKey.bold, + (value) => value == true, + ); bool allSatisfyItalicInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.italic, selection, true); + allSatisfyInSelection( + selection, + StyleKey.italic, + (value) => value == true, + ); bool allSatisfyUnderlineInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.underline, selection, true); + allSatisfyInSelection( + selection, + StyleKey.underline, + (value) => value == true, + ); bool allSatisfyStrikethroughInSelection(Selection selection) => - allSatisfyInSelection(StyleKey.strikethrough, selection, true); + allSatisfyInSelection( + selection, + StyleKey.strikethrough, + (value) => value == true, + ); bool allSatisfyInSelection( - String styleKey, Selection selection, - dynamic matchValue, + String styleKey, + bool Function(dynamic value) test, ) { if (isEmpty) { return false; } if (length == 1) { - return first.allSatisfyInSelection(styleKey, selection, (value) { - return value == matchValue; + return first.allSatisfyInSelection(selection, styleKey, (value) { + return test(value); }); } else { for (var i = 0; i < length; i++) { @@ -154,8 +176,8 @@ extension TextNodesExtension on List { end: Position(path: node.path, offset: node.toRawString().length), ); } - if (!node.allSatisfyInSelection(styleKey, newSelection, (value) { - return value == matchValue; + if (!node.allSatisfyInSelection(newSelection, styleKey, (value) { + return test(value); })) { return false; } 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 6270127610..93d088d66f 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 @@ -47,6 +47,8 @@ class StyleKey { StyleKey.italic, StyleKey.underline, StyleKey.strikethrough, + StyleKey.backgroundColor, + StyleKey.href, ]; static List globalStyleKeys = [ 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 9a1b2f1c02..ff94dfc111 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 @@ -4,38 +4,43 @@ 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'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; +import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart'; import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart'; import 'package:flutter/material.dart'; import 'package:rich_clipboard/rich_clipboard.dart'; -typedef ToolbarEventHandler = void Function( +typedef ToolbarItemEventHandler = void Function( EditorState editorState, BuildContext context); -typedef ToolbarShowValidator = bool Function(EditorState editorState); +typedef ToolbarItemValidator = bool Function(EditorState editorState); +typedef ToolbarItemHighlightCallback = bool Function(EditorState editorState); class ToolbarItem { ToolbarItem({ required this.id, required this.type, - required this.icon, + required this.iconBuilder, this.tooltipsMessage = '', required this.validator, + required this.highlightCallback, required this.handler, }); final String id; final int type; - final Widget icon; + final Widget Function(bool isHighlight) iconBuilder; final String tooltipsMessage; - final ToolbarShowValidator validator; - final ToolbarEventHandler handler; + final ToolbarItemValidator validator; + final ToolbarItemEventHandler handler; + final ToolbarItemHighlightCallback highlightCallback; factory ToolbarItem.divider() { return ToolbarItem( id: 'divider', type: -1, - icon: const FlowySvg(name: 'toolbar/divider'), + iconBuilder: (_) => const FlowySvg(name: 'toolbar/divider'), validator: (editorState) => true, handler: (editorState, context) {}, + highlightCallback: (editorState) => false, ); } @@ -59,103 +64,205 @@ List defaultToolbarItems = [ id: 'appflowy.toolbar.h1', type: 1, tooltipsMessage: 'Heading 1', - icon: const FlowySvg(name: 'toolbar/h1'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/h1', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.heading, + (value) => value == StyleKey.h1, + ), handler: (editorState, context) => formatHeading(editorState, StyleKey.h1), ), ToolbarItem( id: 'appflowy.toolbar.h2', type: 1, tooltipsMessage: 'Heading 2', - icon: const FlowySvg(name: 'toolbar/h2'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/h2', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.heading, + (value) => value == StyleKey.h2, + ), handler: (editorState, context) => formatHeading(editorState, StyleKey.h2), ), ToolbarItem( id: 'appflowy.toolbar.h3', type: 1, tooltipsMessage: 'Heading 3', - icon: const FlowySvg(name: 'toolbar/h3'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/h3', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.heading, + (value) => value == StyleKey.h3, + ), handler: (editorState, context) => formatHeading(editorState, StyleKey.h3), ), ToolbarItem( id: 'appflowy.toolbar.bold', type: 2, tooltipsMessage: 'Bold', - icon: const FlowySvg(name: 'toolbar/bold'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/bold', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.bold, + (value) => value == true, + ), handler: (editorState, context) => formatBold(editorState), ), ToolbarItem( id: 'appflowy.toolbar.italic', type: 2, tooltipsMessage: 'Italic', - icon: const FlowySvg(name: 'toolbar/italic'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/italic', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.italic, + (value) => value == true, + ), handler: (editorState, context) => formatItalic(editorState), ), ToolbarItem( id: 'appflowy.toolbar.underline', type: 2, tooltipsMessage: 'Underline', - icon: const FlowySvg(name: 'toolbar/underline'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/underline', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.underline, + (value) => value == true, + ), handler: (editorState, context) => formatUnderline(editorState), ), ToolbarItem( id: 'appflowy.toolbar.strikethrough', type: 2, tooltipsMessage: 'Strikethrough', - icon: const FlowySvg(name: 'toolbar/strikethrough'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/strikethrough', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.strikethrough, + (value) => value == true, + ), handler: (editorState, context) => formatStrikethrough(editorState), ), ToolbarItem( id: 'appflowy.toolbar.quote', type: 3, tooltipsMessage: 'Quote', - icon: const FlowySvg(name: 'toolbar/quote'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/quote', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.subtype, + (value) => value == StyleKey.quote, + ), handler: (editorState, context) => formatQuote(editorState), ), ToolbarItem( id: 'appflowy.toolbar.bulleted_list', type: 3, tooltipsMessage: 'Bulleted list', - icon: const FlowySvg(name: 'toolbar/bulleted_list'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/bulleted_list', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.subtype, + (value) => value == StyleKey.bulletedList, + ), handler: (editorState, context) => formatBulletedList(editorState), ), ToolbarItem( id: 'appflowy.toolbar.link', type: 4, tooltipsMessage: 'Link', - icon: const FlowySvg(name: 'toolbar/link'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/link', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _onlyShowInSingleTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.href, + (value) => value != null, + ), handler: (editorState, context) => showLinkMenu(context, editorState), ), ToolbarItem( id: 'appflowy.toolbar.highlight', type: 4, tooltipsMessage: 'Highlight', - icon: const FlowySvg(name: 'toolbar/highlight'), + iconBuilder: (isHighlight) => FlowySvg( + name: 'toolbar/highlight', + color: isHighlight ? Colors.lightBlue : null, + ), validator: _showInTextSelection, + highlightCallback: (editorState) => _allSatisfy( + editorState, + StyleKey.backgroundColor, + (value) => value != null, + ), handler: (editorState, context) => formatHighlight(editorState), ), ]; -ToolbarShowValidator _onlyShowInSingleTextSelection = (editorState) { +ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) { final nodes = editorState.service.selectionService.currentSelectedNodes; return (nodes.length == 1 && nodes.first is TextNode); }; -ToolbarShowValidator _showInTextSelection = (editorState) { +ToolbarItemValidator _showInTextSelection = (editorState) { final nodes = editorState.service.selectionService.currentSelectedNodes .whereType(); return nodes.isNotEmpty; }; +bool _allSatisfy( + EditorState editorState, + String styleKey, + bool Function(dynamic value) test, +) { + final selection = editorState.service.selectionService.currentSelection.value; + return selection != null && + editorState.selectedTextNodes.allSatisfyInSelection( + selection, + styleKey, + test, + ); +} + OverlayEntry? _linkMenuOverlay; EditorState? _editorState; bool _changeSelectionInner = false; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart index ce89eef126..4fefa8eadb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart @@ -6,11 +6,13 @@ class ToolbarItemWidget extends StatelessWidget { const ToolbarItemWidget({ Key? key, required this.item, + required this.isHighlight, required this.onPressed, }) : super(key: key); final ToolbarItem item; final VoidCallback onPressed; + final bool isHighlight; @override Widget build(BuildContext context) { @@ -23,8 +25,9 @@ class ToolbarItemWidget extends StatelessWidget { child: MouseRegion( cursor: SystemMouseCursors.click, child: IconButton( + highlightColor: Colors.yellow, padding: EdgeInsets.zero, - icon: item.icon, + icon: item.iconBuilder(isHighlight), iconSize: 28, onPressed: onPressed, ), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart index 395c6818bb..18c2cdc0b1 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart @@ -64,6 +64,7 @@ class _ToolbarWidgetState extends State with ToolbarMixin { (item) => Center( child: ToolbarItemWidget( item: item, + isHighlight: item.highlightCallback(widget.editorState), onPressed: () { item.handler(widget.editorState, context); }, diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart index c4f765f2f4..038f3119af 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart @@ -157,7 +157,7 @@ bool formatRichTextPartialStyle(EditorState editorState, String styleKey, } bool _allSatisfyInSelection( - EditorState editorState, String styleKey, dynamic value) { + EditorState editorState, String styleKey, dynamic matchValue) { final selection = editorState.service.selectionService.currentSelection.value; final nodes = editorState.service.selectionService.currentSelectedNodes; final textNodes = nodes.whereType().toList(growable: false); @@ -166,7 +166,9 @@ bool _allSatisfyInSelection( return false; } - return textNodes.allSatisfyInSelection(styleKey, selection, value); + return textNodes.allSatisfyInSelection(selection, styleKey, (value) { + return value == matchValue; + }); } bool formatRichTextStyle(EditorState editorState, Attributes attributes) { 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 8dba7dcb8e..5d79c44824 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 @@ -102,4 +102,9 @@ class _FlowyToolbarState extends State } return dividedItems; } + + // List _highlightItems( + // List items, + // Selection selection, + // ) {} } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart index 87ae922d91..3d212691cb 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/render/toolbar/toolbar_item_widget_test.dart @@ -11,17 +11,28 @@ void main() async { group('toolbar_item_widget.dart', () { testWidgets('test single toolbar item widget', (tester) async { final key = GlobalKey(); + final iconKey = GlobalKey(); var hit = false; final item = ToolbarItem( id: 'appflowy.toolbar.test', type: 1, - icon: const Icon(Icons.abc), + iconBuilder: (isHighlight) { + return Icon( + key: iconKey, + Icons.abc, + color: isHighlight ? Colors.lightBlue : null, + ); + }, validator: (editorState) => true, handler: (editorState, context) {}, + highlightCallback: (editorState) { + return true; + }, ); final widget = ToolbarItemWidget( key: key, item: item, + isHighlight: true, onPressed: (() { hit = true; }), @@ -36,6 +47,11 @@ void main() async { ); expect(find.byKey(key), findsOneWidget); + expect(find.byKey(iconKey), findsOneWidget); + expect( + (tester.firstWidget(find.byKey(iconKey)) as Icon).color, + Colors.lightBlue, + ); await tester.tap(find.byKey(key)); await tester.pumpAndSettle(); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart index e29308ebbc..d7afacb27a 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart @@ -2,7 +2,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/link_menu/link_menu.dart'; import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/extensions/text_node_extensions.dart'; -import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -91,8 +90,8 @@ Future _testUpdateTextStyleByCommandX( var textNode = editor.nodeAtPath([1]) as TextNode; expect( textNode.allSatisfyInSelection( - matchStyle, selection, + matchStyle, (value) { return value == matchValue; }, @@ -110,8 +109,8 @@ Future _testUpdateTextStyleByCommandX( textNode = editor.nodeAtPath([1]) as TextNode; expect( textNode.allSatisfyInSelection( - matchStyle, selection, + matchStyle, (value) { return value == matchValue; }, @@ -144,12 +143,12 @@ Future _testUpdateTextStyleByCommandX( for (final node in nodes) { expect( node.allSatisfyInSelection( - matchStyle, Selection.single( path: node.path, startOffset: 0, endOffset: text.length, ), + matchStyle, (value) { return value == matchValue; }, @@ -196,11 +195,6 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { // show toolbar expect(find.byType(ToolbarWidget), findsOneWidget); - final item = defaultToolbarItems - .where((item) => item.id == 'appflowy.toolbar.link') - .first; - expect(find.byWidget(item.icon), findsOneWidget); - // trigger the link menu await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true); @@ -215,8 +209,8 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { final node = editor.nodeAtPath([1]) as TextNode; expect( node.allSatisfyInSelection( - StyleKey.href, selection, + StyleKey.href, (value) => value == link, ), true); @@ -244,8 +238,8 @@ Future _testLinkMenuInSingleTextSelection(WidgetTester tester) async { expect( node.allSatisfyInSelection( - StyleKey.href, selection, + StyleKey.href, (value) => value == link, ), false); diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart index 9d833095e7..c979f6121b 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -1,5 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; import 'package:flutter_test/flutter_test.dart'; import '../infra/test_editor.dart'; @@ -30,7 +31,16 @@ void main() async { final item = defaultToolbarItems .where((item) => item.id == 'appflowy.toolbar.link') .first; - expect(find.byWidget(item.icon), findsNothing); + final finder = find.byType(ToolbarItemWidget); + + expect( + tester + .widgetList(finder) + .toList(growable: false) + .where((element) => element.item.id == item.id) + .isEmpty, + true, + ); }); }); } From 0f334962ceea9a77d2b5d06909a977fc548c44ca Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 30 Aug 2022 16:25:32 +0800 Subject: [PATCH 2/2] test: add more test cases to toolbar_service --- .../test/service/toolbar_service_test.dart | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart index c979f6121b..418a333189 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart +++ b/frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart @@ -1,4 +1,5 @@ import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart'; import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; @@ -42,5 +43,125 @@ void main() async { true, ); }); + + testWidgets( + 'Test toolbar service in single text selection with StyleKey.partialStyleKeys', + (tester) async { + final attributes = StyleKey.partialStyleKeys.fold({}, + (previousValue, element) { + if (element == StyleKey.backgroundColor) { + previousValue[element] = '0x6000BCF0'; + } else if (element == StyleKey.href) { + previousValue[element] = 'appflowy.io'; + } else { + previousValue[element] = true; + } + return previousValue; + }); + + const text = 'Welcome to Appflowy 😁'; + final editor = tester.editor + ..insertTextNode(text) + ..insertTextNode( + null, + delta: Delta([ + TextInsert(text), + TextInsert(text, attributes), + TextInsert(text), + ]), + ); + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0, endOffset: text.length), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + + void testHighlight(bool expectedValue) { + for (final styleKey in StyleKey.partialStyleKeys) { + var key = styleKey; + if (styleKey == StyleKey.backgroundColor) { + key = 'highlight'; + } else if (styleKey == StyleKey.href) { + key = 'link'; + } + final itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.$key'); + expect(itemWidget.isHighlight, expectedValue); + } + } + + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0, endOffset: text.length * 2), + ); + testHighlight(false); + + await editor.updateSelection( + Selection.single( + path: [1], + startOffset: text.length, + endOffset: text.length * 2, + ), + ); + testHighlight(true); + + await editor.updateSelection( + Selection.single( + path: [1], + startOffset: text.length + 2, + endOffset: text.length * 2 - 2, + ), + ); + testHighlight(true); + }); + + testWidgets( + 'Test toolbar service in single text selection with StyleKey.globalStyleKeys', + (tester) async { + const text = 'Welcome to Appflowy 😁'; + + final editor = tester.editor + ..insertTextNode(text, attributes: { + StyleKey.subtype: StyleKey.heading, + StyleKey.heading: StyleKey.h1, + }) + ..insertTextNode( + text, + attributes: {StyleKey.subtype: StyleKey.quote}, + ) + ..insertTextNode( + text, + attributes: {StyleKey.subtype: StyleKey.bulletedList}, + ); + await editor.startTesting(); + + await editor.updateSelection( + Selection.single(path: [0], startOffset: 0, endOffset: text.length), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + var itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.h1'); + expect(itemWidget.isHighlight, true); + + await editor.updateSelection( + Selection.single(path: [1], startOffset: 0, endOffset: text.length), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.quote'); + expect(itemWidget.isHighlight, true); + + await editor.updateSelection( + Selection.single(path: [2], startOffset: 0, endOffset: text.length), + ); + expect(find.byType(ToolbarWidget), findsOneWidget); + itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.bulleted_list'); + expect(itemWidget.isHighlight, true); + }); }); } + +ToolbarItemWidget _itemWidgetForId(WidgetTester tester, String id) { + final finder = find.byType(ToolbarItemWidget); + final itemWidgets = tester + .widgetList(finder) + .where((element) => element.item.id == id); + expect(itemWidgets.length, 1); + return itemWidgets.first; +}