Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion lib/src/core/transform/transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ extension TextTransaction on Transaction {
int index,
String text, {
Attributes? attributes,
Attributes? toggledAttributes,
}) {
final delta = node.delta;
if (delta == null) {
Expand All @@ -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)
Expand Down Expand Up @@ -316,6 +321,7 @@ extension TextTransaction on Transaction {
return;
}
afterSelection = beforeSelection;

final format = Delta()
..retain(index)
..retain(length, attributes: attributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,13 @@ class AppFlowyRichTextKeys {
textColor,
highlightColor,
];

// The values supported toggled even if the selection is collapsed.
static List<String> supportToggled = [
bold,
italic,
underline,
strikethrough,
code,
];
}
41 changes: 31 additions & 10 deletions lib/src/editor/command/text_commands.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:math';

import 'package:appflowy_editor/appflowy_editor.dart';

extension TextTransforms on EditorState {
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> onInsert(
Expand Down Expand Up @@ -42,11 +43,21 @@ Future<void> 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions lib/src/editor_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 = <String, bool>{};

final UndoManager undoManager = UndoManager();

Transaction get transaction {
Expand Down
65 changes: 65 additions & 0 deletions test/new/command/text_commands_test.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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();
});
});
}