mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
feat:add funtionality of home and end hotkeys on web version (#43)
Co-authored-by: Pavan <pavannaik20@gmail.com>
This commit is contained in:
@@ -12,183 +12,486 @@ import { Editor, Text, Range, Transforms, BasePoint, Path, Element } from 'slate
|
||||
import { ReactEditor, useReadOnly } from 'slate-react';
|
||||
import smoothScrollIntoViewIfNeeded from 'smooth-scroll-into-view-if-needed';
|
||||
|
||||
export function useShortcuts (editor: ReactEditor) {
|
||||
export function useShortcuts(editor: ReactEditor) {
|
||||
const yjsEditor = editor as YjsEditor;
|
||||
const { viewId } = useEditorContext();
|
||||
const readOnly = useReadOnly();
|
||||
|
||||
const focusedFocusableElement = useCallback((toStart?: boolean) => {
|
||||
if (readOnly) return;
|
||||
const title = document.getElementById(`editor-title-${viewId}`);
|
||||
const focusedFocusableElement = useCallback(
|
||||
(toStart?: boolean) => {
|
||||
if (readOnly) return;
|
||||
const title = document.getElementById(`editor-title-${viewId}`);
|
||||
|
||||
if (!title) return;
|
||||
if (!title) return;
|
||||
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
|
||||
const textNode = title.childNodes[0] as Node;
|
||||
const textNode = title.childNodes[0] as Node;
|
||||
|
||||
if (textNode) {
|
||||
range.setStart(textNode, toStart ? 0 : (textNode?.textContent?.length || 0));
|
||||
} else {
|
||||
range.setStart(title, 0);
|
||||
}
|
||||
if (textNode) {
|
||||
range.setStart(textNode, toStart ? 0 : textNode?.textContent?.length || 0);
|
||||
} else {
|
||||
range.setStart(title, 0);
|
||||
}
|
||||
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
}, [readOnly, viewId]);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
},
|
||||
[readOnly, viewId]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
|
||||
const e = event.nativeEvent;
|
||||
const { selection } = editor;
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLDivElement>) => {
|
||||
const e = event.nativeEvent;
|
||||
const { selection } = editor;
|
||||
|
||||
// Add more cases here for general shortcuts
|
||||
switch (true) {
|
||||
/**
|
||||
* Escape: Esc
|
||||
* Default behavior: Deselect editor
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.ESCAPE)(e):
|
||||
editor.deselect();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
// Add more cases here for general shortcuts
|
||||
switch (true) {
|
||||
case createHotkey(HOT_KEY_NAME.UP)(e): {
|
||||
const path = editor.start(selection).path;
|
||||
/**
|
||||
* Escape: Esc
|
||||
* Default behavior: Deselect editor
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.ESCAPE)(e):
|
||||
editor.deselect();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (selection && Range.isCollapsed(selection)) {
|
||||
switch (true) {
|
||||
case createHotkey(HOT_KEY_NAME.UP)(e): {
|
||||
const path = editor.start(selection).path;
|
||||
|
||||
if (Path.isAncestor([0, 0], path)) {
|
||||
e.preventDefault();
|
||||
focusedFocusableElement(false);
|
||||
break;
|
||||
}
|
||||
|
||||
const before = Editor.before(editor, selection, { unit: 'offset' });
|
||||
const beforeText = findInlineTextNode(editor, before);
|
||||
|
||||
if (before && beforeText) {
|
||||
e.preventDefault();
|
||||
Transforms.move(editor, { unit: 'line', reverse: true, distance: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (Path.isAncestor([0, 0], path)) {
|
||||
e.preventDefault();
|
||||
focusedFocusableElement(false);
|
||||
break;
|
||||
}
|
||||
|
||||
const before = Editor.before(editor, selection, { unit: 'offset' });
|
||||
const beforeText = findInlineTextNode(editor, before);
|
||||
case createHotkey(HOT_KEY_NAME.BACKSPACE)(e): {
|
||||
const [node] = getBlockEntry(yjsEditor);
|
||||
const type = node.type as BlockType;
|
||||
|
||||
if (before && beforeText) {
|
||||
e.preventDefault();
|
||||
Transforms.move(editor, { unit: 'line', reverse: true, distance: 2 });
|
||||
return;
|
||||
}
|
||||
if (type !== BlockType.Paragraph) {
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
const path = editor.start(selection).path;
|
||||
const currentLineText = CustomEditor.getBlockTextContent(node);
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.BACKSPACE)(e): {
|
||||
const [node] = getBlockEntry(yjsEditor);
|
||||
const type = node.type as BlockType;
|
||||
if (
|
||||
editor.children.length > 1 &&
|
||||
(editor.children[0] as Element).blockId === node.blockId &&
|
||||
currentLineText === ''
|
||||
) {
|
||||
CustomEditor.deleteBlock(yjsEditor, node.blockId as string);
|
||||
}
|
||||
|
||||
const before = Editor.before(editor, selection, { unit: 'offset' });
|
||||
|
||||
if (!before && Path.isAncestor([0, 0], path)) {
|
||||
e.preventDefault();
|
||||
focusedFocusableElement(false);
|
||||
}
|
||||
|
||||
if (type !== BlockType.Paragraph) {
|
||||
break;
|
||||
}
|
||||
|
||||
const path = editor.start(selection).path;
|
||||
const currentLineText = CustomEditor.getBlockTextContent(node);
|
||||
case createHotkey(HOT_KEY_NAME.LEFT)(e): {
|
||||
const path = editor.start(selection).path;
|
||||
const before = Editor.before(editor, selection, { unit: 'offset' });
|
||||
|
||||
if (editor.children.length > 1 && (editor.children[0] as Element).blockId === node.blockId && currentLineText === '') {
|
||||
CustomEditor.deleteBlock(yjsEditor, node.blockId as string);
|
||||
if (!before && Path.isAncestor([0, 0], path)) {
|
||||
focusedFocusableElement(false);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const before = Editor.before(editor, selection, { unit: 'offset' });
|
||||
case createHotkey(HOT_KEY_NAME.DOWN)(e): {
|
||||
const after = Editor.after(editor, selection, { unit: 'offset' });
|
||||
const afterText = findInlineTextNode(editor, after);
|
||||
|
||||
if (!before && Path.isAncestor([0, 0], path)) {
|
||||
e.preventDefault();
|
||||
focusedFocusableElement(false);
|
||||
if (afterText) {
|
||||
e.preventDefault();
|
||||
Transforms.move(editor, { unit: 'line', distance: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.OPEN_LINK)(e): {
|
||||
event.preventDefault();
|
||||
const marks = CustomEditor.getAllMarks(editor);
|
||||
|
||||
const link = marks.find((mark) => !!mark[EditorMarkFormat.Href])?.[EditorMarkFormat.Href];
|
||||
|
||||
if (link) {
|
||||
void openUrl(link, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.DELETE_LEFT_SENTENCE)(e): {
|
||||
event.preventDefault();
|
||||
const focus = editor.start(selection);
|
||||
const anchor = Editor.before(editor, focus, { unit: 'line' });
|
||||
|
||||
if (anchor) {
|
||||
editor.delete({
|
||||
at: {
|
||||
anchor,
|
||||
focus,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.DELETE_LEFT_WORD)(e): {
|
||||
event.preventDefault();
|
||||
const focus = editor.start(selection);
|
||||
const anchor = Editor.before(editor, focus, { unit: 'word' });
|
||||
|
||||
if (anchor) {
|
||||
editor.delete({
|
||||
at: {
|
||||
anchor,
|
||||
focus,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.DELETE_RIGHT_WORD)(e): {
|
||||
event.preventDefault();
|
||||
const focus = editor.start(selection);
|
||||
const anchor = Editor.after(editor, focus, { unit: 'word' });
|
||||
|
||||
if (anchor) {
|
||||
editor.delete({
|
||||
at: {
|
||||
anchor,
|
||||
focus,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not process shortcuts if editor is read-only or no selection
|
||||
if (readOnly || !selection) return;
|
||||
const [point] = editor.edges(selection);
|
||||
const node = getBlockEntry(yjsEditor, point);
|
||||
|
||||
// Add more cases here for editing shortcuts
|
||||
switch (!readOnly) {
|
||||
/**
|
||||
* Select all: Mod+A
|
||||
* Default behavior: Select all text in the editor
|
||||
* Special case for select all in code block: Only select all text in code block
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.SELECT_ALL)(e):
|
||||
if (node && node[0].type === BlockType.CodeBlock) {
|
||||
event.preventDefault();
|
||||
editor.select(node[1]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* Indent block: Tab
|
||||
* Default behavior: Indent block
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.INDENT_BLOCK)(e):
|
||||
event.preventDefault();
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.LEFT)(e): {
|
||||
const path = editor.start(selection).path;
|
||||
const before = Editor.before(editor, selection, { unit: 'offset' });
|
||||
if (SOFT_BREAK_TYPES.includes(node[0]?.type as BlockType)) {
|
||||
editor.insertText('\t');
|
||||
break;
|
||||
}
|
||||
|
||||
if (!before && Path.isAncestor([0, 0], path)) {
|
||||
focusedFocusableElement(false);
|
||||
CustomEditor.tabEvent(yjsEditor, e);
|
||||
break;
|
||||
/**
|
||||
* Outdent block: Shift+Tab
|
||||
* Default behavior: Outdent block
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.OUTDENT_BLOCK)(e):
|
||||
event.preventDefault();
|
||||
|
||||
if (SOFT_BREAK_TYPES.includes(node[0]?.type as BlockType)) {
|
||||
editor.deleteBackward('character');
|
||||
break;
|
||||
}
|
||||
|
||||
CustomEditor.tabEvent(yjsEditor, e);
|
||||
break;
|
||||
/**
|
||||
* Split block: Enter
|
||||
* Default behavior: Split block
|
||||
* Special case for soft break types: Insert \n
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.SPLIT_BLOCK)(e):
|
||||
if (SOFT_BREAK_TYPES.includes(node[0]?.type as BlockType)) {
|
||||
event.preventDefault();
|
||||
editor.insertText('\n');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.DOWN)(e): {
|
||||
const after = Editor.after(editor, selection, { unit: 'offset' });
|
||||
const afterText = findInlineTextNode(editor, after);
|
||||
|
||||
if (afterText) {
|
||||
e.preventDefault();
|
||||
Transforms.move(editor, { unit: 'line', distance: 2 });
|
||||
return;
|
||||
/**
|
||||
* Insert soft break: Shift+Enter
|
||||
* Default behavior: Insert \n
|
||||
* Special case for soft break types: Split block
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.INSERT_SOFT_BREAK)(e):
|
||||
event.preventDefault();
|
||||
if (node && SOFT_BREAK_TYPES.includes(node[0]?.type as BlockType)) {
|
||||
editor.insertBreak();
|
||||
} else {
|
||||
editor.insertText('\n');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* Toggle todo: Mod+Enter
|
||||
* Default behavior: Toggle todo
|
||||
* Special case for toggle list block: Toggle collapse
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.TOGGLE_TODO)(e):
|
||||
case createHotkey(HOT_KEY_NAME.TOGGLE_COLLAPSE)(e):
|
||||
event.preventDefault();
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.OPEN_LINK)(e): {
|
||||
if (node[0].type === BlockType.ToggleListBlock) {
|
||||
CustomEditor.toggleToggleList(yjsEditor, node[0].blockId as string);
|
||||
} else if (node[0].type === BlockType.TodoListBlock) {
|
||||
CustomEditor.toggleTodoList(yjsEditor, node[0].blockId as string, false);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
/**
|
||||
* Open link: Opt + SHIFT + Enter
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.OPEN_LINKS)(e): {
|
||||
event.preventDefault();
|
||||
const marks = CustomEditor.getAllMarks(editor);
|
||||
const links = marks.map((mark) => mark[EditorMarkFormat.Href]).filter(Boolean);
|
||||
|
||||
const link = marks.find((mark) => !!mark[EditorMarkFormat.Href])?.[EditorMarkFormat.Href];
|
||||
if (links.length === 0) break;
|
||||
links.forEach((link) => {
|
||||
if (link) void openUrl(link, '_blank');
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (link) {
|
||||
void openUrl(link, '_blank');
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Extend document backward: Mod + Shift + Up
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.EXTEND_DOCUMENT_BACKWARD)(e): {
|
||||
event.preventDefault();
|
||||
const { selection } = editor;
|
||||
|
||||
if (!selection) return;
|
||||
const focus = editor.start(selection);
|
||||
const anchor = editor.start([0, 0]);
|
||||
|
||||
editor.select({
|
||||
anchor,
|
||||
focus,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend document forward: Mod + Shift + Down
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.EXTEND_DOCUMENT_FORWARD)(e): {
|
||||
event.preventDefault();
|
||||
const { selection } = editor;
|
||||
|
||||
if (!selection) return;
|
||||
const anchor = editor.end(selection);
|
||||
const focus = editor.end([editor.children.length - 1, 0]);
|
||||
|
||||
editor.select({
|
||||
anchor,
|
||||
focus,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend line backward: Opt + Shift + Left
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.EXTEND_LINE_BACKWARD)(e):
|
||||
event.preventDefault();
|
||||
Transforms.move(editor, {
|
||||
unit: 'word',
|
||||
reverse: true,
|
||||
});
|
||||
break;
|
||||
/**
|
||||
* Extend line forward: Opt + Shift + Right
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.EXTEND_LINE_FORWARD)(e):
|
||||
event.preventDefault();
|
||||
Transforms.move(editor, { unit: 'word' });
|
||||
break;
|
||||
/**
|
||||
* Paste Text: Mod + Shift + V
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.PASTE_PLAIN_TEXT)(e):
|
||||
event.preventDefault();
|
||||
void navigator.clipboard.readText().then((text) => {
|
||||
CustomEditor.pastedText(yjsEditor, text);
|
||||
});
|
||||
break;
|
||||
/**
|
||||
* Scroll to top: control+ Home
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.SCROLL_TO_TOP)(e): {
|
||||
event.preventDefault();
|
||||
const point = Editor.start(editor, [0, 0]);
|
||||
|
||||
if (!point) return;
|
||||
const dom = ReactEditor.toDOMNode(editor, editor);
|
||||
|
||||
void smoothScrollIntoViewIfNeeded(dom, {
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
editor.select({
|
||||
anchor: point,
|
||||
focus: point,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to bottom: Control + End
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.SCROLL_TO_BOTTOM)(e): {
|
||||
event.preventDefault();
|
||||
const point = Editor.end(editor, [editor.children.length - 1, 0]);
|
||||
|
||||
if (!point) return;
|
||||
const dom = ReactEditor.toDOMNode(editor, editor);
|
||||
|
||||
void smoothScrollIntoViewIfNeeded(dom, {
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
});
|
||||
editor.select({
|
||||
anchor: point,
|
||||
focus: point,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.DELETE_LEFT_SENTENCE)(e): {
|
||||
/**
|
||||
* Align left: Control + Shift + L
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.ALIGN_LEFT)(e): {
|
||||
event.preventDefault();
|
||||
const focus = editor.start(selection);
|
||||
const anchor = Editor.before(editor, focus, { unit: 'line' });
|
||||
|
||||
if (anchor) {
|
||||
editor.delete({
|
||||
at: {
|
||||
anchor,
|
||||
focus,
|
||||
},
|
||||
});
|
||||
}
|
||||
const blockId = node[0].blockId as string;
|
||||
|
||||
CustomEditor.setBlockData(yjsEditor, blockId, {
|
||||
align: AlignType.Left,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Align center: Control + Shift + E
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.ALIGN_CENTER)(e): {
|
||||
event.preventDefault();
|
||||
|
||||
const blockId = node[0].blockId as string;
|
||||
|
||||
CustomEditor.setBlockData(yjsEditor, blockId, {
|
||||
align: AlignType.Center,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Align right: Control + Shift + R
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.ALIGN_RIGHT)(e): {
|
||||
event.preventDefault();
|
||||
|
||||
const blockId = node[0].blockId as string;
|
||||
|
||||
CustomEditor.setBlockData(yjsEditor, blockId, {
|
||||
align: AlignType.Right,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.MOVE_CURSOR_TO_BOTTOM)(e): {
|
||||
event.preventDefault();
|
||||
const point = Editor.end(editor, [editor.children.length - 1, 0]);
|
||||
|
||||
if (!point) return;
|
||||
const dom = ReactEditor.toDOMNode(editor, editor);
|
||||
|
||||
void smoothScrollIntoViewIfNeeded(dom, {
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
});
|
||||
editor.select({
|
||||
anchor: point,
|
||||
focus: point,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.DELETE_LEFT_WORD)(e): {
|
||||
case createHotkey(HOT_KEY_NAME.MOVE_CURSOR_TO_TOP)(e): {
|
||||
event.preventDefault();
|
||||
const focus = editor.start(selection);
|
||||
const anchor = Editor.before(editor, focus, { unit: 'word' });
|
||||
const point = Editor.start(editor, [0, 0]);
|
||||
|
||||
if (anchor) {
|
||||
editor.delete({
|
||||
at: {
|
||||
anchor,
|
||||
focus,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!point) return;
|
||||
const dom = ReactEditor.toDOMNode(editor, editor);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.DELETE_RIGHT_WORD)(e): {
|
||||
event.preventDefault();
|
||||
const focus = editor.start(selection);
|
||||
const anchor = Editor.after(editor, focus, { unit: 'word' });
|
||||
|
||||
if (anchor) {
|
||||
editor.delete({
|
||||
at: {
|
||||
anchor,
|
||||
focus,
|
||||
},
|
||||
});
|
||||
}
|
||||
void smoothScrollIntoViewIfNeeded(dom, {
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
editor.select({
|
||||
anchor: point,
|
||||
focus: point,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -196,301 +499,16 @@ export function useShortcuts (editor: ReactEditor) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Do not process shortcuts if editor is read-only or no selection
|
||||
if (readOnly || !selection) return;
|
||||
const [point] = editor.edges(selection);
|
||||
const node = getBlockEntry(yjsEditor, point);
|
||||
|
||||
// Add more cases here for editing shortcuts
|
||||
switch (!readOnly) {
|
||||
/**
|
||||
* Select all: Mod+A
|
||||
* Default behavior: Select all text in the editor
|
||||
* Special case for select all in code block: Only select all text in code block
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.SELECT_ALL)(e):
|
||||
if (node && node[0].type === BlockType.CodeBlock) {
|
||||
event.preventDefault();
|
||||
editor.select(node[1]);
|
||||
}
|
||||
|
||||
break;
|
||||
/**
|
||||
* Indent block: Tab
|
||||
* Default behavior: Indent block
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.INDENT_BLOCK)(e):
|
||||
event.preventDefault();
|
||||
|
||||
if (SOFT_BREAK_TYPES.includes(node[0]?.type as BlockType)) {
|
||||
editor.insertText('\t');
|
||||
break;
|
||||
}
|
||||
|
||||
CustomEditor.tabEvent(yjsEditor, e);
|
||||
break;
|
||||
/**
|
||||
* Outdent block: Shift+Tab
|
||||
* Default behavior: Outdent block
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.OUTDENT_BLOCK)(e):
|
||||
event.preventDefault();
|
||||
|
||||
if (SOFT_BREAK_TYPES.includes(node[0]?.type as BlockType)) {
|
||||
editor.deleteBackward('character');
|
||||
break;
|
||||
}
|
||||
|
||||
CustomEditor.tabEvent(yjsEditor, e);
|
||||
break;
|
||||
/**
|
||||
* Split block: Enter
|
||||
* Default behavior: Split block
|
||||
* Special case for soft break types: Insert \n
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.SPLIT_BLOCK)(e):
|
||||
if (SOFT_BREAK_TYPES.includes(node[0]?.type as BlockType)) {
|
||||
event.preventDefault();
|
||||
editor.insertText('\n');
|
||||
}
|
||||
|
||||
break;
|
||||
/**
|
||||
* Insert soft break: Shift+Enter
|
||||
* Default behavior: Insert \n
|
||||
* Special case for soft break types: Split block
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.INSERT_SOFT_BREAK)(e):
|
||||
event.preventDefault();
|
||||
if (node && SOFT_BREAK_TYPES.includes(node[0]?.type as BlockType)) {
|
||||
editor.insertBreak();
|
||||
} else {
|
||||
editor.insertText('\n');
|
||||
}
|
||||
|
||||
break;
|
||||
/**
|
||||
* Toggle todo: Mod+Enter
|
||||
* Default behavior: Toggle todo
|
||||
* Special case for toggle list block: Toggle collapse
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.TOGGLE_TODO)(e):
|
||||
case createHotkey(HOT_KEY_NAME.TOGGLE_COLLAPSE)(e):
|
||||
event.preventDefault();
|
||||
|
||||
if (node[0].type === BlockType.ToggleListBlock) {
|
||||
CustomEditor.toggleToggleList(yjsEditor, node[0].blockId as string);
|
||||
} else if (node[0].type === BlockType.TodoListBlock) {
|
||||
CustomEditor.toggleTodoList(yjsEditor, node[0].blockId as string, false);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
/**
|
||||
* Open link: Opt + SHIFT + Enter
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.OPEN_LINKS)(e): {
|
||||
event.preventDefault();
|
||||
const marks = CustomEditor.getAllMarks(editor);
|
||||
const links = marks.map((mark) => mark[EditorMarkFormat.Href]).filter(Boolean);
|
||||
|
||||
if (links.length === 0) break;
|
||||
links.forEach((link) => {
|
||||
if (link) void openUrl(link, '_blank');
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend document backward: Mod + Shift + Up
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.EXTEND_DOCUMENT_BACKWARD)(e): {
|
||||
|
||||
event.preventDefault();
|
||||
const { selection } = editor;
|
||||
|
||||
if (!selection) return;
|
||||
const focus = editor.start(selection);
|
||||
const anchor = editor.start([0, 0]);
|
||||
|
||||
editor.select({
|
||||
anchor,
|
||||
focus,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend document forward: Mod + Shift + Down
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.EXTEND_DOCUMENT_FORWARD)(e): {
|
||||
|
||||
event.preventDefault();
|
||||
const { selection } = editor;
|
||||
|
||||
if (!selection) return;
|
||||
const anchor = editor.end(selection);
|
||||
const focus = editor.end([editor.children.length - 1, 0]);
|
||||
|
||||
editor.select({
|
||||
anchor,
|
||||
focus,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend line backward: Opt + Shift + Left
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.EXTEND_LINE_BACKWARD)(e):
|
||||
event.preventDefault();
|
||||
Transforms.move(editor, {
|
||||
unit: 'word',
|
||||
reverse: true,
|
||||
});
|
||||
break;
|
||||
/**
|
||||
* Extend line forward: Opt + Shift + Right
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.EXTEND_LINE_FORWARD)(e):
|
||||
event.preventDefault();
|
||||
Transforms.move(editor, { unit: 'word' });
|
||||
break;
|
||||
/**
|
||||
* Paste Text: Mod + Shift + V
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.PASTE_PLAIN_TEXT)(e):
|
||||
event.preventDefault();
|
||||
void navigator.clipboard.readText().then((text) => {
|
||||
CustomEditor.pastedText(yjsEditor, text);
|
||||
});
|
||||
break;
|
||||
/**
|
||||
* Scroll to top: Home
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.SCROLL_TO_TOP)(e): {
|
||||
event.preventDefault();
|
||||
const dom = ReactEditor.toDOMNode(editor, editor);
|
||||
|
||||
void smoothScrollIntoViewIfNeeded(dom, {
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to bottom: End
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.SCROLL_TO_BOTTOM)(e): {
|
||||
event.preventDefault();
|
||||
const dom = ReactEditor.toDOMNode(editor, editor);
|
||||
|
||||
void smoothScrollIntoViewIfNeeded(dom, {
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Align left: Control + Shift + L
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.ALIGN_LEFT)(e): {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const blockId = node[0].blockId as string;
|
||||
|
||||
CustomEditor.setBlockData(yjsEditor, blockId, {
|
||||
align: AlignType.Left,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Align center: Control + Shift + E
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.ALIGN_CENTER)(e): {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const blockId = node[0].blockId as string;
|
||||
|
||||
CustomEditor.setBlockData(yjsEditor, blockId, {
|
||||
align: AlignType.Center,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Align right: Control + Shift + R
|
||||
*/
|
||||
case createHotkey(HOT_KEY_NAME.ALIGN_RIGHT)(e): {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const blockId = node[0].blockId as string;
|
||||
|
||||
CustomEditor.setBlockData(yjsEditor, blockId, {
|
||||
align: AlignType.Right,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.MOVE_CURSOR_TO_BOTTOM)(e): {
|
||||
event.preventDefault();
|
||||
const point = Editor.end(editor, [editor.children.length - 1, 0]);
|
||||
|
||||
if (!point) return;
|
||||
const dom = ReactEditor.toDOMNode(editor, editor);
|
||||
|
||||
void smoothScrollIntoViewIfNeeded(dom, {
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
});
|
||||
editor.select({
|
||||
anchor: point,
|
||||
focus: point,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case createHotkey(HOT_KEY_NAME.MOVE_CURSOR_TO_TOP)(e): {
|
||||
event.preventDefault();
|
||||
const point = Editor.start(editor, [0, 0]);
|
||||
|
||||
if (!point) return;
|
||||
const dom = ReactEditor.toDOMNode(editor, editor);
|
||||
|
||||
void smoothScrollIntoViewIfNeeded(dom, {
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
editor.select({
|
||||
anchor: point,
|
||||
focus: point,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [focusedFocusableElement, editor, yjsEditor, readOnly]);
|
||||
},
|
||||
[focusedFocusableElement, editor, yjsEditor, readOnly]
|
||||
);
|
||||
|
||||
return {
|
||||
onKeyDown,
|
||||
};
|
||||
}
|
||||
|
||||
function findInlineTextNode (editor: Editor, point?: BasePoint) {
|
||||
function findInlineTextNode(editor: Editor, point?: BasePoint) {
|
||||
const [node] = editor.nodes({
|
||||
at: point,
|
||||
match: (n) => {
|
||||
@@ -499,4 +517,4 @@ function findInlineTextNode (editor: Editor, point?: BasePoint) {
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,8 +93,8 @@ const defaultHotKeys = {
|
||||
[HOT_KEY_NAME.HIGH_LIGHT]: ['mod+shift+h'],
|
||||
[HOT_KEY_NAME.EXTEND_DOCUMENT_BACKWARD]: ['mod+shift+up'],
|
||||
[HOT_KEY_NAME.EXTEND_DOCUMENT_FORWARD]: ['mod+shift+down'],
|
||||
[HOT_KEY_NAME.SCROLL_TO_TOP]: ['Home'],
|
||||
[HOT_KEY_NAME.SCROLL_TO_BOTTOM]: ['end'],
|
||||
[HOT_KEY_NAME.SCROLL_TO_TOP]: ['control+Home'],
|
||||
[HOT_KEY_NAME.SCROLL_TO_BOTTOM]: ['control+end'],
|
||||
[HOT_KEY_NAME.TOGGLE_THEME]: ['mod+shift+l'],
|
||||
[HOT_KEY_NAME.TOGGLE_SIDEBAR]: ['mod+.'],
|
||||
[HOT_KEY_NAME.FORMAT_LINK]: ['mod+k'],
|
||||
@@ -130,7 +130,6 @@ export const createHotkey = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record<HO
|
||||
const keys = customHotKeys || defaultHotKeys;
|
||||
const hotkeys = keys[hotkeyName];
|
||||
|
||||
|
||||
return (event: KeyboardEvent) => {
|
||||
return hotkeys.some((hotkey) => {
|
||||
return isHotkey(hotkey, event);
|
||||
@@ -155,7 +154,7 @@ export const createHotKeyLabel = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Reco
|
||||
.map((key) => {
|
||||
return key === ' ' ? 'Space' : key.charAt(0).toUpperCase() + key.slice(1);
|
||||
})
|
||||
.join(' + '),
|
||||
.join(' + ')
|
||||
)
|
||||
.join(' / ');
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user