|
@@ -1,8 +1,8 @@
|
|
|
import isHotkey from 'is-hotkey';
|
|
|
import { toggleFormat } from './format';
|
|
|
import { Editor, Range } from 'slate';
|
|
|
-import { getRetainRangeBy, getDelta, getInsertRangeBy } from './text';
|
|
|
-import { TextDelta, TextSelection } from '$app/interfaces/document';
|
|
|
+import { getBeforeRangeAt, getDelta, getAfterRangeAt, pointInEnd, pointInBegin, clonePoint } from './text';
|
|
|
+import { SelectionPoint, TextDelta, TextSelection } from '$app/interfaces/document';
|
|
|
|
|
|
const HOTKEYS: Record<string, string> = {
|
|
|
'mod+b': 'bold',
|
|
@@ -13,6 +13,16 @@ const HOTKEYS: Record<string, string> = {
|
|
|
'mod+shift+S': 'strikethrough',
|
|
|
};
|
|
|
|
|
|
+export const keyBoardEventKeyMap = {
|
|
|
+ Enter: 'Enter',
|
|
|
+ Backspace: 'Backspace',
|
|
|
+ Tab: 'Tab',
|
|
|
+ Up: 'ArrowUp',
|
|
|
+ Down: 'ArrowDown',
|
|
|
+ Left: 'ArrowLeft',
|
|
|
+ Right: 'ArrowRight',
|
|
|
+};
|
|
|
+
|
|
|
export function triggerHotkey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
|
|
for (const hotkey in HOTKEYS) {
|
|
|
if (isHotkey(hotkey, event)) {
|
|
@@ -29,19 +39,73 @@ export function canHandleEnterKey(event: React.KeyboardEvent<HTMLDivElement>, ed
|
|
|
}
|
|
|
|
|
|
export function canHandleBackspaceKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
|
|
- const isBackspaceKey = event.key === 'Backspace';
|
|
|
+ const isBackspaceKey = isHotkey('backspace', event);
|
|
|
const selection = editor.selection;
|
|
|
+
|
|
|
if (!isBackspaceKey || !selection) {
|
|
|
return false;
|
|
|
}
|
|
|
// It should be handled if the selection is collapsed and the cursor is at the beginning of the block
|
|
|
- const { anchor } = selection;
|
|
|
const isCollapsed = Range.isCollapsed(selection);
|
|
|
- return isCollapsed && anchor.offset === 0 && anchor.path.toString() === '0,0';
|
|
|
+ return isCollapsed && pointInBegin(editor, selection);
|
|
|
}
|
|
|
|
|
|
export function canHandleTabKey(event: React.KeyboardEvent<HTMLDivElement>, _: Editor) {
|
|
|
- return event.key === 'Tab';
|
|
|
+ return isHotkey('tab', event);
|
|
|
+}
|
|
|
+
|
|
|
+export function canHandleUpKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
|
|
+ const isUpKey = event.key === keyBoardEventKeyMap.Up;
|
|
|
+ const selection = editor.selection;
|
|
|
+ if (!isUpKey || !selection) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // It should be handled if the selection is collapsed and the cursor is at the first line of the block
|
|
|
+ const isCollapsed = Range.isCollapsed(selection);
|
|
|
+
|
|
|
+ const beforeString = Editor.string(editor, getBeforeRangeAt(editor, selection));
|
|
|
+ const isTopEdge = !beforeString.includes('\n');
|
|
|
+
|
|
|
+ return isCollapsed && isTopEdge;
|
|
|
+}
|
|
|
+
|
|
|
+export function canHandleDownKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
|
|
+ const isDownKey = event.key === keyBoardEventKeyMap.Down;
|
|
|
+ const selection = editor.selection;
|
|
|
+ if (!isDownKey || !selection) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // It should be handled if the selection is collapsed and the cursor is at the last line of the block
|
|
|
+ const isCollapsed = Range.isCollapsed(selection);
|
|
|
+
|
|
|
+ const afterString = Editor.string(editor, getAfterRangeAt(editor, selection));
|
|
|
+ const isBottomEdge = !afterString.includes('\n');
|
|
|
+
|
|
|
+ return isCollapsed && isBottomEdge;
|
|
|
+}
|
|
|
+
|
|
|
+export function canHandleLeftKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
|
|
+ const isLeftKey = event.key === keyBoardEventKeyMap.Left;
|
|
|
+ const selection = editor.selection;
|
|
|
+ if (!isLeftKey || !selection) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // It should be handled if the selection is collapsed and the cursor is at the beginning of the block
|
|
|
+ const isCollapsed = Range.isCollapsed(selection);
|
|
|
+
|
|
|
+ return isCollapsed && pointInBegin(editor, selection);
|
|
|
+}
|
|
|
+
|
|
|
+export function canHandleRightKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
|
|
|
+ const isRightKey = event.key === keyBoardEventKeyMap.Right;
|
|
|
+ const selection = editor.selection;
|
|
|
+ if (!isRightKey || !selection) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // It should be handled if the selection is collapsed and the cursor is at the end of the block
|
|
|
+ const isCollapsed = Range.isCollapsed(selection);
|
|
|
+ return isCollapsed && pointInEnd(editor, selection);
|
|
|
}
|
|
|
|
|
|
export function onHandleEnterKey(
|
|
@@ -52,37 +116,47 @@ export function onHandleEnterKey(
|
|
|
onWrap,
|
|
|
}: {
|
|
|
onSplit: (...args: [TextDelta[], TextDelta[]]) => Promise<void>;
|
|
|
- onWrap: (newDelta: TextDelta[], selection: TextSelection) => Promise<void>;
|
|
|
+ onWrap: (newDelta: TextDelta[], _selection: TextSelection) => Promise<void>;
|
|
|
}
|
|
|
) {
|
|
|
+ const selection = editor.selection;
|
|
|
+ if (!selection) return;
|
|
|
// get the retain content
|
|
|
- const retainRange = getRetainRangeBy(editor);
|
|
|
+ const retainRange = getBeforeRangeAt(editor, selection);
|
|
|
const retain = getDelta(editor, retainRange);
|
|
|
// get the insert content
|
|
|
- const insertRange = getInsertRangeBy(editor);
|
|
|
+ const insertRange = getAfterRangeAt(editor, selection);
|
|
|
const insert = getDelta(editor, insertRange);
|
|
|
|
|
|
// if the shift key is pressed, break wrap the current node
|
|
|
- if (event.shiftKey || event.ctrlKey || event.altKey) {
|
|
|
- const selection = getSelectionAfterBreakWrap(editor);
|
|
|
- if (!selection) return;
|
|
|
+ if (isHotkey('shift+enter', event)) {
|
|
|
+ const newSelection = getSelectionAfterBreakWrap(editor);
|
|
|
+ if (!newSelection) return;
|
|
|
+
|
|
|
// insert `\n` after the retain content
|
|
|
- void onWrap([...retain, { insert: '\n' }, ...insert], selection);
|
|
|
+ void onWrap([...retain, { insert: '\n' }, ...insert], newSelection);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // if the enter key is pressed, split the current node
|
|
|
+ if (isHotkey('enter', event)) {
|
|
|
+ // retain this node and insert a new node
|
|
|
+ void onSplit(retain, insert);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // retain this node and insert a new node
|
|
|
- void onSplit(retain, insert);
|
|
|
+ // other cases, do nothing
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
function getSelectionAfterBreakWrap(editor: Editor) {
|
|
|
const selection = editor.selection;
|
|
|
if (!selection) return;
|
|
|
const start = Range.start(selection);
|
|
|
- const cursor = { ...start, offset: start.offset + 1 };
|
|
|
+ const cursor = { path: start.path, offset: start.offset + 1 } as SelectionPoint;
|
|
|
const newSelection = {
|
|
|
- anchor: Object.create(cursor),
|
|
|
- focus: Object.create(cursor),
|
|
|
+ anchor: clonePoint(cursor),
|
|
|
+ focus: clonePoint(cursor),
|
|
|
} as TextSelection;
|
|
|
return newSelection;
|
|
|
}
|