Browse Source

feat: implement highlight in toolbar

Lucas.Xu 2 years ago
parent
commit
3e2dc161f8

+ 5 - 0
frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/highlight.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M13 7.87829V5.125C13 4.82663 12.8946 4.54048 12.7071 4.3295C12.5196 4.11853 12.2652 4 12 4H4C3.73478 4 3.48043 4.11853 3.29289 4.3295C3.10536 4.54048 3 4.82663 3 5.125V10.875C3 11.1734 3.10536 11.4595 3.29289 11.6705C3.48043 11.8815 3.73478 12 4 12H8.44737" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
+<rect width="1" height="4" rx="0.5" transform="matrix(-1 0 0 1 13.5 10)" fill="white"/>
+<rect width="1" height="4" rx="0.5" transform="matrix(1.19249e-08 -1 -1 -1.19249e-08 15 12.5)" fill="white"/>
+</svg>

+ 27 - 15
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart

@@ -7,18 +7,22 @@ import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 
 extension TextNodeExtension on TextNode {
   bool allSatisfyBoldInSelection(Selection selection) =>
-      allSatisfyInSelection(StyleKey.bold, selection);
+      allSatisfyInSelection(StyleKey.bold, true, selection);
 
   bool allSatisfyItalicInSelection(Selection selection) =>
-      allSatisfyInSelection(StyleKey.italic, selection);
+      allSatisfyInSelection(StyleKey.italic, true, selection);
 
   bool allSatisfyUnderlineInSelection(Selection selection) =>
-      allSatisfyInSelection(StyleKey.underline, selection);
+      allSatisfyInSelection(StyleKey.underline, true, selection);
 
   bool allSatisfyStrikethroughInSelection(Selection selection) =>
-      allSatisfyInSelection(StyleKey.strikethrough, selection);
+      allSatisfyInSelection(StyleKey.strikethrough, true, selection);
 
-  bool allSatisfyInSelection(String styleKey, Selection selection) {
+  bool allSatisfyInSelection(
+    String styleKey,
+    dynamic value,
+    Selection selection,
+  ) {
     final ops = delta.whereType<TextInsert>();
     final startOffset =
         selection.isBackward ? selection.start.offset : selection.end.offset;
@@ -33,7 +37,7 @@ extension TextNodeExtension on TextNode {
       if (start < endOffset && start + length > startOffset) {
         if (op.attributes == null ||
             !op.attributes!.containsKey(styleKey) ||
-            op.attributes![styleKey] == false) {
+            op.attributes![styleKey] != value) {
           return false;
         }
       }
@@ -42,7 +46,11 @@ extension TextNodeExtension on TextNode {
     return true;
   }
 
-  bool allNotSatisfyInSelection(String styleKey, Selection selection) {
+  bool allNotSatisfyInSelection(
+    String styleKey,
+    dynamic value,
+    Selection selection,
+  ) {
     final ops = delta.whereType<TextInsert>();
     final startOffset =
         selection.isBackward ? selection.start.offset : selection.end.offset;
@@ -57,7 +65,7 @@ extension TextNodeExtension on TextNode {
       if (start < endOffset && start + length > startOffset) {
         if (op.attributes != null &&
             op.attributes!.containsKey(styleKey) &&
-            op.attributes![styleKey] == true) {
+            op.attributes![styleKey] == value) {
           return false;
         }
       }
@@ -69,23 +77,27 @@ extension TextNodeExtension on TextNode {
 
 extension TextNodesExtension on List<TextNode> {
   bool allSatisfyBoldInSelection(Selection selection) =>
-      allSatisfyInSelection(StyleKey.bold, selection);
+      allSatisfyInSelection(StyleKey.bold, selection, true);
 
   bool allSatisfyItalicInSelection(Selection selection) =>
-      allSatisfyInSelection(StyleKey.italic, selection);
+      allSatisfyInSelection(StyleKey.italic, selection, true);
 
   bool allSatisfyUnderlineInSelection(Selection selection) =>
-      allSatisfyInSelection(StyleKey.underline, selection);
+      allSatisfyInSelection(StyleKey.underline, selection, true);
 
   bool allSatisfyStrikethroughInSelection(Selection selection) =>
-      allSatisfyInSelection(StyleKey.strikethrough, selection);
+      allSatisfyInSelection(StyleKey.strikethrough, selection, true);
 
-  bool allSatisfyInSelection(String styleKey, Selection selection) {
+  bool allSatisfyInSelection(
+    String styleKey,
+    Selection selection,
+    dynamic value,
+  ) {
     if (isEmpty) {
       return false;
     }
     if (length == 1) {
-      return first.allSatisfyInSelection(styleKey, selection);
+      return first.allSatisfyInSelection(styleKey, value, selection);
     } else {
       for (var i = 0; i < length; i++) {
         final node = this[i];
@@ -105,7 +117,7 @@ extension TextNodesExtension on List<TextNode> {
             end: Position(path: node.path, offset: node.toRawString().length),
           );
         }
-        if (!node.allSatisfyInSelection(styleKey, newSelection)) {
+        if (!node.allSatisfyInSelection(styleKey, value, newSelection)) {
           return false;
         }
       }

+ 2 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart

@@ -66,6 +66,8 @@ class StyleKey {
 double defaultMaxTextNodeWidth = 780.0;
 double defaultLinePadding = 8.0;
 double baseFontSize = 16.0;
+String defaultHighlightColor = '0x6000BCF0';
+String defaultBackgroundColor = '0x00000000';
 // TODO: customize.
 Map<String, double> headingToFontSize = {
   StyleKey.h1: baseFontSize + 15,

+ 3 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart

@@ -16,6 +16,7 @@ ToolbarEventHandlers defaultToolbarEventHandlers = {
   'underline': (editorState) => formatUnderline(editorState),
   'quote': (editorState) => formatQuote(editorState),
   'bulleted_list': (editorState) => formatBulletedList(editorState),
+  'highlight': (editorState) => formatHighlight(editorState),
   'Text': (editorState) => formatText(editorState),
   'H1': (editorState) => formatHeading(editorState, StyleKey.h1),
   'H2': (editorState) => formatHeading(editorState, StyleKey.h2),
@@ -103,6 +104,8 @@ class _ToolbarWidgetState extends State<ToolbarWidget> with ToolbarMixin {
             _centerToolbarIcon('quote'),
             // _centerToolbarIcon('number_list'),
             _centerToolbarIcon('bulleted_list'),
+            _centerToolbarIcon('divider', width: 2),
+            _centerToolbarIcon('highlight'),
           ],
         ),
       ),

+ 20 - 7
frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart

@@ -139,7 +139,25 @@ bool formatStrikethrough(EditorState editorState) {
   return formatRichTextPartialStyle(editorState, StyleKey.strikethrough);
 }
 
-bool formatRichTextPartialStyle(EditorState editorState, String styleKey) {
+bool formatHighlight(EditorState editorState) {
+  bool value = _allSatisfyInSelection(
+      editorState, StyleKey.backgroundColor, defaultHighlightColor);
+  return formatRichTextPartialStyle(editorState, StyleKey.backgroundColor,
+      customValue: value ? defaultBackgroundColor : defaultHighlightColor);
+}
+
+bool formatRichTextPartialStyle(EditorState editorState, String styleKey,
+    {Object? customValue}) {
+  Attributes attributes = {
+    styleKey: customValue ??
+        !_allSatisfyInSelection(editorState, styleKey, customValue ?? true),
+  };
+
+  return formatRichTextStyle(editorState, attributes);
+}
+
+bool _allSatisfyInSelection(
+    EditorState editorState, String styleKey, dynamic value) {
   final selection = editorState.service.selectionService.currentSelection.value;
   final nodes = editorState.service.selectionService.currentSelectedNodes;
   final textNodes = nodes.whereType<TextNode>().toList(growable: false);
@@ -148,12 +166,7 @@ bool formatRichTextPartialStyle(EditorState editorState, String styleKey) {
     return false;
   }
 
-  bool value = !textNodes.allSatisfyInSelection(styleKey, selection);
-  Attributes attributes = {
-    styleKey: value,
-  };
-
-  return formatRichTextStyle(editorState, attributes);
+  return textNodes.allSatisfyInSelection(styleKey, selection, value);
 }
 
 bool formatRichTextStyle(EditorState editorState, Attributes attributes) {

+ 4 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart

@@ -31,6 +31,10 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
       event.isShiftPressed) {
     formatStrikethrough(editorState);
     return KeyEventResult.handled;
+  } else if (event.logicalKey == LogicalKeyboardKey.keyH &&
+      event.isShiftPressed) {
+    formatHighlight(editorState);
+    return KeyEventResult.handled;
   }
 
   return KeyEventResult.ignored;

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -80,7 +80,7 @@ class FlowySelection extends StatefulWidget {
   const FlowySelection({
     Key? key,
     this.cursorColor = Colors.black,
-    this.selectionColor = const Color.fromARGB(60, 61, 61, 213),
+    this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
     required this.editorState,
     required this.child,
   }) : super(key: key);

+ 3 - 0
frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart

@@ -115,6 +115,9 @@ extension on LogicalKeyboardKey {
     if (this == LogicalKeyboardKey.keyU) {
       return PhysicalKeyboardKey.keyU;
     }
+    if (this == LogicalKeyboardKey.keyH) {
+      return PhysicalKeyboardKey.keyH;
+    }
     if (this == LogicalKeyboardKey.keyZ) {
       return PhysicalKeyboardKey.keyZ;
     }

+ 36 - 10
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart

@@ -15,6 +15,7 @@ void main() async {
       await _testUpdateTextStyleByCommandX(
         tester,
         StyleKey.bold,
+        true,
         LogicalKeyboardKey.keyB,
       );
     });
@@ -22,6 +23,7 @@ void main() async {
       await _testUpdateTextStyleByCommandX(
         tester,
         StyleKey.italic,
+        true,
         LogicalKeyboardKey.keyI,
       );
     });
@@ -29,21 +31,40 @@ void main() async {
       await _testUpdateTextStyleByCommandX(
         tester,
         StyleKey.underline,
+        true,
         LogicalKeyboardKey.keyU,
       );
     });
-    testWidgets('Presses Command + S to update text style', (tester) async {
+    testWidgets('Presses Command + Shift + S to update text style',
+        (tester) async {
       await _testUpdateTextStyleByCommandX(
         tester,
         StyleKey.strikethrough,
+        true,
         LogicalKeyboardKey.keyS,
       );
     });
+
+    testWidgets('Presses Command + Shift + H to update text style',
+        (tester) async {
+      await _testUpdateTextStyleByCommandX(
+        tester,
+        StyleKey.backgroundColor,
+        defaultHighlightColor,
+        LogicalKeyboardKey.keyH,
+      );
+    });
   });
 }
 
 Future<void> _testUpdateTextStyleByCommandX(
-    WidgetTester tester, String matchStyle, LogicalKeyboardKey key) async {
+  WidgetTester tester,
+  String matchStyle,
+  dynamic matchValue,
+  LogicalKeyboardKey key,
+) async {
+  final isShiftPressed =
+      key == LogicalKeyboardKey.keyS || key == LogicalKeyboardKey.keyH;
   const text = 'Welcome to Appflowy 😁';
   final editor = tester.editor
     ..insertTextNode(text)
@@ -56,31 +77,34 @@ Future<void> _testUpdateTextStyleByCommandX(
   await editor.updateSelection(selection);
   await editor.pressLogicKey(
     key,
-    isShiftPressed: key == LogicalKeyboardKey.keyS,
+    isShiftPressed: isShiftPressed,
     isMetaPressed: true,
   );
   var textNode = editor.nodeAtPath([1]) as TextNode;
-  expect(textNode.allSatisfyInSelection(matchStyle, selection), true);
+  expect(
+      textNode.allSatisfyInSelection(matchStyle, matchValue, selection), true);
 
   selection =
       Selection.single(path: [1], startOffset: 0, endOffset: text.length);
   await editor.updateSelection(selection);
   await editor.pressLogicKey(
     key,
-    isShiftPressed: key == LogicalKeyboardKey.keyS,
+    isShiftPressed: isShiftPressed,
     isMetaPressed: true,
   );
   textNode = editor.nodeAtPath([1]) as TextNode;
-  expect(textNode.allSatisfyInSelection(matchStyle, selection), true);
+  expect(
+      textNode.allSatisfyInSelection(matchStyle, matchValue, selection), true);
 
   await editor.updateSelection(selection);
   await editor.pressLogicKey(
     key,
-    isShiftPressed: key == LogicalKeyboardKey.keyS,
+    isShiftPressed: isShiftPressed,
     isMetaPressed: true,
   );
   textNode = editor.nodeAtPath([1]) as TextNode;
-  expect(textNode.allNotSatisfyInSelection(matchStyle, selection), true);
+  expect(textNode.allNotSatisfyInSelection(matchStyle, matchValue, selection),
+      true);
 
   selection = Selection(
     start: Position(path: [0], offset: 0),
@@ -89,7 +113,7 @@ Future<void> _testUpdateTextStyleByCommandX(
   await editor.updateSelection(selection);
   await editor.pressLogicKey(
     key,
-    isShiftPressed: key == LogicalKeyboardKey.keyS,
+    isShiftPressed: isShiftPressed,
     isMetaPressed: true,
   );
   var nodes = editor.editorState.service.selectionService.currentSelectedNodes
@@ -99,6 +123,7 @@ Future<void> _testUpdateTextStyleByCommandX(
     expect(
       node.allSatisfyInSelection(
         matchStyle,
+        matchValue,
         Selection.single(
             path: node.path, startOffset: 0, endOffset: text.length),
       ),
@@ -109,7 +134,7 @@ Future<void> _testUpdateTextStyleByCommandX(
   await editor.updateSelection(selection);
   await editor.pressLogicKey(
     key,
-    isShiftPressed: key == LogicalKeyboardKey.keyS,
+    isShiftPressed: isShiftPressed,
     isMetaPressed: true,
   );
   nodes = editor.editorState.service.selectionService.currentSelectedNodes
@@ -119,6 +144,7 @@ Future<void> _testUpdateTextStyleByCommandX(
     expect(
       node.allNotSatisfyInSelection(
         matchStyle,
+        matchValue,
         Selection.single(
             path: node.path, startOffset: 0, endOffset: text.length),
       ),