chore: rename shortcut folder (#569)
* chore: rename shortcut folder * chore: bump version 1.5.1
This commit is contained in:
parent
9c32a7fd16
commit
b8368d82a5
43 changed files with 24 additions and 16 deletions
|
|
@ -1,3 +1,11 @@
|
||||||
|
## 1.5.1
|
||||||
|
* feat: toggledStyle notifier by @Xazin in ([#562](https://github.com/AppFlowy-IO/appflowy-editor/pull/562))
|
||||||
|
* feat: power checkbox shortcut by @Xazin in ([#553](https://github.com/AppFlowy-IO/appflowy-editor/pull/553))
|
||||||
|
* * fix: Applied Corner Radius to Block Selection Area by @0xJaskeerat in ([#557](https://github.com/AppFlowy-IO/appflowy-editor/pull/557))
|
||||||
|
* fix: do not select all if selection is null by @Xazin in ([#563](https://github.com/AppFlowy-IO/appflowy-editor/pull/563))
|
||||||
|
* fix: on ui event reset selection type by @Xazin in ([#55](https://github.com/AppFlowy-IO/appflowy-editor/pull/555))
|
||||||
|
* fix: image copy error width/height is double by @q200892907 in ([#566](https://github.com/AppFlowy-IO/appflowy-editor/pull/566))
|
||||||
|
|
||||||
## 1.5.0
|
## 1.5.0
|
||||||
* feat: enable toggling and canceling of formatting with shortcuts by @LucasXu0 in ([#528](https://github.com/AppFlowy-IO/appflowy-editor/pull/528))
|
* feat: enable toggling and canceling of formatting with shortcuts by @LucasXu0 in ([#528](https://github.com/AppFlowy-IO/appflowy-editor/pull/528))
|
||||||
* feat: disable the toolbar in the code block by @LucasXu0 in ([#534](https://github.com/AppFlowy-IO/appflowy-editor/pull/534))
|
* feat: disable the toolbar in the code block by @LucasXu0 in ([#534](https://github.com/AppFlowy-IO/appflowy-editor/pull/534))
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,9 @@ final List<CharacterShortcutEvent> standardCharacterShortcutEvents = [
|
||||||
|
|
||||||
// markdown syntax
|
// markdown syntax
|
||||||
...markdownSyntaxShortcutEvents,
|
...markdownSyntaxShortcutEvents,
|
||||||
|
|
||||||
|
// convert => to arrow
|
||||||
|
formatGreaterEqual,
|
||||||
];
|
];
|
||||||
|
|
||||||
final List<CommandShortcutEvent> standardCommandShortcutEvents = [
|
final List<CommandShortcutEvent> standardCommandShortcutEvents = [
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
export 'shortcuts/character_shortcut_events/character_shortcut_events.dart';
|
export 'shortcuts/character/character_shortcut_events.dart';
|
||||||
export 'shortcuts/command_shortcut_events/command_shortcut_events.dart';
|
|
||||||
|
|
||||||
export 'shortcuts/character_shortcut_event.dart';
|
export 'shortcuts/character_shortcut_event.dart';
|
||||||
|
export 'shortcuts/command/command_shortcut_events.dart';
|
||||||
export 'shortcuts/command_shortcut_event.dart';
|
export 'shortcuts/command_shortcut_event.dart';
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
export 'double/format_bold.dart';
|
||||||
|
export 'double/format_double_characters.dart';
|
||||||
|
export 'double/format_double_hyphen.dart';
|
||||||
|
export 'double/format_arrow_character.dart';
|
||||||
|
export 'double/format_strikethrough.dart';
|
||||||
|
export 'format_single_character/format_code.dart';
|
||||||
|
export 'format_single_character/format_italic.dart';
|
||||||
|
export 'format_single_character/format_single_character.dart';
|
||||||
|
export 'format_single_character/format_strikethrough.dart';
|
||||||
|
export 'insert_newline.dart';
|
||||||
|
export 'markdown_syntax_character_shortcut_events.dart';
|
||||||
|
export 'slash_command.dart';
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
const _greater = '>';
|
||||||
|
const _equals = '=';
|
||||||
|
const _arrow = '⇒';
|
||||||
|
|
||||||
|
/// format '=' + '>' into an ⇒
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
final CharacterShortcutEvent formatGreaterEqual = CharacterShortcutEvent(
|
||||||
|
key: 'format = + > into ⇒',
|
||||||
|
character: _greater,
|
||||||
|
handler: (editorState) async => handleEqualGreaterReplacement(
|
||||||
|
editorState: editorState,
|
||||||
|
character: _greater,
|
||||||
|
replacement: _arrow,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO(Xazin): Combine two character replacement methods into
|
||||||
|
// a helper function
|
||||||
|
Future<bool> handleEqualGreaterReplacement({
|
||||||
|
required EditorState editorState,
|
||||||
|
required String character,
|
||||||
|
required String replacement,
|
||||||
|
}) async {
|
||||||
|
assert(character.length == 1);
|
||||||
|
|
||||||
|
Selection? selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selection.isCollapsed) {
|
||||||
|
await editorState.deleteSelection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = editorState.getNodeAtPath(selection.end.path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
if (node == null || delta == null || delta.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.end.offset > 0) {
|
||||||
|
final plain = delta.toPlainText();
|
||||||
|
|
||||||
|
final previousCharacter = plain[selection.end.offset - 1];
|
||||||
|
if (previousCharacter != _equals) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final replace = editorState.transaction
|
||||||
|
..replaceText(
|
||||||
|
node,
|
||||||
|
selection.end.offset - 1,
|
||||||
|
1,
|
||||||
|
_arrow,
|
||||||
|
);
|
||||||
|
|
||||||
|
await editorState.apply(replace);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character/double/format_double_characters.dart';
|
||||||
|
|
||||||
|
const _asterisk = '*';
|
||||||
|
const _underscore = '_';
|
||||||
|
|
||||||
|
/// format the text surrounded by double asterisks to bold
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
final CharacterShortcutEvent formatDoubleAsterisksToBold =
|
||||||
|
CharacterShortcutEvent(
|
||||||
|
key: 'format the text surrounded by double asterisks to bold',
|
||||||
|
character: _asterisk,
|
||||||
|
handler: (editorState) async => handleFormatByWrappingWithDoubleCharacter(
|
||||||
|
editorState: editorState,
|
||||||
|
character: _asterisk,
|
||||||
|
formatStyle: DoubleCharacterFormatStyle.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// format the text surrounded by double underscores to bold
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
final CharacterShortcutEvent formatDoubleUnderscoresToBold =
|
||||||
|
CharacterShortcutEvent(
|
||||||
|
key: 'format the text surrounded by double underscores to bold',
|
||||||
|
character: _underscore,
|
||||||
|
handler: (editorState) async => handleFormatByWrappingWithDoubleCharacter(
|
||||||
|
editorState: editorState,
|
||||||
|
character: _underscore,
|
||||||
|
formatStyle: DoubleCharacterFormatStyle.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
// We currently have only one format style is triggered by double characters.
|
||||||
|
// **abc** or __abc__ -> bold abc
|
||||||
|
// If we have more in the future, we should add them in this enum and update the [style] variable in [handleDoubleCharactersFormat].
|
||||||
|
enum DoubleCharacterFormatStyle {
|
||||||
|
bold,
|
||||||
|
strikethrough,
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handleFormatByWrappingWithDoubleCharacter({
|
||||||
|
// for demonstration purpose, the following comments use * to represent the character from the parameter [char].
|
||||||
|
required EditorState editorState,
|
||||||
|
required String character,
|
||||||
|
required DoubleCharacterFormatStyle formatStyle,
|
||||||
|
}) {
|
||||||
|
assert(character.length == 1);
|
||||||
|
final selection = editorState.selection;
|
||||||
|
// if the selection is not collapsed or the cursor is at the first three index range, we don't need to format it.
|
||||||
|
// we should return false to let the IME handle it.
|
||||||
|
if (selection == null || !selection.isCollapsed || selection.end.offset < 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final path = selection.end.path;
|
||||||
|
final node = editorState.getNodeAtPath(path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
// if the node doesn't contain the delta(which means it isn't a text),
|
||||||
|
// we don't need to format it.
|
||||||
|
if (node == null || delta == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final plainText = delta.toPlainText();
|
||||||
|
|
||||||
|
// The plainText should have at least 4 characters,like **a*.
|
||||||
|
// The last char in the plainText should be *[char]. Otherwise, we don't need to format it.
|
||||||
|
if (plainText.length < 4 ||
|
||||||
|
plainText[selection.end.offset - 1] != character) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find all the index of *[char]
|
||||||
|
final charIndexList = <int>[];
|
||||||
|
for (var i = 0; i < plainText.length; i++) {
|
||||||
|
if (plainText[i] == character) {
|
||||||
|
charIndexList.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charIndexList.length < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// for example: **abc* -> [0, 1, 5]
|
||||||
|
// thirdLastCharIndex = 0, secondLastCharIndex = 1, lastCharIndex = 5
|
||||||
|
// make sure the third *[char] and second *[char] are connected
|
||||||
|
// make sure the second *[char] and last *[char] are split by at least one character
|
||||||
|
final thirdLastCharIndex = charIndexList[charIndexList.length - 3];
|
||||||
|
final secondLastCharIndex = charIndexList[charIndexList.length - 2];
|
||||||
|
final lastCharIndex = charIndexList[charIndexList.length - 1];
|
||||||
|
if (secondLastCharIndex != thirdLastCharIndex + 1 ||
|
||||||
|
lastCharIndex == secondLastCharIndex + 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all the conditions are met, we should format the text.
|
||||||
|
// 1. delete all the *[char]
|
||||||
|
// 2. update the style of the text surrounded by the double *[char] to [formatStyle]
|
||||||
|
// 3. update the cursor position.
|
||||||
|
final deletion = editorState.transaction
|
||||||
|
..deleteText(node, lastCharIndex, 1)
|
||||||
|
..deleteText(node, thirdLastCharIndex, 2);
|
||||||
|
editorState.apply(deletion);
|
||||||
|
|
||||||
|
// To minimize errors, retrieve the format style from an enum that is specific to double characters.
|
||||||
|
final String style;
|
||||||
|
|
||||||
|
switch (formatStyle) {
|
||||||
|
case DoubleCharacterFormatStyle.bold:
|
||||||
|
style = 'bold';
|
||||||
|
break;
|
||||||
|
case DoubleCharacterFormatStyle.strikethrough:
|
||||||
|
style = 'strikethrough';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
style = '';
|
||||||
|
assert(false, 'Invalid format style');
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the text is already formatted, we should remove the format.
|
||||||
|
final sliced = delta.slice(
|
||||||
|
thirdLastCharIndex + 2,
|
||||||
|
selection.end.offset - 1,
|
||||||
|
);
|
||||||
|
final result = sliced.everyAttributes((element) => element[style] == true);
|
||||||
|
|
||||||
|
final format = editorState.transaction
|
||||||
|
..formatText(
|
||||||
|
node,
|
||||||
|
thirdLastCharIndex,
|
||||||
|
selection.end.offset - thirdLastCharIndex - 3,
|
||||||
|
{
|
||||||
|
style: !result,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: path,
|
||||||
|
offset: selection.end.offset - 3,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
editorState.apply(format);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
const _hyphen = '-';
|
||||||
|
const _emDash = '—'; // This is an em dash — not a single dash - !!
|
||||||
|
|
||||||
|
/// format two hyphens into an em dash
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
final CharacterShortcutEvent formatDoubleHyphenEmDash = CharacterShortcutEvent(
|
||||||
|
key: 'format double hyphen into an em dash',
|
||||||
|
character: _hyphen,
|
||||||
|
handler: (editorState) async => handleDoubleCharacterReplacement(
|
||||||
|
editorState: editorState,
|
||||||
|
character: _hyphen,
|
||||||
|
replacement: _emDash,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<bool> handleDoubleCharacterReplacement({
|
||||||
|
required EditorState editorState,
|
||||||
|
required String character,
|
||||||
|
required String replacement,
|
||||||
|
}) async {
|
||||||
|
assert(character.length == 1);
|
||||||
|
|
||||||
|
Selection? selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selection.isCollapsed) {
|
||||||
|
await editorState.deleteSelection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = editorState.getNodeAtPath(selection.end.path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
if (node == null || delta == null || delta.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.end.offset > 0) {
|
||||||
|
final plain = delta.toPlainText();
|
||||||
|
|
||||||
|
final previousCharacter = plain[selection.end.offset - 1];
|
||||||
|
if (previousCharacter != _hyphen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final replace = editorState.transaction
|
||||||
|
..replaceText(
|
||||||
|
node,
|
||||||
|
selection.end.offset - 1,
|
||||||
|
1,
|
||||||
|
_emDash,
|
||||||
|
);
|
||||||
|
|
||||||
|
await editorState.apply(replace);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character/double/format_double_characters.dart';
|
||||||
|
|
||||||
|
const _tile = '~';
|
||||||
|
|
||||||
|
/// format the text surrounded by double asterisks to bold
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
final CharacterShortcutEvent formatDoubleTilesToStrikethrough =
|
||||||
|
CharacterShortcutEvent(
|
||||||
|
key: 'format the text surrounded by double asterisks to bold',
|
||||||
|
character: _tile,
|
||||||
|
handler: (editorState) async => handleFormatByWrappingWithDoubleCharacter(
|
||||||
|
editorState: editorState,
|
||||||
|
character: _tile,
|
||||||
|
formatStyle: DoubleCharacterFormatStyle.strikethrough,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character/format_single_character/format_single_character.dart';
|
||||||
|
|
||||||
|
const _backquote = '`';
|
||||||
|
|
||||||
|
/// format the text surrounded by single backquote to code
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
final CharacterShortcutEvent formatBackquoteToCode = CharacterShortcutEvent(
|
||||||
|
key: 'format the text surrounded by single backquote to code',
|
||||||
|
character: _backquote,
|
||||||
|
handler: (editorState) async => handleFormatByWrappingWithSingleCharacter(
|
||||||
|
editorState: editorState,
|
||||||
|
character: _backquote,
|
||||||
|
formatStyle: FormatStyleByWrappingWithSingleChar.code,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
|
||||||
|
import 'format_single_character.dart';
|
||||||
|
|
||||||
|
const _underscore = '_';
|
||||||
|
const _asterisk = '*';
|
||||||
|
|
||||||
|
/// format the text surrounded by single underscore to italic
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
CharacterShortcutEvent formatUnderscoreToItalic = CharacterShortcutEvent(
|
||||||
|
key: 'format the text surrounded by single underscore to italic',
|
||||||
|
character: _underscore,
|
||||||
|
handler: (editorState) async => handleFormatByWrappingWithSingleCharacter(
|
||||||
|
editorState: editorState,
|
||||||
|
character: _underscore,
|
||||||
|
formatStyle: FormatStyleByWrappingWithSingleChar.italic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// format the text surrounded by single asterisk to italic
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
final CharacterShortcutEvent formatAsteriskToItalic = CharacterShortcutEvent(
|
||||||
|
key: 'format the text surrounded by single asterisk to italic',
|
||||||
|
character: _asterisk,
|
||||||
|
handler: (editorState) async => handleFormatByWrappingWithSingleCharacter(
|
||||||
|
editorState: editorState,
|
||||||
|
character: _asterisk,
|
||||||
|
formatStyle: FormatStyleByWrappingWithSingleChar.italic,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
enum FormatStyleByWrappingWithSingleChar {
|
||||||
|
code,
|
||||||
|
italic,
|
||||||
|
strikethrough,
|
||||||
|
}
|
||||||
|
|
||||||
|
bool handleFormatByWrappingWithSingleCharacter({
|
||||||
|
required EditorState editorState,
|
||||||
|
required String character,
|
||||||
|
required FormatStyleByWrappingWithSingleChar formatStyle,
|
||||||
|
}) {
|
||||||
|
assert(character.length == 1);
|
||||||
|
|
||||||
|
final selection = editorState.selection;
|
||||||
|
// if the selection is not collapsed or the cursor is at the first two index range, we don't need to format it.
|
||||||
|
// we should return false to let the IME handle it.
|
||||||
|
if (selection == null || !selection.isCollapsed || selection.end.offset < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final path = selection.end.path;
|
||||||
|
final node = editorState.getNodeAtPath(path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
// if the node doesn't contain the delta(which means it isn't a text)
|
||||||
|
// we don't need to format it.
|
||||||
|
if (node == null || delta == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final plainText = delta.toPlainText();
|
||||||
|
|
||||||
|
final headCharIndex = plainText.indexOf(character);
|
||||||
|
final endCharIndex = plainText.lastIndexOf(character);
|
||||||
|
|
||||||
|
// Determine if a 'Character' already exists in the node and only once.
|
||||||
|
// 1. This is no 'Character' in the plainText: indexOf returns -1.
|
||||||
|
// 2. More than one 'Character' in the plainText: the headCharIndex and endCharIndex are supposed to be the same, if not, which means plainText has more than one character. For example: when plainText is '_abc', it will trigger formatting(remind:the last char is used to trigger the formatting,so it won't be counted in the plainText.). But adding '_' after 'a__ab' won't trigger formatting.
|
||||||
|
// 3. there are two characters connecting together, like adding '_' after 'abc_' won't trigger formatting.
|
||||||
|
if (headCharIndex == -1 ||
|
||||||
|
headCharIndex != endCharIndex ||
|
||||||
|
headCharIndex == selection.end.offset - 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all the conditions are met, we should format the text to italic.
|
||||||
|
// 1. delete the previous 'Character',
|
||||||
|
// 2. update the style of the text surrounded by the two 'Character's to [formatStyle]
|
||||||
|
// 3. update the cursor position.
|
||||||
|
|
||||||
|
final deletion = editorState.transaction
|
||||||
|
..deleteText(
|
||||||
|
node,
|
||||||
|
headCharIndex,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
editorState.apply(deletion);
|
||||||
|
|
||||||
|
// To minimize errors, retrieve the format style from an enum that is specific to single characters.
|
||||||
|
final String style;
|
||||||
|
|
||||||
|
switch (formatStyle) {
|
||||||
|
case FormatStyleByWrappingWithSingleChar.code:
|
||||||
|
style = 'code';
|
||||||
|
break;
|
||||||
|
case FormatStyleByWrappingWithSingleChar.italic:
|
||||||
|
style = 'italic';
|
||||||
|
break;
|
||||||
|
case FormatStyleByWrappingWithSingleChar.strikethrough:
|
||||||
|
style = 'strikethrough';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
style = '';
|
||||||
|
assert(false, 'Invalid format style');
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the text is already formatted, we should remove the format.
|
||||||
|
final sliced = delta.slice(
|
||||||
|
headCharIndex + 1,
|
||||||
|
selection.end.offset,
|
||||||
|
);
|
||||||
|
final result = sliced.everyAttributes((element) => element[style] == true);
|
||||||
|
|
||||||
|
final format = editorState.transaction
|
||||||
|
..formatText(
|
||||||
|
node,
|
||||||
|
headCharIndex,
|
||||||
|
selection.end.offset - headCharIndex - 1,
|
||||||
|
{
|
||||||
|
style: !result,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: path,
|
||||||
|
offset: selection.end.offset - 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
editorState.apply(format);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character/format_single_character/format_single_character.dart';
|
||||||
|
|
||||||
|
const String _tilde = '~';
|
||||||
|
|
||||||
|
/// format the text surrounded by single tilde to strikethrough
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
|
||||||
|
CharacterShortcutEvent formatTildeToStrikethrough = CharacterShortcutEvent(
|
||||||
|
key: 'format the text surrounded by single tilde to strikethrough',
|
||||||
|
character: _tilde,
|
||||||
|
handler: (editorState) async => handleFormatByWrappingWithSingleCharacter(
|
||||||
|
editorState: editorState,
|
||||||
|
character: _tilde,
|
||||||
|
formatStyle: FormatStyleByWrappingWithSingleChar.strikethrough,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/command/transform.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/util/util.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// insert a new line block
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - mobile
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
final CharacterShortcutEvent insertNewLine = CharacterShortcutEvent(
|
||||||
|
key: 'insert a new line',
|
||||||
|
character: '\n',
|
||||||
|
handler: _insertNewLineHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {
|
||||||
|
// on desktop or web, shift + enter to insert a '\n' character to the same line.
|
||||||
|
// so, we should return the false to let the system handle it.
|
||||||
|
if (PlatformExtension.isNotMobile && RawKeyboard.instance.isShiftPressed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selection = editorState.selection?.normalized;
|
||||||
|
if (selection == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the selection
|
||||||
|
await editorState.deleteSelection(selection);
|
||||||
|
// insert a new line
|
||||||
|
await editorState.insertNewLine(position: selection.start);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
|
// Include all the shortcut(formatting) events triggered by wrapping text with double characters.
|
||||||
|
// 1. double asterisk to bold -> **abc**
|
||||||
|
// 2. double underscore to bold -> __abc__
|
||||||
|
|
||||||
|
// Include all the shortcut(formatting) events triggered by wrapping text with a single character.
|
||||||
|
// 1. backquote to code -> `abc`
|
||||||
|
// 2. underscore to italic -> _abc_
|
||||||
|
// 3. asterisk to italic -> *abc*
|
||||||
|
// 4. tilde to strikethrough -> ~abc~
|
||||||
|
|
||||||
|
final List<CharacterShortcutEvent> markdownSyntaxShortcutEvents = [
|
||||||
|
// format code, `code`
|
||||||
|
formatBackquoteToCode,
|
||||||
|
|
||||||
|
// format italic,
|
||||||
|
// _italic_
|
||||||
|
// *italic*
|
||||||
|
formatUnderscoreToItalic,
|
||||||
|
formatAsteriskToItalic,
|
||||||
|
|
||||||
|
// format strikethrough,
|
||||||
|
// ~strikethrough~
|
||||||
|
// ~~strikethrough~~
|
||||||
|
formatTildeToStrikethrough,
|
||||||
|
formatDoubleTilesToStrikethrough,
|
||||||
|
|
||||||
|
// format bold, **bold** or __bold__
|
||||||
|
formatDoubleAsterisksToBold,
|
||||||
|
formatDoubleUnderscoresToBold,
|
||||||
|
|
||||||
|
// format -- into em dash
|
||||||
|
formatDoubleHyphenEmDash,
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Show the slash menu
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
final CharacterShortcutEvent slashCommand = CharacterShortcutEvent(
|
||||||
|
key: 'show the slash menu',
|
||||||
|
character: '/',
|
||||||
|
handler: (editorState) async => await _showSlashMenu(
|
||||||
|
editorState,
|
||||||
|
standardSelectionMenuItems,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
CharacterShortcutEvent customSlashCommand(
|
||||||
|
List<SelectionMenuItem> items, {
|
||||||
|
bool shouldInsertSlash = true,
|
||||||
|
SelectionMenuStyle style = SelectionMenuStyle.light,
|
||||||
|
}) {
|
||||||
|
return CharacterShortcutEvent(
|
||||||
|
key: 'show the slash menu',
|
||||||
|
character: '/',
|
||||||
|
handler: (editorState) => _showSlashMenu(
|
||||||
|
editorState,
|
||||||
|
items,
|
||||||
|
shouldInsertSlash: shouldInsertSlash,
|
||||||
|
style: style,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<String> supportSlashMenuNodeWhiteList = {
|
||||||
|
ParagraphBlockKeys.type,
|
||||||
|
HeadingBlockKeys.type,
|
||||||
|
TodoListBlockKeys.type,
|
||||||
|
BulletedListBlockKeys.type,
|
||||||
|
NumberedListBlockKeys.type,
|
||||||
|
QuoteBlockKeys.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
SelectionMenuService? _selectionMenuService;
|
||||||
|
Future<bool> _showSlashMenu(
|
||||||
|
EditorState editorState,
|
||||||
|
List<SelectionMenuItem> items, {
|
||||||
|
bool shouldInsertSlash = true,
|
||||||
|
SelectionMenuStyle style = SelectionMenuStyle.light,
|
||||||
|
}) async {
|
||||||
|
if (PlatformExtension.isMobile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the selection
|
||||||
|
if (!selection.isCollapsed) {
|
||||||
|
await editorState.deleteSelection(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
final afterSelection = editorState.selection;
|
||||||
|
if (afterSelection == null || !afterSelection.isCollapsed) {
|
||||||
|
assert(false, 'the selection should be collapsed');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path);
|
||||||
|
|
||||||
|
// only enable in white-list nodes
|
||||||
|
if (node == null || !_isSupportSlashMenuNode(node)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert the slash character
|
||||||
|
if (shouldInsertSlash) {
|
||||||
|
if (kIsWeb) {
|
||||||
|
// Have no idea why the focus will lose after inserting on web.
|
||||||
|
keepEditorFocusNotifier.increase();
|
||||||
|
await editorState.insertTextAtPosition('/', position: selection.start);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(timeStamp) => keepEditorFocusNotifier.decrease(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await editorState.insertTextAtPosition('/', position: selection.start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// show the slash menu
|
||||||
|
() {
|
||||||
|
// this code is copied from the the old editor.
|
||||||
|
// TODO: refactor this code
|
||||||
|
final context = editorState.getNodeAtPath(selection.start.path)?.context;
|
||||||
|
if (context != null) {
|
||||||
|
_selectionMenuService = SelectionMenu(
|
||||||
|
context: context,
|
||||||
|
editorState: editorState,
|
||||||
|
selectionMenuItems: items,
|
||||||
|
deleteSlashByDefault: shouldInsertSlash,
|
||||||
|
style: style,
|
||||||
|
);
|
||||||
|
_selectionMenuService?.show();
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isSupportSlashMenuNode(Node node) {
|
||||||
|
var result = supportSlashMenuNodeWhiteList.contains(node.type);
|
||||||
|
if (node.level > 1 && node.parent != null) {
|
||||||
|
return result && _isSupportSlashMenuNode(node.parent!);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,126 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final List<CommandShortcutEvent> arrowDownKeys = [
|
||||||
|
moveCursorDownCommand,
|
||||||
|
moveCursorBottomSelectCommand,
|
||||||
|
moveCursorBottomCommand,
|
||||||
|
moveCursorDownSelectCommand,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Arrow down key events.
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
|
||||||
|
// arrow down key
|
||||||
|
// move the cursor downward vertically
|
||||||
|
final CommandShortcutEvent moveCursorDownCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor downward',
|
||||||
|
command: 'arrow down',
|
||||||
|
handler: _moveCursorDownCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorDownCommandHandler = (editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final downPosition = selection.end.moveVertical(editorState, upwards: false);
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
downPosition == null ? null : Selection.collapsed(downPosition),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// arrow down + shift + ctrl or cmd
|
||||||
|
/// move the cursor to the bottommost position of the document and select everything in between
|
||||||
|
CommandShortcutEvent moveCursorBottomSelectCommand = CommandShortcutEvent(
|
||||||
|
key: 'move cursor to end of file and select all',
|
||||||
|
command: 'ctrl+shift+arrow down',
|
||||||
|
macOSCommand: 'cmd+shift+arrow down',
|
||||||
|
handler: _moveCursorBottomSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorBottomSelectCommandHandler =
|
||||||
|
(editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = editorState.getLastSelectable();
|
||||||
|
if (result == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final position = result.$2.end(result.$1);
|
||||||
|
editorState.scrollService?.jumpToBottom();
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: position),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// arrow down + ctrl or cmd
|
||||||
|
/// move the cursor to the bottommost position of the document
|
||||||
|
CommandShortcutEvent moveCursorBottomCommand = CommandShortcutEvent(
|
||||||
|
key: 'move cursor to end of file',
|
||||||
|
command: 'ctrl+arrow down',
|
||||||
|
macOSCommand: 'cmd+arrow down',
|
||||||
|
handler: _moveCursorBottomCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorBottomCommandHandler = (editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = editorState.getLastSelectable();
|
||||||
|
if (result == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final position = result.$2.end(result.$1);
|
||||||
|
editorState.scrollService?.jumpToBottom();
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
Selection.collapsed(position),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// arrow down + shift
|
||||||
|
/// moves vertically down one line and selects everything between
|
||||||
|
CommandShortcutEvent moveCursorDownSelectCommand = CommandShortcutEvent(
|
||||||
|
key: 'move cursor down and select one line',
|
||||||
|
command: 'shift+arrow down',
|
||||||
|
macOSCommand: 'shift+arrow down',
|
||||||
|
handler: _moveCursorDownSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorDownSelectCommandHandler =
|
||||||
|
(editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
final end = selection.end.moveVertical(editorState, upwards: false);
|
||||||
|
if (end == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: end),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,215 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final List<CommandShortcutEvent> arrowLeftKeys = [
|
||||||
|
moveCursorLeftCommand,
|
||||||
|
moveCursorToBeginCommand,
|
||||||
|
moveCursorToLeftWordCommand,
|
||||||
|
moveCursorLeftSelectCommand,
|
||||||
|
moveCursorBeginSelectCommand,
|
||||||
|
moveCursorLeftWordSelectCommand,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Arrow left key events.
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
|
||||||
|
// arrow left key
|
||||||
|
// move the cursor forward one character
|
||||||
|
final CommandShortcutEvent moveCursorLeftCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor forward one character',
|
||||||
|
command: 'arrow left',
|
||||||
|
handler: _arrowLeftCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _arrowLeftCommandHandler = (editorState) {
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
editorState.moveCursorBackward(SelectionMoveRange.character);
|
||||||
|
} else {
|
||||||
|
editorState.moveCursorForward(SelectionMoveRange.character);
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// arrow left key + ctrl or command
|
||||||
|
// move the cursor to the beginning of the block
|
||||||
|
final CommandShortcutEvent moveCursorToBeginCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor at the start of line',
|
||||||
|
command: 'home',
|
||||||
|
macOSCommand: 'cmd+arrow left',
|
||||||
|
handler: _moveCursorToBeginCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorToBeginCommandHandler = (editorState) {
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
editorState.moveCursorBackward(SelectionMoveRange.line);
|
||||||
|
} else {
|
||||||
|
editorState.moveCursorForward(SelectionMoveRange.line);
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// arrow left key + alt
|
||||||
|
// move the cursor to the left word
|
||||||
|
final CommandShortcutEvent moveCursorToLeftWordCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor to the left word',
|
||||||
|
command: 'ctrl+arrow left',
|
||||||
|
macOSCommand: 'alt+arrow left',
|
||||||
|
handler: _moveCursorToLeftWordCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorToLeftWordCommandHandler =
|
||||||
|
(editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = editorState.getNodeAtPath(selection.end.path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
|
||||||
|
if (node == null || delta == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
final endOfWord = selection.end.moveHorizontal(
|
||||||
|
editorState,
|
||||||
|
forward: false,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
);
|
||||||
|
final selectedWord = delta.toPlainText().substring(
|
||||||
|
selection.end.offset,
|
||||||
|
endOfWord?.offset,
|
||||||
|
);
|
||||||
|
// check if the selected word is whitespace
|
||||||
|
if (selectedWord.trim().isEmpty) {
|
||||||
|
editorState.moveCursorBackward(SelectionMoveRange.word);
|
||||||
|
}
|
||||||
|
editorState.moveCursorBackward(SelectionMoveRange.word);
|
||||||
|
} else {
|
||||||
|
final startOfWord = selection.end.moveHorizontal(
|
||||||
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
);
|
||||||
|
if (startOfWord == null) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
final selectedWord = delta.toPlainText().substring(
|
||||||
|
startOfWord.offset,
|
||||||
|
selection.end.offset,
|
||||||
|
);
|
||||||
|
// check if the selected word is whitespace
|
||||||
|
if (selectedWord.trim().isEmpty) {
|
||||||
|
editorState.moveCursorForward(SelectionMoveRange.word);
|
||||||
|
}
|
||||||
|
editorState.moveCursorForward(SelectionMoveRange.word);
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// arrow left key + alt + shift
|
||||||
|
final CommandShortcutEvent moveCursorLeftWordSelectCommand =
|
||||||
|
CommandShortcutEvent(
|
||||||
|
key: 'move the cursor to select the left word',
|
||||||
|
command: 'ctrl+shift+arrow left',
|
||||||
|
macOSCommand: 'alt+shift+arrow left',
|
||||||
|
handler: _moveCursorLeftWordSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorLeftWordSelectCommandHandler =
|
||||||
|
(editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
var forward = true;
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
forward = false;
|
||||||
|
}
|
||||||
|
final end = selection.end.moveHorizontal(
|
||||||
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
forward: forward,
|
||||||
|
);
|
||||||
|
if (end == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: end),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// arrow left key + shift
|
||||||
|
// selects only one character
|
||||||
|
final CommandShortcutEvent moveCursorLeftSelectCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor left select',
|
||||||
|
command: 'shift+arrow left',
|
||||||
|
handler: _moveCursorLeftSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorLeftSelectCommandHandler =
|
||||||
|
(editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
var forward = true;
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
forward = false;
|
||||||
|
}
|
||||||
|
final end = selection.end.moveHorizontal(editorState, forward: forward);
|
||||||
|
if (end == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: end),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
final CommandShortcutEvent moveCursorBeginSelectCommand = CommandShortcutEvent(
|
||||||
|
key: 'move cursor to select till start of line',
|
||||||
|
command: 'shift+home',
|
||||||
|
macOSCommand: 'cmd+shift+arrow left',
|
||||||
|
handler: _moveCursorBeginSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorBeginSelectCommandHandler =
|
||||||
|
(editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
final nodes = editorState.getNodesInSelection(selection);
|
||||||
|
if (nodes.isEmpty) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
var end = selection.end;
|
||||||
|
final position = isRTL(editorState)
|
||||||
|
? nodes.last.selectable?.end()
|
||||||
|
: nodes.last.selectable?.start();
|
||||||
|
if (position != null) {
|
||||||
|
end = position;
|
||||||
|
}
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: end),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool isRTL(EditorState editorState) {
|
||||||
|
if (editorState.selection != null) {
|
||||||
|
final node = editorState.getNodeAtPath(editorState.selection!.end.path);
|
||||||
|
return node?.selectable?.textDirection() == TextDirection.rtl;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final List<CommandShortcutEvent> arrowRightKeys = [
|
||||||
|
moveCursorRightCommand,
|
||||||
|
moveCursorToEndCommand,
|
||||||
|
moveCursorToRightWordCommand,
|
||||||
|
moveCursorRightSelectCommand,
|
||||||
|
moveCursorEndSelectCommand,
|
||||||
|
moveCursorRightWordSelectCommand,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Arrow right key events.
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
|
||||||
|
// arrow right key
|
||||||
|
// move the cursor backward one character
|
||||||
|
final CommandShortcutEvent moveCursorRightCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor backward one character',
|
||||||
|
command: 'arrow right',
|
||||||
|
handler: _arrowRightCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _arrowRightCommandHandler = (editorState) {
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
editorState.moveCursorForward(SelectionMoveRange.character);
|
||||||
|
} else {
|
||||||
|
editorState.moveCursorBackward(SelectionMoveRange.character);
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// arrow right key + ctrl or command
|
||||||
|
// move the cursor to the end of the block
|
||||||
|
final CommandShortcutEvent moveCursorToEndCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor to the end of line',
|
||||||
|
command: 'end',
|
||||||
|
macOSCommand: 'cmd+arrow right',
|
||||||
|
handler: _moveCursorToEndCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorToEndCommandHandler = (editorState) {
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
editorState.moveCursorForward(SelectionMoveRange.line);
|
||||||
|
} else {
|
||||||
|
editorState.moveCursorBackward(SelectionMoveRange.line);
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// arrow right key + alt
|
||||||
|
// move the cursor to the right word
|
||||||
|
final CommandShortcutEvent moveCursorToRightWordCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor to the right word',
|
||||||
|
command: 'ctrl+arrow right',
|
||||||
|
macOSCommand: 'alt+arrow right',
|
||||||
|
handler: _moveCursorToRightWordCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorToRightWordCommandHandler =
|
||||||
|
(editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final node = editorState.getNodeAtPath(selection.end.path);
|
||||||
|
final delta = node?.delta;
|
||||||
|
|
||||||
|
if (node == null || delta == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
final startOfWord = selection.end.moveHorizontal(
|
||||||
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
);
|
||||||
|
if (startOfWord == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
final selectedWord = delta.toPlainText().substring(
|
||||||
|
startOfWord.offset,
|
||||||
|
selection.end.offset,
|
||||||
|
);
|
||||||
|
// check if the selected word is whitespace
|
||||||
|
if (selectedWord.trim().isEmpty) {
|
||||||
|
editorState.moveCursorForward(SelectionMoveRange.word);
|
||||||
|
}
|
||||||
|
editorState.moveCursorForward(SelectionMoveRange.word);
|
||||||
|
} else {
|
||||||
|
final endOfWord = selection.end.moveHorizontal(
|
||||||
|
editorState,
|
||||||
|
forward: false,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
);
|
||||||
|
if (endOfWord == null) {
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
final selectedLine = delta.toPlainText();
|
||||||
|
final selectedWord = selectedLine.substring(
|
||||||
|
selection.end.offset,
|
||||||
|
endOfWord.offset,
|
||||||
|
);
|
||||||
|
// check if the selected word is whitespace
|
||||||
|
if (selectedWord.trim().isEmpty) {
|
||||||
|
editorState.moveCursorBackward(SelectionMoveRange.word);
|
||||||
|
}
|
||||||
|
editorState.moveCursorBackward(SelectionMoveRange.word);
|
||||||
|
}
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// arrow right key + alt + shift
|
||||||
|
final CommandShortcutEvent moveCursorRightWordSelectCommand =
|
||||||
|
CommandShortcutEvent(
|
||||||
|
key: 'move the cursor to select the right word',
|
||||||
|
command: 'ctrl+shift+arrow right',
|
||||||
|
macOSCommand: 'alt+shift+arrow right',
|
||||||
|
handler: _moveCursorRightWordSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorRightWordSelectCommandHandler =
|
||||||
|
(editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
var forward = false;
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
forward = true;
|
||||||
|
}
|
||||||
|
final end = selection.end.moveHorizontal(
|
||||||
|
editorState,
|
||||||
|
selectionRange: SelectionRange.word,
|
||||||
|
forward: forward,
|
||||||
|
);
|
||||||
|
if (end == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: end),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// arrow right key + shift
|
||||||
|
// selects only one character
|
||||||
|
final CommandShortcutEvent moveCursorRightSelectCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor right select',
|
||||||
|
command: 'shift+arrow right',
|
||||||
|
handler: _moveCursorRightSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorRightSelectCommandHandler =
|
||||||
|
(editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
var forward = false;
|
||||||
|
if (isRTL(editorState)) {
|
||||||
|
forward = true;
|
||||||
|
}
|
||||||
|
final end = selection.end.moveHorizontal(editorState, forward: forward);
|
||||||
|
if (end == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: end),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
// arrow right key + shift + ctrl or cmd
|
||||||
|
final CommandShortcutEvent moveCursorEndSelectCommand = CommandShortcutEvent(
|
||||||
|
key: 'move cursor to select till end of line',
|
||||||
|
command: 'shift+end',
|
||||||
|
macOSCommand: 'cmd+shift+arrow right',
|
||||||
|
handler: _moveCursorEndSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorEndSelectCommandHandler = (editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
final nodes = editorState.getNodesInSelection(selection);
|
||||||
|
if (nodes.isEmpty) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
var end = selection.end;
|
||||||
|
final position = isRTL(editorState)
|
||||||
|
? nodes.last.selectable?.start()
|
||||||
|
: nodes.last.selectable?.end();
|
||||||
|
if (position != null) {
|
||||||
|
end = position;
|
||||||
|
}
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: end),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final List<CommandShortcutEvent> arrowUpKeys = [
|
||||||
|
moveCursorUpCommand,
|
||||||
|
moveCursorTopSelectCommand,
|
||||||
|
moveCursorTopCommand,
|
||||||
|
moveCursorUpSelectCommand,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Arrow up key events.
|
||||||
|
///
|
||||||
|
/// - support
|
||||||
|
/// - desktop
|
||||||
|
/// - web
|
||||||
|
///
|
||||||
|
|
||||||
|
// arrow up key
|
||||||
|
// move the cursor upward vertically
|
||||||
|
final CommandShortcutEvent moveCursorUpCommand = CommandShortcutEvent(
|
||||||
|
key: 'move the cursor upward',
|
||||||
|
command: 'arrow up',
|
||||||
|
handler: _moveCursorUpCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorUpCommandHandler = (editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final upPosition = selection.end.moveVertical(editorState);
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
upPosition == null ? null : Selection.collapsed(upPosition),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// arrow up + shift + ctrl or cmd
|
||||||
|
/// move the cursor to the topmost position of the document and select everything in between
|
||||||
|
final CommandShortcutEvent moveCursorTopSelectCommand = CommandShortcutEvent(
|
||||||
|
key: 'move cursor to start of file and select all',
|
||||||
|
command: 'ctrl+shift+arrow up',
|
||||||
|
macOSCommand: 'cmd+shift+arrow up',
|
||||||
|
handler: _moveCursorTopSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorTopSelectCommandHandler = (editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
final result = editorState.getFirstSelectable();
|
||||||
|
if (result == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final position = result.$2.start(result.$1);
|
||||||
|
editorState.scrollService?.jumpToTop();
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: position),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// arrow up + ctrl or cmd
|
||||||
|
/// move the cursor to the topmost position of the document
|
||||||
|
final CommandShortcutEvent moveCursorTopCommand = CommandShortcutEvent(
|
||||||
|
key: 'move cursor to start of file',
|
||||||
|
command: 'ctrl+arrow up',
|
||||||
|
macOSCommand: 'cmd+arrow up',
|
||||||
|
handler: _moveCursorTopCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorTopCommandHandler = (editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = editorState.getFirstSelectable();
|
||||||
|
if (result == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final position = result.$2.start(result.$1);
|
||||||
|
editorState.scrollService?.jumpToTop();
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
Selection.collapsed(position),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// arrow up + ctrl or cmd
|
||||||
|
/// moves vertically down one line and selects everything between
|
||||||
|
final CommandShortcutEvent moveCursorUpSelectCommand = CommandShortcutEvent(
|
||||||
|
key: 'move cursor up and select one line',
|
||||||
|
command: 'shift+arrow up',
|
||||||
|
macOSCommand: 'shift+arrow up',
|
||||||
|
handler: _moveCursorUpSelectCommandHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
CommandShortcutEventHandler _moveCursorUpSelectCommandHandler = (editorState) {
|
||||||
|
final selection = editorState.selection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
final end = selection.end.moveVertical(editorState);
|
||||||
|
if (end == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
selection.copyWith(end: end),
|
||||||
|
reason: SelectionUpdateReason.uiEvent,
|
||||||
|
);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue