hotkey.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import isHotkey from 'is-hotkey';
  2. import { toggleFormat } from './format';
  3. import { Editor, Range } from 'slate';
  4. import { getBeforeRangeAt, getDelta, getAfterRangeAt, pointInEnd, pointInBegin, clonePoint } from './text';
  5. import { SelectionPoint, TextDelta, TextSelection } from '$app/interfaces/document';
  6. const HOTKEYS: Record<string, string> = {
  7. 'mod+b': 'bold',
  8. 'mod+i': 'italic',
  9. 'mod+u': 'underline',
  10. 'mod+e': 'code',
  11. 'mod+shift+X': 'strikethrough',
  12. 'mod+shift+S': 'strikethrough',
  13. };
  14. export const keyBoardEventKeyMap = {
  15. Enter: 'Enter',
  16. Backspace: 'Backspace',
  17. Tab: 'Tab',
  18. Up: 'ArrowUp',
  19. Down: 'ArrowDown',
  20. Left: 'ArrowLeft',
  21. Right: 'ArrowRight',
  22. };
  23. export function triggerHotkey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
  24. for (const hotkey in HOTKEYS) {
  25. if (isHotkey(hotkey, event)) {
  26. event.preventDefault();
  27. const format = HOTKEYS[hotkey];
  28. toggleFormat(editor, format);
  29. }
  30. }
  31. }
  32. export function canHandleEnterKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
  33. const isEnter = event.key === 'Enter';
  34. return isEnter && editor.selection;
  35. }
  36. export function canHandleBackspaceKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
  37. const isBackspaceKey = isHotkey('backspace', event);
  38. const selection = editor.selection;
  39. if (!isBackspaceKey || !selection) {
  40. return false;
  41. }
  42. // It should be handled if the selection is collapsed and the cursor is at the beginning of the block
  43. const isCollapsed = Range.isCollapsed(selection);
  44. return isCollapsed && pointInBegin(editor, selection);
  45. }
  46. export function canHandleTabKey(event: React.KeyboardEvent<HTMLDivElement>, _: Editor) {
  47. return isHotkey('tab', event);
  48. }
  49. export function canHandleUpKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
  50. const isUpKey = event.key === keyBoardEventKeyMap.Up;
  51. const selection = editor.selection;
  52. if (!isUpKey || !selection) {
  53. return false;
  54. }
  55. // It should be handled if the selection is collapsed and the cursor is at the first line of the block
  56. const isCollapsed = Range.isCollapsed(selection);
  57. const beforeString = Editor.string(editor, getBeforeRangeAt(editor, selection));
  58. const isTopEdge = !beforeString.includes('\n');
  59. return isCollapsed && isTopEdge;
  60. }
  61. export function canHandleDownKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
  62. const isDownKey = event.key === keyBoardEventKeyMap.Down;
  63. const selection = editor.selection;
  64. if (!isDownKey || !selection) {
  65. return false;
  66. }
  67. // It should be handled if the selection is collapsed and the cursor is at the last line of the block
  68. const isCollapsed = Range.isCollapsed(selection);
  69. const afterString = Editor.string(editor, getAfterRangeAt(editor, selection));
  70. const isBottomEdge = !afterString.includes('\n');
  71. return isCollapsed && isBottomEdge;
  72. }
  73. export function canHandleLeftKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
  74. const isLeftKey = event.key === keyBoardEventKeyMap.Left;
  75. const selection = editor.selection;
  76. if (!isLeftKey || !selection) {
  77. return false;
  78. }
  79. // It should be handled if the selection is collapsed and the cursor is at the beginning of the block
  80. const isCollapsed = Range.isCollapsed(selection);
  81. return isCollapsed && pointInBegin(editor, selection);
  82. }
  83. export function canHandleRightKey(event: React.KeyboardEvent<HTMLDivElement>, editor: Editor) {
  84. const isRightKey = event.key === keyBoardEventKeyMap.Right;
  85. const selection = editor.selection;
  86. if (!isRightKey || !selection) {
  87. return false;
  88. }
  89. // It should be handled if the selection is collapsed and the cursor is at the end of the block
  90. const isCollapsed = Range.isCollapsed(selection);
  91. return isCollapsed && pointInEnd(editor, selection);
  92. }
  93. export function onHandleEnterKey(
  94. event: React.KeyboardEvent<HTMLDivElement>,
  95. editor: Editor,
  96. {
  97. onSplit,
  98. onWrap,
  99. }: {
  100. onSplit: (...args: [TextDelta[], TextDelta[]]) => Promise<void>;
  101. onWrap: (newDelta: TextDelta[], _selection: TextSelection) => Promise<void>;
  102. }
  103. ) {
  104. const selection = editor.selection;
  105. if (!selection) return;
  106. // get the retain content
  107. const retainRange = getBeforeRangeAt(editor, selection);
  108. const retain = getDelta(editor, retainRange);
  109. // get the insert content
  110. const insertRange = getAfterRangeAt(editor, selection);
  111. const insert = getDelta(editor, insertRange);
  112. // if the shift key is pressed, break wrap the current node
  113. if (isHotkey('shift+enter', event)) {
  114. const newSelection = getSelectionAfterBreakWrap(editor);
  115. if (!newSelection) return;
  116. // insert `\n` after the retain content
  117. void onWrap([...retain, { insert: '\n' }, ...insert], newSelection);
  118. return;
  119. }
  120. // if the enter key is pressed, split the current node
  121. if (isHotkey('enter', event)) {
  122. // retain this node and insert a new node
  123. void onSplit(retain, insert);
  124. return;
  125. }
  126. // other cases, do nothing
  127. return;
  128. }
  129. function getSelectionAfterBreakWrap(editor: Editor) {
  130. const selection = editor.selection;
  131. if (!selection) return;
  132. const start = Range.start(selection);
  133. const cursor = { path: start.path, offset: start.offset + 1 } as SelectionPoint;
  134. const newSelection = {
  135. anchor: clonePoint(cursor),
  136. focus: clonePoint(cursor),
  137. } as TextSelection;
  138. return newSelection;
  139. }