diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/checkbox_event_handler.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/checkbox_event_handler.dart new file mode 100644 index 0000000000..55b449c492 --- /dev/null +++ b/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; +}; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart index ad05a636d2..021093de5d 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart +++ b/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 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', diff --git a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart b/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart index 5102407e1b..d00e458e5c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart +++ b/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; } diff --git a/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/checkbox_event_handler_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/checkbox_event_handler_test.dart new file mode 100644 index 0000000000..e1fd1f6ccc --- /dev/null +++ b/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); + } + }); + }); +}