feat: enable toggling and canceling of the formatting with shortcuts (#528)
This commit is contained in:
parent
8a04d99529
commit
b17bddceb8
7 changed files with 153 additions and 16 deletions
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue