feat: add command shortcut keys to convert text to title (#921)

* feat: add command shortcut keys to convert text to title

* fix: use const values from heading block keys

* chore: add test cases for command

* fix: comments and nomenclature

* chore: add appropriate description
This commit is contained in:
Ahad Patel 2024-12-11 14:37:18 +05:30 • committed by GitHub
commit 8d3f5b904b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 299 additions and 1 deletions

View file

@ -178,5 +178,9 @@
"cmdToggleStrikethrough": "toggle strikethrough", "cmdToggleStrikethrough": "toggle strikethrough",
"cmdToggleTodoList": "toggle the todo list", "cmdToggleTodoList": "toggle the todo list",
"cmdToggleUnderline": "toggle underline", "cmdToggleUnderline": "toggle underline",
"cmdUndo": "undo" "cmdUndo": "undo",
"cmdToggleH1": "toggle H1",
"cmdToggleH2": "toggle H2",
"cmdToggleH3": "toggle H3",
"cmdToggleBody": "toggle body"
} }

View file

@ -0,0 +1,95 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
final List<CommandShortcutEvent> toggleHeadingCommands = [
toggleH1,
toggleH2,
toggleH3,
toggleBody,
];
/// Markdown key event.
///
/// Cmd / Ctrl + Shift + T: toggle H1
/// Cmd / Ctrl + Shift + G: toggle H2
/// Cmd / Ctrl + Shift + J: toggle H3
/// Cmd / Ctrl + Shift + B: toggle Body
/// - support
/// - desktop
/// - web
///
final CommandShortcutEvent toggleH1 = CommandShortcutEvent(
key: 'toggle H1',
getDescription: () => AppFlowyEditorL10n.current.cmdToggleH1,
command: 'ctrl+shift+t',
macOSCommand: 'cmd+shift+t',
handler: (editorState) => _toggleAttribute(
editorState,
1,
),
);
final CommandShortcutEvent toggleH2 = CommandShortcutEvent(
key: 'toggle H2',
getDescription: () => AppFlowyEditorL10n.current.cmdToggleH2,
command: 'ctrl+shift+g',
macOSCommand: 'cmd+shift+g',
handler: (editorState) => _toggleAttribute(
editorState,
2,
),
);
final CommandShortcutEvent toggleH3 = CommandShortcutEvent(
key: 'toggle H3',
getDescription: () => AppFlowyEditorL10n.current.cmdToggleH3,
command: 'ctrl+shift+j',
macOSCommand: 'cmd+shift+j',
handler: (editorState) => _toggleAttribute(
editorState,
3,
),
);
final CommandShortcutEvent toggleBody = CommandShortcutEvent(
key: 'toggle Body',
getDescription: () => AppFlowyEditorL10n.current.cmdToggleBody,
command: 'ctrl+shift+b',
macOSCommand: 'cmd+shift+b',
handler: (editorState) => _toggleAttribute(editorState, 1, true),
);
KeyEventResult _toggleAttribute(
EditorState editorState,
int? level, [
bool? isBody,
]) {
final selection = editorState.selection;
if (selection == null) {
return KeyEventResult.ignored;
}
final node = editorState.getNodeAtPath(selection.start.path)!;
bool isHeading = isBody ??
node.type == HeadingBlockKeys.type &&
node.attributes[HeadingBlockKeys.level] == level;
final delta = (node.delta ?? Delta()).toJson();
editorState.formatNode(
selection,
(node) => node.copyWith(
type: isHeading ? ParagraphBlockKeys.type : HeadingBlockKeys.type,
attributes: {
HeadingBlockKeys.level: level,
blockComponentBackgroundColor:
node.attributes[blockComponentBackgroundColor],
blockComponentTextDirection:
node.attributes[blockComponentTextDirection],
blockComponentDelta: delta,
},
),
);
return KeyEventResult.handled;
}

View file

@ -1,4 +1,5 @@
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/block_component/heading_block_component/heading_command_shortcut.dart';
import 'package:appflowy_editor/src/editor/util/platform_extension.dart'; import 'package:appflowy_editor/src/editor/util/platform_extension.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -129,6 +130,7 @@ final List<CommandShortcutEvent> standardCommandShortcutEvents = [
// //
toggleTodoListCommand, toggleTodoListCommand,
...toggleMarkdownCommands, ...toggleMarkdownCommands,
...toggleHeadingCommands,
toggleHighlightCommand, toggleHighlightCommand,
showLinkMenuCommand, showLinkMenuCommand,
openInlineLinkCommand, openInlineLinkCommand,

View file

