diff --git a/lib/src/core/transform/transaction.dart b/lib/src/core/transform/transaction.dart index fcd38bc14..23bd70642 100644 --- a/lib/src/core/transform/transaction.dart +++ b/lib/src/core/transform/transaction.dart @@ -195,6 +195,7 @@ extension TextTransaction on Transaction { int index, String text, { Attributes? attributes, + Attributes? toggledAttributes, }) { final delta = node.delta; if (delta == null) { @@ -207,7 +208,11 @@ extension TextTransaction on Transaction { 'The index($index) is out of range or negative.', ); - final newAttributes = attributes ?? delta.sliceAttributes(index); + final newAttributes = attributes ?? delta.sliceAttributes(index) ?? {}; + + if (toggledAttributes != null) { + newAttributes.addAll(toggledAttributes); + } final insert = Delta() ..retain(index) @@ -316,6 +321,7 @@ extension TextTransaction on Transaction { return; } afterSelection = beforeSelection; + final format = Delta() ..retain(index) ..retain(length, attributes: attributes); diff --git a/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart b/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart index 698b84be3..bda584a88 100644 --- a/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart +++ b/lib/src/editor/block_component/rich_text/appflowy_rich_text_keys.dart @@ -19,4 +19,13 @@ class AppFlowyRichTextKeys { textColor, highlightColor, ]; + + // The values supported toggled even if the selection is collapsed. + static List supportToggled = [ + bold, + italic, + underline, + strikethrough, + code, + ]; } diff --git a/lib/src/editor/command/text_commands.dart b/lib/src/editor/command/text_commands.dart index ea5637182..a0cb434f7 100644 --- a/lib/src/editor/command/text_commands.dart +++ b/lib/src/editor/command/text_commands.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:appflowy_editor/appflowy_editor.dart'; extension TextTransforms on EditorState { @@ -185,18 +187,37 @@ extension TextTransforms on EditorState { if (selection == null) { return; } + final nodes = getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[key] == true, + if (selection.isCollapsed) { + // get the attributes from the previous one character. + selection = selection.copyWith( + start: selection.start.copyWith( + offset: max( + selection.startIndex - 1, + 0, + ), + ), ); - }); - await formatDelta( - selection, - { - key: !isHighlight, - }, - ); + final toggled = nodes.allSatisfyInSelection(selection, (delta) { + return delta.everyAttributes( + (attributes) => attributes[key] == true, + ); + }); + toggledStyle[key] = !toggled; + } else { + final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { + return delta.everyAttributes( + (attributes) => attributes[key] == true, + ); + }); + await formatDelta( + selection, + { + key: !isHighlight, + }, + ); + } } /// format the node at the given selection. diff --git a/lib/src/editor/editor_component/service/ime/delta_input_on_insert_impl.dart b/lib/src/editor/editor_component/service/ime/delta_input_on_insert_impl.dart index 7ad230e5d..4d6d88f69 100644 --- a/lib/src/editor/editor_component/service/ime/delta_input_on_insert_impl.dart +++ b/lib/src/editor/editor_component/service/ime/delta_input_on_insert_impl.dart @@ -1,5 +1,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/src/editor/editor_component/service/ime/character_shortcut_event_helper.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; Future onInsert( @@ -42,11 +43,21 @@ Future onInsert( } assert(node.delta != null); + if (kDebugMode) { + // verify the toggled keys are supported. + assert( + editorState.toggledStyle.keys.every( + (element) => AppFlowyRichTextKeys.supportToggled.contains(element), + ), + ); + } + final transaction = editorState.transaction ..insertText( node, selection.startIndex, insertion.textInserted, + toggledAttributes: editorState.toggledStyle, ); return editorState.apply(transaction); } diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/markdown_commands.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/markdown_commands.dart index 1e39c2655..336a023a8 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/markdown_commands.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/markdown_commands.dart @@ -24,35 +24,50 @@ final CommandShortcutEvent toggleBoldCommand = CommandShortcutEvent( key: 'toggle bold', command: 'ctrl+b', macOSCommand: 'cmd+b', - handler: (editorState) => _toggleAttribute(editorState, 'bold'), + handler: (editorState) => _toggleAttribute( + editorState, + AppFlowyRichTextKeys.bold, + ), ); final CommandShortcutEvent toggleItalicCommand = CommandShortcutEvent( key: 'toggle italic', command: 'ctrl+i', macOSCommand: 'cmd+i', - handler: (editorState) => _toggleAttribute(editorState, 'italic'), + handler: (editorState) => _toggleAttribute( + editorState, + AppFlowyRichTextKeys.italic, + ), ); final CommandShortcutEvent toggleUnderlineCommand = CommandShortcutEvent( key: 'toggle underline', command: 'ctrl+u', macOSCommand: 'cmd+u', - handler: (editorState) => _toggleAttribute(editorState, 'underline'), + handler: (editorState) => _toggleAttribute( + editorState, + AppFlowyRichTextKeys.underline, + ), ); final CommandShortcutEvent toggleStrikethroughCommand = CommandShortcutEvent( key: 'toggle strikethrough', command: 'ctrl+shift+s', macOSCommand: 'cmd+shift+s', - handler: (editorState) => _toggleAttribute(editorState, 'strikethrough'), + handler: (editorState) => _toggleAttribute( + editorState, + AppFlowyRichTextKeys.strikethrough, + ), ); final CommandShortcutEvent toggleCodeCommand = CommandShortcutEvent( key: 'toggle code', command: 'ctrl+e', macOSCommand: 'cmd+e', - handler: (editorState) => _toggleAttribute(editorState, 'code'), + handler: (editorState) => _toggleAttribute( + editorState, + AppFlowyRichTextKeys.code, + ), ); KeyEventResult _toggleAttribute( diff --git a/lib/src/editor_state.dart b/lib/src/editor_state.dart index 33da0ee56..5b2034a27 100644 --- a/lib/src/editor_state.dart +++ b/lib/src/editor_state.dart @@ -100,6 +100,9 @@ class EditorState { /// Sets the selection of the editor. set selection(Selection? value) { + // clear the toggled style when the selection is changed. + toggledStyle.clear(); + selectionNotifier.value = value; } @@ -145,6 +148,13 @@ class EditorState { sync: true, ); + /// Store the toggled format style, like bold, italic, etc. + /// All the values must be the key from [AppFlowyRichTextKeys.supportToggled]. + /// + /// NOTES: It only works once; + /// after the selection is changed, the toggled style will be cleared. + final toggledStyle = {}; + final UndoManager undoManager = UndoManager(); Transaction get transaction { diff --git a/test/new/command/text_commands_test.dart b/test/new/command/text_commands_test.dart index 61f64b40c..ed032a9d5 100644 --- a/test/new/command/text_commands_test.dart +++ b/test/new/command/text_commands_test.dart @@ -1,6 +1,10 @@ +import 'dart:io'; + import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../infra/testable_editor.dart'; import '../util/util.dart'; void main() async { @@ -313,4 +317,65 @@ void main() async { expect(texts, ['come', 'To', 'App']); }); }); + + group('toggle style', () { + testWidgets('toggle the style if the previous character isn\'t formatted', + (tester) async { + const text = ''; + final editor = tester.editor..addParagraph(initialText: text); + + await editor.startTesting(); + await editor.updateSelection( + Selection.single(path: [0], startOffset: text.length), + ); + + // toggle bold, italic, underline + final keys = [ + LogicalKeyboardKey.keyB, + LogicalKeyboardKey.keyI, + LogicalKeyboardKey.keyU, + ]; + for (final key in keys) { + await editor.pressKey( + key: key, + isControlPressed: !Platform.isMacOS, + isMetaPressed: Platform.isMacOS, + ); + } + + await editor.ime.insertText('Hello'); + final delta1 = editor.nodeAtPath([0])!.delta!; + expect(delta1.toJson(), [ + { + "insert": "Hello", + "attributes": {"bold": true, "italic": true, "underline": true} + } + ]); + + // cancel the toggled style + for (final key in keys) { + await editor.pressKey( + key: key, + isControlPressed: !Platform.isMacOS, + isMetaPressed: Platform.isMacOS, + ); + } + + await editor.ime.insertText('World'); + final delta2 = editor.nodeAtPath([0])!.delta!; + expect(delta2.toJson(), [ + { + "insert": "Hello", + "attributes": {"bold": true, "italic": true, "underline": true} + }, + { + "insert": "World", + "attributes": {"bold": false, "italic": false, "underline": false} + }, + ]); + + expect(editor.editorState.toggledStyle, isEmpty); + await editor.dispose(); + }); + }); }