소스 검색

[FR] Shortcut for toggling checkbox (#1817)

* feat: shortcut for toggling checkbox

* refactor: separate checkbox event handler

* test: chechbox event handler

* chore: remove unused imports

* refactor: command to ctrl and enter

* refactor: handler to use transactions

* test: checkbox event handler

* chore: remove unused import

* refactor: simplify handler logic
Mayur Mahajan 2 년 전
부모
커밋
95ec607482

+ 38 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/checkbox_event_handler.dart

@@ -0,0 +1,38 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+ShortcutEventHandler toggleCheckbox = (editorState, event) {
+  final selection = editorState.service.selectionService.currentSelection.value;
+  final nodes = editorState.service.selectionService.currentSelectedNodes;
+  final checkboxTextNodes = nodes
+      .where(
+        (element) =>
+            element is TextNode &&
+            element.subtype == BuiltInAttributeKey.checkbox,
+      )
+      .toList(growable: false);
+
+  if (selection == null || checkboxTextNodes.isEmpty) {
+    return KeyEventResult.ignored;
+  }
+
+  bool isAllCheckboxesChecked = checkboxTextNodes
+      .every((node) => node.attributes[BuiltInAttributeKey.checkbox] == true);
+  final transaction = editorState.transaction;
+  transaction.afterSelection = selection;
+
+  if (isAllCheckboxesChecked) {
+    //if all the checkboxes are checked, then make all of the checkboxes unchecked
+    for (final node in checkboxTextNodes) {
+      transaction.updateNode(node, {BuiltInAttributeKey.checkbox: false});
+    }
+  } else {
+    //If any one of the checkboxes is unchecked then make all checkboxes checked
+    for (final node in checkboxTextNodes) {
+      transaction.updateNode(node, {BuiltInAttributeKey.checkbox: true});
+    }
+  }
+
+  editorState.apply(transaction);
+  return KeyEventResult.handled;
+};

+ 8 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart

@@ -14,6 +14,7 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/format_s
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/space_on_web_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/tab_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
+import 'package:appflowy_editor/src/service/internal_key_event_handlers/checkbox_event_handler.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
 import 'package:flutter/foundation.dart';
 
@@ -159,6 +160,13 @@ List<ShortcutEvent> builtInShortcutEvents = [
     linuxCommand: 'ctrl+u',
     handler: formatUnderlineEventHandler,
   ),
+  ShortcutEvent(
+    key: 'Toggle Checkbox',
+    command: 'meta+enter',
+    windowsCommand: 'ctrl+enter',
+    linuxCommand: 'ctrl+enter',
+    handler: toggleCheckbox,
+  ),
   ShortcutEvent(
     key: 'Format strikethrough',
     command: 'meta+shift+s',

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

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

+ 241 - 0
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/checkbox_event_handler_test.dart

@@ -0,0 +1,241 @@
+import 'dart:io';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/service/shortcut_event/built_in_shortcut_events.dart';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import '../../infra/test_editor.dart';
+
+void main() async {
+  setUpAll(() {
+    TestWidgetsFlutterBinding.ensureInitialized();
+  });
+
+  group('checkbox_event_handler_test.dart', () {
+    testWidgets('toggle checkbox with shortcut ctrl+enter', (tester) async {
+      const text = 'Checkbox1';
+      final editor = tester.editor
+        ..insertTextNode(
+          '',
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: false,
+          },
+          delta: Delta(
+            operations: [TextInsert(text)],
+          ),
+        );
+      await editor.startTesting();
+      await editor.updateSelection(
+        Selection.single(path: [0], startOffset: text.length),
+      );
+
+      final checkboxNode = editor.nodeAtPath([0]) as TextNode;
+      expect(checkboxNode.subtype, BuiltInAttributeKey.checkbox);
+      expect(checkboxNode.attributes[BuiltInAttributeKey.checkbox], false);
+
+      for (final event in builtInShortcutEvents) {
+        if (event.key == 'Toggle Checkbox') {
+          event.updateCommand(
+            windowsCommand: 'ctrl+enter',
+            linuxCommand: 'ctrl+enter',
+            macOSCommand: 'meta+enter',
+          );
+        }
+      }
+
+      if (Platform.isWindows || Platform.isLinux) {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.enter,
+          isControlPressed: true,
+        );
+      } else {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.enter,
+          isMetaPressed: true,
+        );
+      }
+
+      expect(checkboxNode.attributes[BuiltInAttributeKey.checkbox], true);
+
+      await editor.updateSelection(
+        Selection.single(path: [0], startOffset: text.length),
+      );
+
+      if (Platform.isWindows || Platform.isLinux) {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.enter,
+          isControlPressed: true,
+        );
+      } else {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.enter,
+          isMetaPressed: true,
+        );
+      }
+
+      expect(checkboxNode.attributes[BuiltInAttributeKey.checkbox], false);
+    });
+
+    testWidgets(
+        'test if all checkboxes get unchecked after toggling them, if all of them were already checked',
+        (tester) async {
+      const text = 'Checkbox';
+      final editor = tester.editor
+        ..insertTextNode(
+          '',
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: true,
+          },
+          delta: Delta(
+            operations: [TextInsert(text)],
+          ),
+        )
+        ..insertTextNode(
+          '',
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: true,
+          },
+          delta: Delta(
+            operations: [TextInsert(text)],
+          ),
+        )
+        ..insertTextNode(
+          '',
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: true,
+          },
+          delta: Delta(
+            operations: [TextInsert(text)],
+          ),
+        );
+
+      await editor.startTesting();
+      await editor.updateSelection(
+        Selection.single(path: [0], startOffset: text.length),
+      );
+
+      final nodes =
+          editor.editorState.service.selectionService.currentSelectedNodes;
+      final checkboxTextNodes = nodes
+          .where(
+            (element) =>
+                element is TextNode &&
+                element.subtype == BuiltInAttributeKey.checkbox,
+          )
+          .toList(growable: false);
+
+      for (final node in checkboxTextNodes) {
+        expect(node.attributes[BuiltInAttributeKey.checkbox], true);
+      }
+
+      for (final event in builtInShortcutEvents) {
+        if (event.key == 'Toggle Checkbox') {
+          event.updateCommand(
+            windowsCommand: 'ctrl+enter',
+            linuxCommand: 'ctrl+enter',
+            macOSCommand: 'meta+enter',
+          );
+        }
+      }
+
+      if (Platform.isWindows || Platform.isLinux) {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.enter,
+          isControlPressed: true,
+        );
+      } else {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.enter,
+          isMetaPressed: true,
+        );
+      }
+
+      for (final node in checkboxTextNodes) {
+        expect(node.attributes[BuiltInAttributeKey.checkbox], false);
+      }
+    });
+
+    testWidgets(
+        'test if all checkboxes get checked after toggling them, if any one of them were already checked',
+        (tester) async {
+      const text = 'Checkbox';
+      final editor = tester.editor
+        ..insertTextNode(
+          '',
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: false,
+          },
+          delta: Delta(
+            operations: [TextInsert(text)],
+          ),
+        )
+        ..insertTextNode(
+          '',
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: true,
+          },
+          delta: Delta(
+            operations: [TextInsert(text)],
+          ),
+        )
+        ..insertTextNode(
+          '',
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: false,
+          },
+          delta: Delta(
+            operations: [TextInsert(text)],
+          ),
+        );
+
+      await editor.startTesting();
+      await editor.updateSelection(
+        Selection.single(path: [0], startOffset: text.length),
+      );
+
+      final nodes =
+          editor.editorState.service.selectionService.currentSelectedNodes;
+      final checkboxTextNodes = nodes
+          .where(
+            (element) =>
+                element is TextNode &&
+                element.subtype == BuiltInAttributeKey.checkbox,
+          )
+          .toList(growable: false);
+
+      for (final event in builtInShortcutEvents) {
+        if (event.key == 'Toggle Checkbox') {
+          event.updateCommand(
+            windowsCommand: 'ctrl+enter',
+            linuxCommand: 'ctrl+enter',
+            macOSCommand: 'meta+enter',
+          );
+        }
+      }
+
+      if (Platform.isWindows || Platform.isLinux) {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.enter,
+          isControlPressed: true,
+        );
+      } else {
+        await editor.pressLogicKey(
+          LogicalKeyboardKey.enter,
+          isMetaPressed: true,
+        );
+      }
+
+      for (final node in checkboxTextNodes) {
+        expect(node.attributes[BuiltInAttributeKey.checkbox], true);
+      }
+    });
+  });
+}