chore: rename shortcut folder (#569)

* chore: rename shortcut folder

* chore: bump version 1.5.1
This commit is contained in:
Lucas.Xu 2023-10-31 14:51:38 +08:00 • committed by GitHub
commit b8368d82a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 24 additions and 16 deletions

View file

@ -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))

View file

@ -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 = [

View file

@ -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';

View file

@ -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';

View file

@ -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;
}

View file

@ -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,
),
);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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,
),
);

View file

@ -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,
),
);

View file

@ -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,
),
);

View file

@ -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;
}

View file

@ -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,
),
);

View file

@ -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;
};

View file

@ -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,
];

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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