feat: enable toggling and canceling of the formatting with shortcuts (#528)

This commit is contained in:
Lucas.Xu 2023-10-11 13:10:37 +08:00 • committed by GitHub
commit b17bddceb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 153 additions and 16 deletions

View file

@ -195,6 +195,7 @@ extension TextTransaction on Transaction {
int index, int index,
String text, { String text, {
Attributes? attributes, Attributes? attributes,
Attributes? toggledAttributes,
}) { }) {
final delta = node.delta; final delta = node.delta;
if (delta == null) { if (delta == null) {
@ -207,7 +208,11 @@ extension TextTransaction on Transaction {
'The index($index) is out of range or negative.', '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() final insert = Delta()
..retain(index) ..retain(index)
@ -316,6 +321,7 @@ extension TextTransaction on Transaction {
return; return;
} }
afterSelection = beforeSelection; afterSelection = beforeSelection;
final format = Delta() final format = Delta()
..retain(index) ..retain(index)
..retain(length, attributes: attributes); ..retain(length, attributes: attributes);

View file

@ -19,4 +19,13 @@ class AppFlowyRichTextKeys {
textColor, textColor,
highlightColor, highlightColor,
]; ];
// The values supported toggled even if the selection is collapsed.
static List<String> supportToggled = [
bold,
italic,
underline,
strikethrough,
code,
];
} }

View file

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
extension TextTransforms on EditorState { extension TextTransforms on EditorState {
@ -185,18 +187,37 @@ extension TextTransforms on EditorState {
if (selection == null) { if (selection == null) {
return; return;
} }
final nodes = getNodesInSelection(selection); final nodes = getNodesInSelection(selection);
final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { if (selection.isCollapsed) {
return delta.everyAttributes( // get the attributes from the previous one character.
(attributes) => attributes[key] == true, selection = selection.copyWith(
start: selection.start.copyWith(
offset: max(
selection.startIndex - 1,
0,
),
),
); );
}); final toggled = nodes.allSatisfyInSelection(selection, (delta) {
await formatDelta( return delta.everyAttributes(
selection, (attributes) => attributes[key] == true,
{ );
key: !isHighlight, });
}, 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. /// format the node at the given selection.

View file

@ -1,5 +1,6 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/ime/character_shortcut_event_helper.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'; import 'package:flutter/services.dart';
Future<void> onInsert( Future<void> onInsert(
@ -42,11 +43,21 @@ Future<void> onInsert(
} }
assert(node.delta != null); 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 final transaction = editorState.transaction
..insertText( ..insertText(
node, node,
selection.startIndex, selection.startIndex,
insertion.textInserted, insertion.textInserted,
toggledAttributes: editorState.toggledStyle,
); );
return editorState.apply(transaction); return editorState.apply(transaction);
} }

View file

@ -24,35 +24,50 @@ final CommandShortcutEvent toggleBoldCommand = CommandShortcutEvent(
key: 'toggle bold', key: 'toggle bold',
command: 'ctrl+b', command: 'ctrl+b',
macOSCommand: 'cmd+b', macOSCommand: 'cmd+b',
handler: (editorState) => _toggleAttribute(editorState, 'bold'), handler: (editorState) => _toggleAttribute(
editorState,
AppFlowyRichTextKeys.bold,
),
); );
final CommandShortcutEvent toggleItalicCommand = CommandShortcutEvent( final CommandShortcutEvent toggleItalicCommand = CommandShortcutEvent(
key: 'toggle italic', key: 'toggle italic',
command: 'ctrl+i', command: 'ctrl+i',
macOSCommand: 'cmd+i', macOSCommand: 'cmd+i',
handler: (editorState) => _toggleAttribute(editorState, 'italic'), handler: (editorState) => _toggleAttribute(
editorState,
AppFlowyRichTextKeys.italic,
),
); );
final CommandShortcutEvent toggleUnderlineCommand = CommandShortcutEvent( final CommandShortcutEvent toggleUnderlineCommand = CommandShortcutEvent(
key: 'toggle underline', key: 'toggle underline',
command: 'ctrl+u', command: 'ctrl+u',
macOSCommand: 'cmd+u', macOSCommand: 'cmd+u',
handler: (editorState) => _toggleAttribute(editorState, 'underline'), handler: (editorState) => _toggleAttribute(
editorState,
AppFlowyRichTextKeys.underline,
),
); );
final CommandShortcutEvent toggleStrikethroughCommand = CommandShortcutEvent( final CommandShortcutEvent toggleStrikethroughCommand = CommandShortcutEvent(
key: 'toggle strikethrough', key: 'toggle strikethrough',
command: 'ctrl+shift+s', command: 'ctrl+shift+s',
macOSCommand: 'cmd+shift+s', macOSCommand: 'cmd+shift+s',
handler: (editorState) => _toggleAttribute(editorState, 'strikethrough'), handler: (editorState) => _toggleAttribute(
editorState,
AppFlowyRichTextKeys.strikethrough,
),
); );
final CommandShortcutEvent toggleCodeCommand = CommandShortcutEvent( final CommandShortcutEvent toggleCodeCommand = CommandShortcutEvent(
key: 'toggle code', key: 'toggle code',
command: 'ctrl+e', command: 'ctrl+e',
macOSCommand: 'cmd+e', macOSCommand: 'cmd+e',
handler: (editorState) => _toggleAttribute(editorState, 'code'), handler: (editorState) => _toggleAttribute(
editorState,
AppFlowyRichTextKeys.code,
),
); );
KeyEventResult _toggleAttribute( KeyEventResult _toggleAttribute(

View file

@ -100,6 +100,9 @@ class EditorState {
/// Sets the selection of the editor. /// Sets the selection of the editor.
set selection(Selection? value) { set selection(Selection? value) {
// clear the toggled style when the selection is changed.
toggledStyle.clear();
selectionNotifier.value = value; selectionNotifier.value = value;
} }
@ -145,6 +148,13 @@ class EditorState {
sync: true, 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 = <String, bool>{};
final UndoManager undoManager = UndoManager(); final UndoManager undoManager = UndoManager();
Transaction get transaction { Transaction get transaction {

View file

@ -1,6 +1,10 @@
import 'dart:io';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../infra/testable_editor.dart';
import '../util/util.dart'; import '../util/util.dart';
void main() async { void main() async {
@ -313,4 +317,65 @@ void main() async {
expect(texts, ['come', 'To', 'App']); 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();
});
});
} }