@ -156,8 +156,12 @@ class MessageLookup extends MessageLookupByLibrary {
"cmdTableStopAtTheBeginningOfTheCell": "cmdTableStopAtTheBeginningOfTheCell":
MessageLookupByLibrary.simpleMessage( MessageLookupByLibrary.simpleMessage(
"Stop at the beginning of the cell"), "Stop at the beginning of the cell"),
"cmdToggleBody": MessageLookupByLibrary.simpleMessage("toggle body"),
"cmdToggleBold": MessageLookupByLibrary.simpleMessage("toggle bold"), "cmdToggleBold": MessageLookupByLibrary.simpleMessage("toggle bold"),
"cmdToggleCode": MessageLookupByLibrary.simpleMessage("toggle code"), "cmdToggleCode": MessageLookupByLibrary.simpleMessage("toggle code"),
"cmdToggleH1": MessageLookupByLibrary.simpleMessage("toggle H1"),
"cmdToggleH2": MessageLookupByLibrary.simpleMessage("toggle H2"),
"cmdToggleH3": MessageLookupByLibrary.simpleMessage("toggle H3"),
"cmdToggleHighlight": "cmdToggleHighlight":
MessageLookupByLibrary.simpleMessage("toggle highlight"), MessageLookupByLibrary.simpleMessage("toggle highlight"),
"cmdToggleItalic": "cmdToggleItalic":

View file

@ -1840,6 +1840,46 @@ class AppFlowyEditorLocalizations {
args: [], args: [],
); );
} }
/// `toggle H1`
String get cmdToggleH1 {
return Intl.message(
'toggle H1',
name: 'cmdToggleH1',
desc: '',
args: [],
);
}
/// `toggle H2`
String get cmdToggleH2 {
return Intl.message(
'toggle H2',
name: 'cmdToggleH2',
desc: '',
args: [],
);
}
/// `toggle H3`
String get cmdToggleH3 {
return Intl.message(
'toggle H3',
name: 'cmdToggleH3',
desc: '',
args: [],
);
}
/// `toggle body`
String get cmdToggleBody {
return Intl.message(
'toggle body',
name: 'cmdToggleBody',
desc: '',
args: [],
);
}
} }
class AppLocalizationDelegate class AppLocalizationDelegate

View file

@ -0,0 +1,153 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/block_component/heading_block_component/heading_command_shortcut.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../util/util.dart';
void main() async {
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
group('heading_command_shortcut_test.dart', () {
const text = 'Welcome to AppFlowy Editor 🔥!';
test('toggle H1 from paragraph', () {
final document = Document.blank()
..addParagraph(
initialText: text,
);
final editorState = EditorState(document: document);
Node node = editorState.getNodeAtPath([0])!;
expect(node.type, ParagraphBlockKeys.type);
final selection = Selection.collapsed(
Position(
path: [0],
offset: 1,
),
);
editorState.selection = selection;
toggleH1.execute(editorState);
// Before
// Welcome to AppFlowy Editor 🔥!
// After
// <H1>Welcome to AppFlowy Editor 🔥! <H1>
node = editorState.getNodeAtPath([0])!;
expect(node.type, HeadingBlockKeys.type);
expect(node.level, 1);
toggleH1.execute(editorState);
// Before
// <H1>Welcome to AppFlowy Editor 🔥! <H1>
// After
// Welcome to AppFlowy Editor 🔥!
expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type);
});
test('toggle H2 from paragraph', () {
final document = Document.blank()
..addParagraph(
initialText: text,
);
final editorState = EditorState(document: document);
Node node = editorState.getNodeAtPath([0])!;
expect(node.type, ParagraphBlockKeys.type);
final selection = Selection.collapsed(
Position(
path: [0],
offset: 1,
),
);
editorState.selection = selection;
toggleH2.execute(editorState);
// Before
// Welcome to AppFlowy Editor 🔥!
// After
// <H2>Welcome to AppFlowy Editor 🔥! <H2>
node = editorState.getNodeAtPath([0])!;
expect(node.type, HeadingBlockKeys.type);
expect(node.attributes[HeadingBlockKeys.level], 2);
toggleH2.execute(editorState);
// Before
// <H2>Welcome to AppFlowy Editor 🔥! <H2>
// After
// Welcome to AppFlowy Editor 🔥!
expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type);
});
test('toggle H3 from paragraph', () {
final document = Document.blank()
..addParagraph(
initialText: text,
);
final editorState = EditorState(document: document);
Node node = editorState.getNodeAtPath([0])!;
expect(node.type, ParagraphBlockKeys.type);
final selection = Selection.collapsed(
Position(
path: [0],
offset: 1,
),
);
editorState.selection = selection;
toggleH3.execute(editorState);
// Before
// Welcome to AppFlowy Editor 🔥!
// After
// <H3>Welcome to AppFlowy Editor 🔥! <H3>
node = editorState.getNodeAtPath([0])!;
expect(node.type, HeadingBlockKeys.type);
expect(node.attributes[HeadingBlockKeys.level], 3);
toggleH3.execute(editorState);
// Before
// <H3>Welcome to AppFlowy Editor 🔥! <H3>
// After
// Welcome to AppFlowy Editor 🔥!
expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type);
});
test('toggle body type from heading type', () {
final document = Document.blank()
..insert([0], [headingNode(level: 2, text: text)]);
final editorState = EditorState(document: document);
Node node = editorState.getNodeAtPath([0])!;
expect(node.type, HeadingBlockKeys.type);
expect(node.attributes[HeadingBlockKeys.level], 2);
final selection = Selection.collapsed(
Position(
path: [0],
offset: 1,
),
);
editorState.selection = selection;
toggleBody.execute(editorState);
// Before
// <H2>Welcome to AppFlowy Editor 🔥! <H2>
// After
// Welcome to AppFlowy Editor 🔥!
node = editorState.getNodeAtPath([0])!;
expect(node.type, ParagraphBlockKeys.type);
});
});
}