@crystallize/design-system 1.3.2 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/TableComponent-I2YOOYOU.css +281 -0
  3. package/dist/TableComponent-QINOO453.mjs +1377 -0
  4. package/dist/arrow-clockwise-Z2G6UEGP.svg +1 -0
  5. package/dist/arrow-counterclockwise-2O5EYVJT.svg +1 -0
  6. package/dist/bg-color-HB2WDYGO.svg +1 -0
  7. package/dist/camera-CR7D2PNH.svg +1 -0
  8. package/dist/caret-right-fill-FFBNEXVX.svg +1 -0
  9. package/dist/chat-square-quote-CI6PUJHH.svg +1 -0
  10. package/dist/chevron-down-3FRWSIKS.svg +1 -0
  11. package/dist/chunk-VUXQZRSP.mjs +737 -0
  12. package/dist/clipboard-OSEFDF25.svg +1 -0
  13. package/dist/close-FH57ZMJF.svg +1 -0
  14. package/dist/code-SEVR6TIQ.svg +1 -0
  15. package/dist/copy-DMGDODUL.svg +1 -0
  16. package/dist/diagram-2-CEJUD2B4.svg +1 -0
  17. package/dist/download-JXUGIUCX.svg +1 -0
  18. package/dist/draggable-block-menu-KKHDNKJA.svg +1 -0
  19. package/dist/dropdown-more-BHZ5COKX.svg +1 -0
  20. package/dist/file-image-TIQPFJX4.svg +1 -0
  21. package/dist/filetype-gif-OG2BEYYK.svg +1 -0
  22. package/dist/font-color-J4GA3ZJO.svg +1 -0
  23. package/dist/font-family-ZU5N6TTE.svg +1 -0
  24. package/dist/gear-ICMT4NTP.svg +1 -0
  25. package/dist/horizontal-rule-N6RD2V7H.svg +1 -0
  26. package/dist/indent-MJ6JIMCK.svg +1 -0
  27. package/dist/index.css +2711 -315
  28. package/dist/index.d.ts +145 -40
  29. package/dist/index.js +10376 -1481
  30. package/dist/index.mjs +7609 -746
  31. package/dist/journal-code-XUT44HDV.svg +1 -0
  32. package/dist/justify-J7X5JEEX.svg +1 -0
  33. package/dist/link-W52N4JKZ.svg +1 -0
  34. package/dist/list-ol-2ZEUN4Z7.svg +1 -0
  35. package/dist/list-ul-DVKNUP47.svg +1 -0
  36. package/dist/lock-WCYOZOHW.svg +1 -0
  37. package/dist/lock-fill-JZSKOSHK.svg +1 -0
  38. package/dist/markdown-4BGQNLLT.svg +1 -0
  39. package/dist/mic-H5FNOMM7.svg +1 -0
  40. package/dist/outdent-2LUMUMIP.svg +1 -0
  41. package/dist/paint-bucket-VCISMZTH.svg +1 -0
  42. package/dist/palette-SWGFPRWZ.svg +1 -0
  43. package/dist/pencil-fill-STFSC26F.svg +1 -0
  44. package/dist/plug-HGGGEVS3.svg +1 -0
  45. package/dist/plug-fill-OTG3U4TN.svg +1 -0
  46. package/dist/plus-CQISIKEC.svg +1 -0
  47. package/dist/plus-slash-minus-N22JU4TI.svg +1 -0
  48. package/dist/prettier-WUJ7B5NV.svg +1 -0
  49. package/dist/prettier-error-DYJSLYDP.svg +1 -0
  50. package/dist/square-check-UTG6FU6D.svg +1 -0
  51. package/dist/success-YVXUMPEZ.svg +1 -0
  52. package/dist/table-BR6DI4ZQ.svg +1 -0
  53. package/dist/text-center-UQI6PAEF.svg +1 -0
  54. package/dist/text-left-KT2B6TR3.svg +1 -0
  55. package/dist/text-paragraph-MFTUIIQG.svg +1 -0
  56. package/dist/text-right-SKELPISG.svg +1 -0
  57. package/dist/trash-UOM6D7TD.svg +1 -0
  58. package/dist/type-bold-PY7COC3N.svg +1 -0
  59. package/dist/type-h1-6KJP7YOM.svg +1 -0
  60. package/dist/type-h2-VHI2USC3.svg +1 -0
  61. package/dist/type-h3-JIU77CHO.svg +1 -0
  62. package/dist/type-h4-P5EHKDAL.svg +1 -0
  63. package/dist/type-h5-CS2KYVRG.svg +1 -0
  64. package/dist/type-h6-J2O74LJZ.svg +1 -0
  65. package/dist/type-italic-3DSFOSG2.svg +1 -0
  66. package/dist/type-strikethrough-E2KKQFSX.svg +1 -0
  67. package/dist/type-subscript-BMPTRIBU.svg +1 -0
  68. package/dist/type-superscript-EDF6EPAA.svg +1 -0
  69. package/dist/type-underline-CBFA5VLF.svg +1 -0
  70. package/dist/upload-Q6KICGZW.svg +1 -0
  71. package/dist/user-EOI2NEFZ.svg +1 -0
  72. package/package.json +30 -6
  73. package/src/dialog/dialog.tsx +1 -0
  74. package/src/icon-button/icon-button.css +16 -14
  75. package/src/index.ts +4 -4
  76. package/src/input/input.css +1 -1
  77. package/src/input-with-label/input-with-label.css +1 -1
  78. package/src/rich-text-editor/appSettings.ts +28 -0
  79. package/src/rich-text-editor/context/SettingsContext.tsx +71 -0
  80. package/src/rich-text-editor/context/SharedAutocompleteContext.tsx +60 -0
  81. package/src/rich-text-editor/context/SharedHistoryContext.tsx +25 -0
  82. package/src/rich-text-editor/hooks/useReport.ts +64 -0
  83. package/src/rich-text-editor/images/cat-typing.gif +0 -0
  84. package/src/rich-text-editor/images/emoji/1F600.png +0 -0
  85. package/src/rich-text-editor/images/emoji/1F641.png +0 -0
  86. package/src/rich-text-editor/images/emoji/1F642.png +0 -0
  87. package/src/rich-text-editor/images/emoji/2764.png +0 -0
  88. package/src/rich-text-editor/images/emoji/LICENSE.md +5 -0
  89. package/src/rich-text-editor/images/icons/LICENSE.md +5 -0
  90. package/src/rich-text-editor/images/icons/arrow-clockwise.svg +1 -0
  91. package/src/rich-text-editor/images/icons/arrow-counterclockwise.svg +1 -0
  92. package/src/rich-text-editor/images/icons/bg-color.svg +1 -0
  93. package/src/rich-text-editor/images/icons/camera.svg +1 -0
  94. package/src/rich-text-editor/images/icons/card-checklist.svg +1 -0
  95. package/src/rich-text-editor/images/icons/caret-right-fill.svg +1 -0
  96. package/src/rich-text-editor/images/icons/chat-left-text.svg +1 -0
  97. package/src/rich-text-editor/images/icons/chat-right-dots.svg +1 -0
  98. package/src/rich-text-editor/images/icons/chat-right-text.svg +1 -0
  99. package/src/rich-text-editor/images/icons/chat-right.svg +1 -0
  100. package/src/rich-text-editor/images/icons/chat-square-quote.svg +1 -0
  101. package/src/rich-text-editor/images/icons/chevron-down.svg +1 -0
  102. package/src/rich-text-editor/images/icons/clipboard.svg +1 -0
  103. package/src/rich-text-editor/images/icons/close.svg +1 -0
  104. package/src/rich-text-editor/images/icons/code.svg +1 -0
  105. package/src/rich-text-editor/images/icons/comments.svg +1 -0
  106. package/src/rich-text-editor/images/icons/copy.svg +1 -0
  107. package/src/rich-text-editor/images/icons/diagram-2.svg +1 -0
  108. package/src/rich-text-editor/images/icons/download.svg +1 -0
  109. package/src/rich-text-editor/images/icons/draggable-block-menu.svg +1 -0
  110. package/src/rich-text-editor/images/icons/dropdown-more.svg +1 -0
  111. package/src/rich-text-editor/images/icons/figma.svg +1 -0
  112. package/src/rich-text-editor/images/icons/file-image.svg +1 -0
  113. package/src/rich-text-editor/images/icons/filetype-gif.svg +1 -0
  114. package/src/rich-text-editor/images/icons/font-color.svg +1 -0
  115. package/src/rich-text-editor/images/icons/font-family.svg +1 -0
  116. package/src/rich-text-editor/images/icons/gear.svg +1 -0
  117. package/src/rich-text-editor/images/icons/horizontal-rule.svg +1 -0
  118. package/src/rich-text-editor/images/icons/indent.svg +1 -0
  119. package/src/rich-text-editor/images/icons/journal-code.svg +1 -0
  120. package/src/rich-text-editor/images/icons/journal-text.svg +1 -0
  121. package/src/rich-text-editor/images/icons/justify.svg +1 -0
  122. package/src/rich-text-editor/images/icons/link.svg +1 -0
  123. package/src/rich-text-editor/images/icons/list-ol.svg +1 -0
  124. package/src/rich-text-editor/images/icons/list-ul.svg +1 -0
  125. package/src/rich-text-editor/images/icons/lock-fill.svg +1 -0
  126. package/src/rich-text-editor/images/icons/lock.svg +1 -0
  127. package/src/rich-text-editor/images/icons/markdown.svg +1 -0
  128. package/src/rich-text-editor/images/icons/mic.svg +1 -0
  129. package/src/rich-text-editor/images/icons/outdent.svg +1 -0
  130. package/src/rich-text-editor/images/icons/paint-bucket.svg +1 -0
  131. package/src/rich-text-editor/images/icons/palette.svg +1 -0
  132. package/src/rich-text-editor/images/icons/pencil-fill.svg +1 -0
  133. package/src/rich-text-editor/images/icons/plug-fill.svg +1 -0
  134. package/src/rich-text-editor/images/icons/plug.svg +1 -0
  135. package/src/rich-text-editor/images/icons/plus-slash-minus.svg +1 -0
  136. package/src/rich-text-editor/images/icons/plus.svg +1 -0
  137. package/src/rich-text-editor/images/icons/prettier-error.svg +1 -0
  138. package/src/rich-text-editor/images/icons/prettier.svg +1 -0
  139. package/src/rich-text-editor/images/icons/send.svg +1 -0
  140. package/src/rich-text-editor/images/icons/square-check.svg +1 -0
  141. package/src/rich-text-editor/images/icons/sticky.svg +1 -0
  142. package/src/rich-text-editor/images/icons/success.svg +1 -0
  143. package/src/rich-text-editor/images/icons/table.svg +1 -0
  144. package/src/rich-text-editor/images/icons/text-center.svg +1 -0
  145. package/src/rich-text-editor/images/icons/text-left.svg +1 -0
  146. package/src/rich-text-editor/images/icons/text-paragraph.svg +1 -0
  147. package/src/rich-text-editor/images/icons/text-right.svg +1 -0
  148. package/src/rich-text-editor/images/icons/trash.svg +1 -0
  149. package/src/rich-text-editor/images/icons/trash3.svg +1 -0
  150. package/src/rich-text-editor/images/icons/tweet.svg +1 -0
  151. package/src/rich-text-editor/images/icons/type-bold.svg +1 -0
  152. package/src/rich-text-editor/images/icons/type-h1.svg +1 -0
  153. package/src/rich-text-editor/images/icons/type-h2.svg +1 -0
  154. package/src/rich-text-editor/images/icons/type-h3.svg +1 -0
  155. package/src/rich-text-editor/images/icons/type-h4.svg +1 -0
  156. package/src/rich-text-editor/images/icons/type-h5.svg +1 -0
  157. package/src/rich-text-editor/images/icons/type-h6.svg +1 -0
  158. package/src/rich-text-editor/images/icons/type-italic.svg +1 -0
  159. package/src/rich-text-editor/images/icons/type-strikethrough.svg +1 -0
  160. package/src/rich-text-editor/images/icons/type-subscript.svg +1 -0
  161. package/src/rich-text-editor/images/icons/type-superscript.svg +1 -0
  162. package/src/rich-text-editor/images/icons/type-underline.svg +1 -0
  163. package/src/rich-text-editor/images/icons/upload.svg +1 -0
  164. package/src/rich-text-editor/images/icons/user.svg +1 -0
  165. package/src/rich-text-editor/images/icons/youtube.svg +1 -0
  166. package/src/rich-text-editor/images/image/LICENSE.md +5 -0
  167. package/src/rich-text-editor/images/landscape.jpg +0 -0
  168. package/src/rich-text-editor/images/logo.svg +1 -0
  169. package/src/rich-text-editor/images/yellow-flower-small.jpg +0 -0
  170. package/src/rich-text-editor/images/yellow-flower.jpg +0 -0
  171. package/src/rich-text-editor/index.ts +1 -0
  172. package/src/rich-text-editor/model/crystallize-rich-text-types/code.ts +39 -0
  173. package/src/rich-text-editor/model/crystallize-rich-text-types/headings.ts +12 -0
  174. package/src/rich-text-editor/model/crystallize-rich-text-types/index.ts +69 -0
  175. package/src/rich-text-editor/model/crystallize-rich-text-types/link.ts +9 -0
  176. package/src/rich-text-editor/model/crystallize-rich-text-types/table.ts +16 -0
  177. package/src/rich-text-editor/model/crystallize-to-lexical.ts +186 -0
  178. package/src/rich-text-editor/model/lexical-to-crystallize.ts +232 -0
  179. package/src/rich-text-editor/nodes/AutocompleteNode.tsx +96 -0
  180. package/src/rich-text-editor/nodes/BaseNodes.ts +45 -0
  181. package/src/rich-text-editor/nodes/KeywordNode.ts +73 -0
  182. package/src/rich-text-editor/nodes/TableCellNodes.ts +31 -0
  183. package/src/rich-text-editor/nodes/TableComponent.tsx +1547 -0
  184. package/src/rich-text-editor/nodes/TableNode.tsx +398 -0
  185. package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +83 -0
  186. package/src/rich-text-editor/plugins/AutoLinkPlugin/index.tsx +47 -0
  187. package/src/rich-text-editor/plugins/AutocompletePlugin/index.tsx +2536 -0
  188. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +60 -0
  189. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.css +14 -0
  190. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +140 -0
  191. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.css +46 -0
  192. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +155 -0
  193. package/src/rich-text-editor/plugins/CodeHighlightPlugin/index.ts +21 -0
  194. package/src/rich-text-editor/plugins/ComponentPickerPlugin/index.tsx +320 -0
  195. package/src/rich-text-editor/plugins/DragDropPastePlugin/index.ts +40 -0
  196. package/src/rich-text-editor/plugins/DraggableBlockPlugin/index.css +36 -0
  197. package/src/rich-text-editor/plugins/DraggableBlockPlugin/index.tsx +368 -0
  198. package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.css +40 -0
  199. package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.tsx +305 -0
  200. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.css +128 -0
  201. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +351 -0
  202. package/src/rich-text-editor/plugins/LinkPlugin/index.tsx +16 -0
  203. package/src/rich-text-editor/plugins/ListMaxIndentLevelPlugin/index.ts +86 -0
  204. package/src/rich-text-editor/plugins/MarkdownShortcutPlugin/index.tsx +16 -0
  205. package/src/rich-text-editor/plugins/MarkdownTransformers/index.ts +195 -0
  206. package/src/rich-text-editor/plugins/MaxLengthPlugin/index.tsx +49 -0
  207. package/src/rich-text-editor/plugins/SpeechToTextPlugin/index.ts +113 -0
  208. package/src/rich-text-editor/plugins/TabFocusPlugin/index.tsx +65 -0
  209. package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +481 -0
  210. package/src/rich-text-editor/plugins/TableCellResizer/index.css +12 -0
  211. package/src/rich-text-editor/plugins/TableCellResizer/index.tsx +386 -0
  212. package/src/rich-text-editor/plugins/TablePlugin.tsx +190 -0
  213. package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +726 -0
  214. package/src/rich-text-editor/plugins/TreeViewPlugin/index.tsx +25 -0
  215. package/src/rich-text-editor/plugins/TypingPerfPlugin/index.ts +117 -0
  216. package/src/rich-text-editor/rich-text-editor.css +1396 -0
  217. package/src/rich-text-editor/rich-text-editor.stories.tsx +385 -0
  218. package/src/rich-text-editor/rich-text-editor.tsx +228 -0
  219. package/src/rich-text-editor/tests/rich-text-editor-basic-rendering.test.tsx +47 -0
  220. package/src/rich-text-editor/tests/rich-text-editor-code.test.tsx +39 -0
  221. package/src/rich-text-editor/tests/rich-text-editor-model-basics.test.tsx +56 -0
  222. package/src/rich-text-editor/tests/rich-text-editor-model-conversions.test.tsx +195 -0
  223. package/src/rich-text-editor/tests/rich-text-editor-onchange.test.tsx +37 -0
  224. package/src/rich-text-editor/tests/rich-text-editor-quote.test.tsx +36 -0
  225. package/src/rich-text-editor/tests/rich-text-editor-text-formats.test.tsx +135 -0
  226. package/src/rich-text-editor/tests/rich-text-editor-typing.test.tsx +73 -0
  227. package/src/rich-text-editor/tests/utils.ts +23 -0
  228. package/src/rich-text-editor/themes/PlaygroundEditorTheme.css +433 -0
  229. package/src/rich-text-editor/themes/PlaygroundEditorTheme.ts +113 -0
  230. package/src/rich-text-editor/types.ts +5 -0
  231. package/src/rich-text-editor/ui/ContentEditable.css +13 -0
  232. package/src/rich-text-editor/ui/ContentEditable.tsx +15 -0
  233. package/src/rich-text-editor/ui/LinkPreview.css +57 -0
  234. package/src/rich-text-editor/ui/LinkPreview.tsx +169 -0
  235. package/src/rich-text-editor/utils/environment.ts +1 -0
  236. package/src/rich-text-editor/utils/getDOMRangeRect.ts +42 -0
  237. package/src/rich-text-editor/utils/getSelectedNode.ts +27 -0
  238. package/src/rich-text-editor/utils/guard.ts +10 -0
  239. package/src/rich-text-editor/utils/isMobileWidth.ts +7 -0
  240. package/src/rich-text-editor/utils/joinClasses.ts +13 -0
  241. package/src/rich-text-editor/utils/point.ts +55 -0
  242. package/src/rich-text-editor/utils/rect.ts +158 -0
  243. package/src/rich-text-editor/utils/setFloatingElemPosition.ts +46 -0
  244. package/src/rich-text-editor/utils/swipe.ts +127 -0
  245. package/src/rich-text-editor/utils/url.ts +33 -0
  246. package/src/Tokens.stories.tsx +0 -18
@@ -0,0 +1,1547 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type { RangeSelection, TextFormatType } from 'lexical';
10
+
11
+ import {
12
+ $generateJSONFromSelectedNodes,
13
+ $generateNodesFromSerializedNodes,
14
+ $insertGeneratedNodes,
15
+ } from '@lexical/clipboard';
16
+ import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html';
17
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
18
+ import { LexicalNestedComposer } from '@lexical/react/LexicalNestedComposer';
19
+ import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
20
+ import { mergeRegister } from '@lexical/utils';
21
+ import {
22
+ $addUpdateTag,
23
+ $createParagraphNode,
24
+ $createRangeSelection,
25
+ $getNodeByKey,
26
+ $getRoot,
27
+ $getSelection,
28
+ $isNodeSelection,
29
+ $isRangeSelection,
30
+ CLICK_COMMAND,
31
+ COMMAND_PRIORITY_LOW,
32
+ COPY_COMMAND,
33
+ createEditor,
34
+ CUT_COMMAND,
35
+ EditorThemeClasses,
36
+ FORMAT_TEXT_COMMAND,
37
+ KEY_ARROW_DOWN_COMMAND,
38
+ KEY_ARROW_LEFT_COMMAND,
39
+ KEY_ARROW_RIGHT_COMMAND,
40
+ KEY_ARROW_UP_COMMAND,
41
+ KEY_BACKSPACE_COMMAND,
42
+ KEY_DELETE_COMMAND,
43
+ KEY_ENTER_COMMAND,
44
+ KEY_ESCAPE_COMMAND,
45
+ KEY_TAB_COMMAND,
46
+ LexicalEditor,
47
+ NodeKey,
48
+ PASTE_COMMAND,
49
+ } from 'lexical';
50
+ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
51
+ import * as React from 'react';
52
+ import { createPortal } from 'react-dom';
53
+
54
+ import { CellContext } from '../plugins/TablePlugin';
55
+ import {
56
+ $isTableNode,
57
+ Cell,
58
+ cellHTMLCache,
59
+ cellTextContentCache,
60
+ createRow,
61
+ createUID,
62
+ exportTableCellsToHTML,
63
+ extractRowsFromHTML,
64
+ Rows,
65
+ TableNode,
66
+ } from './TableNode';
67
+ import { IS_APPLE } from '../utils/environment';
68
+
69
+ type SortOptions = { type: 'ascending' | 'descending'; x: number };
70
+
71
+ const NO_CELLS: [] = [];
72
+
73
+ function $createSelectAll(): RangeSelection {
74
+ const sel = $createRangeSelection();
75
+ sel.focus.set('root', $getRoot().getChildrenSize(), 'element');
76
+ return sel;
77
+ }
78
+
79
+ function createEmptyParagraphHTML(theme: EditorThemeClasses): string {
80
+ return `<p class="${theme.paragraph}"><br></p>`;
81
+ }
82
+
83
+ function focusCell(tableElem: HTMLElement, id: string): void {
84
+ const cellElem = tableElem.querySelector(`[data-id=${id}]`) as HTMLElement;
85
+ if (cellElem == null) {
86
+ return;
87
+ }
88
+ cellElem.focus();
89
+ }
90
+
91
+ function isStartingResize(target: HTMLElement): boolean {
92
+ return target.nodeType === 1 && target.hasAttribute('data-table-resize');
93
+ }
94
+
95
+ function generateHTMLFromJSON(editorStateJSON: string, cellEditor: LexicalEditor): string {
96
+ const editorState = cellEditor.parseEditorState(editorStateJSON);
97
+ let html = cellHTMLCache.get(editorStateJSON);
98
+ if (html === undefined) {
99
+ html = editorState.read(() => $generateHtmlFromNodes(cellEditor, null));
100
+ const textContent = editorState.read(() => $getRoot().getTextContent());
101
+ cellHTMLCache.set(editorStateJSON, html);
102
+ cellTextContentCache.set(editorStateJSON, textContent);
103
+ }
104
+ return html;
105
+ }
106
+
107
+ function getCurrentDocument(editor: LexicalEditor): Document {
108
+ const rootElement = editor.getRootElement();
109
+ return rootElement !== null ? rootElement.ownerDocument : document;
110
+ }
111
+
112
+ function isCopy(keyCode: number, shiftKey: boolean, metaKey: boolean, ctrlKey: boolean): boolean {
113
+ if (shiftKey) {
114
+ return false;
115
+ }
116
+ if (keyCode === 67) {
117
+ return IS_APPLE ? metaKey : ctrlKey;
118
+ }
119
+
120
+ return false;
121
+ }
122
+
123
+ function isCut(keyCode: number, shiftKey: boolean, metaKey: boolean, ctrlKey: boolean): boolean {
124
+ if (shiftKey) {
125
+ return false;
126
+ }
127
+ if (keyCode === 88) {
128
+ return IS_APPLE ? metaKey : ctrlKey;
129
+ }
130
+
131
+ return false;
132
+ }
133
+
134
+ function isPaste(keyCode: number, shiftKey: boolean, metaKey: boolean, ctrlKey: boolean): boolean {
135
+ if (shiftKey) {
136
+ return false;
137
+ }
138
+ if (keyCode === 86) {
139
+ return IS_APPLE ? metaKey : ctrlKey;
140
+ }
141
+
142
+ return false;
143
+ }
144
+
145
+ function getCellID(domElement: HTMLElement): null | string {
146
+ let node: null | HTMLElement = domElement;
147
+ while (node !== null) {
148
+ const possibleID = node.getAttribute('data-id');
149
+ if (possibleID != null) {
150
+ return possibleID;
151
+ }
152
+ node = node.parentElement;
153
+ }
154
+ return null;
155
+ }
156
+
157
+ function getTableCellWidth(domElement: HTMLElement): number {
158
+ let node: null | HTMLElement = domElement;
159
+ while (node !== null) {
160
+ if (node.nodeName === 'TH' || node.nodeName === 'TD') {
161
+ return node.getBoundingClientRect().width;
162
+ }
163
+ node = node.parentElement;
164
+ }
165
+ return 0;
166
+ }
167
+
168
+ function $updateCells(
169
+ rows: Rows,
170
+ ids: Array<string>,
171
+ cellCoordMap: Map<string, [number, number]>,
172
+ cellEditor: null | LexicalEditor,
173
+ updateTableNode: (fn2: (tableNode: TableNode) => void) => void,
174
+ fn: () => void,
175
+ ): void {
176
+ for (const id of ids) {
177
+ const cell = getCell(rows, id, cellCoordMap);
178
+ if (cell !== null && cellEditor !== null) {
179
+ const editorState = cellEditor.parseEditorState(cell.json);
180
+ cellEditor._headless = true;
181
+ cellEditor.setEditorState(editorState);
182
+ cellEditor.update(fn, { discrete: true });
183
+ cellEditor._headless = false;
184
+ const newJSON = JSON.stringify(cellEditor.getEditorState());
185
+ updateTableNode(tableNode => {
186
+ const [x, y] = cellCoordMap.get(id) as [number, number];
187
+ $addUpdateTag('history-push');
188
+ tableNode.updateCellJSON(x, y, newJSON);
189
+ });
190
+ }
191
+ }
192
+ }
193
+
194
+ function isTargetOnPossibleUIControl(target: HTMLElement): boolean {
195
+ let node: HTMLElement | null = target;
196
+ while (node !== null) {
197
+ const nodeName = node.nodeName;
198
+ if (nodeName === 'BUTTON' || nodeName === 'INPUT' || nodeName === 'TEXTAREA') {
199
+ return true;
200
+ }
201
+ node = node.parentElement;
202
+ }
203
+ return false;
204
+ }
205
+
206
+ function getSelectedRect(
207
+ startID: string,
208
+ endID: string,
209
+ cellCoordMap: Map<string, [number, number]>,
210
+ ): null | { startX: number; endX: number; startY: number; endY: number } {
211
+ const startCoords = cellCoordMap.get(startID);
212
+ const endCoords = cellCoordMap.get(endID);
213
+ if (startCoords === undefined || endCoords === undefined) {
214
+ return null;
215
+ }
216
+ const startX = Math.min(startCoords[0], endCoords[0]);
217
+ const endX = Math.max(startCoords[0], endCoords[0]);
218
+ const startY = Math.min(startCoords[1], endCoords[1]);
219
+ const endY = Math.max(startCoords[1], endCoords[1]);
220
+
221
+ return {
222
+ endX,
223
+ endY,
224
+ startX,
225
+ startY,
226
+ };
227
+ }
228
+
229
+ function getSelectedIDs(
230
+ rows: Rows,
231
+ startID: string,
232
+ endID: string,
233
+ cellCoordMap: Map<string, [number, number]>,
234
+ ): Array<string> {
235
+ const rect = getSelectedRect(startID, endID, cellCoordMap);
236
+ if (rect === null) {
237
+ return [];
238
+ }
239
+ const { startX, endY, endX, startY } = rect;
240
+ const ids = [];
241
+
242
+ for (let x = startX; x <= endX; x++) {
243
+ for (let y = startY; y <= endY; y++) {
244
+ ids.push(rows[y].cells[x].id);
245
+ }
246
+ }
247
+ return ids;
248
+ }
249
+
250
+ function extractCellsFromRows(rows: Rows, rect: { startX: number; endX: number; startY: number; endY: number }): Rows {
251
+ const { startX, endY, endX, startY } = rect;
252
+ const newRows: Rows = [];
253
+
254
+ for (let y = startY; y <= endY; y++) {
255
+ const row = rows[y];
256
+ const newRow = createRow();
257
+ for (let x = startX; x <= endX; x++) {
258
+ const cellClone = { ...row.cells[x] };
259
+ cellClone.id = createUID();
260
+ newRow.cells.push(cellClone);
261
+ }
262
+ newRows.push(newRow);
263
+ }
264
+ return newRows;
265
+ }
266
+
267
+ function TableCellEditor({ cellEditor }: { cellEditor: LexicalEditor }) {
268
+ const { cellEditorConfig, cellEditorPlugins } = useContext(CellContext);
269
+
270
+ if (cellEditorPlugins === null || cellEditorConfig === null) {
271
+ return null;
272
+ }
273
+
274
+ return (
275
+ <LexicalNestedComposer
276
+ initialEditor={cellEditor}
277
+ initialTheme={cellEditorConfig.theme}
278
+ initialNodes={cellEditorConfig.nodes}
279
+ skipCollabChecks={true}
280
+ >
281
+ {cellEditorPlugins}
282
+ </LexicalNestedComposer>
283
+ );
284
+ }
285
+
286
+ function getCell(rows: Rows, cellID: string, cellCoordMap: Map<string, [number, number]>): null | Cell {
287
+ const coords = cellCoordMap.get(cellID);
288
+ if (coords === undefined) {
289
+ return null;
290
+ }
291
+ const [x, y] = coords;
292
+ const row = rows[y];
293
+ return row.cells[x];
294
+ }
295
+
296
+ function TableActionMenu({
297
+ cell,
298
+ rows,
299
+ cellCoordMap,
300
+ menuElem,
301
+ updateCellsByID,
302
+ onClose,
303
+ updateTableNode,
304
+ setSortingOptions,
305
+ sortingOptions,
306
+ }: {
307
+ cell: Cell;
308
+ menuElem: HTMLElement;
309
+ updateCellsByID: (ids: Array<string>, fn: () => void) => void;
310
+ onClose: () => void;
311
+ updateTableNode: (fn2: (tableNode: TableNode) => void) => void;
312
+ cellCoordMap: Map<string, [number, number]>;
313
+ rows: Rows;
314
+ setSortingOptions: (options: null | SortOptions) => void;
315
+ sortingOptions: null | SortOptions;
316
+ }) {
317
+ const dropDownRef = useRef<null | HTMLDivElement>(null);
318
+
319
+ useEffect(() => {
320
+ const dropdownElem = dropDownRef.current;
321
+ if (dropdownElem !== null) {
322
+ const rect = menuElem.getBoundingClientRect();
323
+ dropdownElem.style.top = `${rect.y}px`;
324
+ dropdownElem.style.left = `${rect.x}px`;
325
+ }
326
+ }, [menuElem]);
327
+
328
+ useEffect(() => {
329
+ const handleClickOutside = (event: MouseEvent) => {
330
+ const dropdownElem = dropDownRef.current;
331
+ if (dropdownElem !== null && !dropdownElem.contains(event.target as Node)) {
332
+ event.stopPropagation();
333
+ }
334
+ };
335
+
336
+ window.addEventListener('click', handleClickOutside);
337
+ return () => window.removeEventListener('click', handleClickOutside);
338
+ }, [onClose]);
339
+ const coords = cellCoordMap.get(cell.id);
340
+
341
+ if (coords === undefined) {
342
+ return null;
343
+ }
344
+ const [x, y] = coords;
345
+
346
+ return (
347
+ <div
348
+ className="dropdown"
349
+ ref={dropDownRef}
350
+ onPointerMove={e => {
351
+ e.stopPropagation();
352
+ }}
353
+ onPointerDown={e => {
354
+ e.stopPropagation();
355
+ }}
356
+ onPointerUp={e => {
357
+ e.stopPropagation();
358
+ }}
359
+ onClick={e => {
360
+ e.stopPropagation();
361
+ }}
362
+ >
363
+ <button
364
+ className="item"
365
+ onClick={() => {
366
+ updateTableNode(tableNode => {
367
+ $addUpdateTag('history-push');
368
+ tableNode.updateCellType(x, y, cell.type === 'normal' ? 'header' : 'normal');
369
+ });
370
+ onClose();
371
+ }}
372
+ >
373
+ <span className="text">{cell.type === 'normal' ? 'Make header' : 'Remove header'}</span>
374
+ </button>
375
+ <button
376
+ className="item"
377
+ onClick={() => {
378
+ updateCellsByID([cell.id], () => {
379
+ const root = $getRoot();
380
+ root.clear();
381
+ root.append($createParagraphNode());
382
+ });
383
+ onClose();
384
+ }}
385
+ >
386
+ <span className="text">Clear cell</span>
387
+ </button>
388
+ <hr />
389
+ {cell.type === 'header' && y === 0 && (
390
+ <>
391
+ {sortingOptions !== null && sortingOptions.x === x && (
392
+ <button
393
+ className="item"
394
+ onClick={() => {
395
+ setSortingOptions(null);
396
+ onClose();
397
+ }}
398
+ >
399
+ <span className="text">Remove sorting</span>
400
+ </button>
401
+ )}
402
+ {(sortingOptions === null || sortingOptions.x !== x || sortingOptions.type === 'descending') && (
403
+ <button
404
+ className="item"
405
+ onClick={() => {
406
+ setSortingOptions({ type: 'ascending', x });
407
+ onClose();
408
+ }}
409
+ >
410
+ <span className="text">Sort ascending</span>
411
+ </button>
412
+ )}
413
+ {(sortingOptions === null || sortingOptions.x !== x || sortingOptions.type === 'ascending') && (
414
+ <button
415
+ className="item"
416
+ onClick={() => {
417
+ setSortingOptions({ type: 'descending', x });
418
+ onClose();
419
+ }}
420
+ >
421
+ <span className="text">Sort descending</span>
422
+ </button>
423
+ )}
424
+ <hr />
425
+ </>
426
+ )}
427
+ <button
428
+ className="item"
429
+ onClick={() => {
430
+ updateTableNode(tableNode => {
431
+ $addUpdateTag('history-push');
432
+ tableNode.insertRowAt(y);
433
+ });
434
+ onClose();
435
+ }}
436
+ >
437
+ <span className="text">Insert row above</span>
438
+ </button>
439
+ <button
440
+ className="item"
441
+ onClick={() => {
442
+ updateTableNode(tableNode => {
443
+ $addUpdateTag('history-push');
444
+ tableNode.insertRowAt(y + 1);
445
+ });
446
+ onClose();
447
+ }}
448
+ >
449
+ <span className="text">Insert row below</span>
450
+ </button>
451
+ <hr />
452
+ <button
453
+ className="item"
454
+ onClick={() => {
455
+ updateTableNode(tableNode => {
456
+ $addUpdateTag('history-push');
457
+ tableNode.insertColumnAt(x);
458
+ });
459
+ onClose();
460
+ }}
461
+ >
462
+ <span className="text">Insert column left</span>
463
+ </button>
464
+ <button
465
+ className="item"
466
+ onClick={() => {
467
+ updateTableNode(tableNode => {
468
+ $addUpdateTag('history-push');
469
+ tableNode.insertColumnAt(x + 1);
470
+ });
471
+ onClose();
472
+ }}
473
+ >
474
+ <span className="text">Insert column right</span>
475
+ </button>
476
+ <hr />
477
+ {rows[0].cells.length !== 1 && (
478
+ <button
479
+ className="item"
480
+ onClick={() => {
481
+ updateTableNode(tableNode => {
482
+ $addUpdateTag('history-push');
483
+ tableNode.deleteColumnAt(x);
484
+ });
485
+ onClose();
486
+ }}
487
+ >
488
+ <span className="text">Delete column</span>
489
+ </button>
490
+ )}
491
+ {rows.length !== 1 && (
492
+ <button
493
+ className="item"
494
+ onClick={() => {
495
+ updateTableNode(tableNode => {
496
+ $addUpdateTag('history-push');
497
+ tableNode.deleteRowAt(y);
498
+ });
499
+ onClose();
500
+ }}
501
+ >
502
+ <span className="text">Delete row</span>
503
+ </button>
504
+ )}
505
+ <button
506
+ className="item"
507
+ onClick={() => {
508
+ updateTableNode(tableNode => {
509
+ $addUpdateTag('history-push');
510
+ tableNode.selectNext();
511
+ tableNode.remove();
512
+ });
513
+ onClose();
514
+ }}
515
+ >
516
+ <span className="text">Delete table</span>
517
+ </button>
518
+ </div>
519
+ );
520
+ }
521
+
522
+ function TableCell({
523
+ cell,
524
+ cellCoordMap,
525
+ cellEditor,
526
+ isEditing,
527
+ isSelected,
528
+ isPrimarySelected,
529
+ theme,
530
+ updateCellsByID,
531
+ updateTableNode,
532
+ rows,
533
+ setSortingOptions,
534
+ sortingOptions,
535
+ }: {
536
+ cell: Cell;
537
+ isEditing: boolean;
538
+ isSelected: boolean;
539
+ isPrimarySelected: boolean;
540
+ theme: EditorThemeClasses;
541
+ cellEditor: LexicalEditor;
542
+ updateCellsByID: (ids: Array<string>, fn: () => void) => void;
543
+ updateTableNode: (fn2: (tableNode: TableNode) => void) => void;
544
+ cellCoordMap: Map<string, [number, number]>;
545
+ rows: Rows;
546
+ setSortingOptions: (options: null | SortOptions) => void;
547
+ sortingOptions: null | SortOptions;
548
+ }) {
549
+ const [showMenu, setShowMenu] = useState(false);
550
+ const menuRootRef = useRef(null);
551
+ const isHeader = cell.type !== 'normal';
552
+ const editorStateJSON = cell.json;
553
+ const CellComponent = isHeader ? 'th' : 'td';
554
+ const cellWidth = cell.width;
555
+ const menuElem = menuRootRef.current;
556
+ const coords = cellCoordMap.get(cell.id);
557
+ const isSorted = sortingOptions !== null && coords !== undefined && coords[0] === sortingOptions.x && coords[1] === 0;
558
+
559
+ useEffect(() => {
560
+ if (isEditing || !isPrimarySelected) {
561
+ setShowMenu(false);
562
+ }
563
+ }, [isEditing, isPrimarySelected]);
564
+
565
+ return (
566
+ <CellComponent
567
+ className={`${theme.tableCell} ${isHeader ? theme.tableCellHeader : ''} ${
568
+ isSelected ? theme.tableCellSelected : ''
569
+ }`}
570
+ data-id={cell.id}
571
+ tabIndex={-1}
572
+ style={{ width: cellWidth !== null ? cellWidth : undefined }}
573
+ >
574
+ {isPrimarySelected && (
575
+ <div className={`${theme.tableCellPrimarySelected} ${isEditing ? theme.tableCellEditing : ''}`} />
576
+ )}
577
+ {isPrimarySelected && isEditing ? (
578
+ <TableCellEditor cellEditor={cellEditor} />
579
+ ) : (
580
+ <>
581
+ <div
582
+ dangerouslySetInnerHTML={{
583
+ __html:
584
+ editorStateJSON === ''
585
+ ? createEmptyParagraphHTML(theme)
586
+ : generateHTMLFromJSON(editorStateJSON, cellEditor),
587
+ }}
588
+ />
589
+ <div className={theme.tableCellResizer} data-table-resize="true" />
590
+ </>
591
+ )}
592
+ {isPrimarySelected && !isEditing && (
593
+ <div className={theme.tableCellActionButtonContainer} ref={menuRootRef}>
594
+ <button
595
+ className={theme.tableCellActionButton}
596
+ onClick={e => {
597
+ setShowMenu(!showMenu);
598
+ e.stopPropagation();
599
+ }}
600
+ >
601
+ <i className="chevron-down" />
602
+ </button>
603
+ </div>
604
+ )}
605
+ {showMenu &&
606
+ menuElem !== null &&
607
+ createPortal(
608
+ <TableActionMenu
609
+ cell={cell}
610
+ menuElem={menuElem}
611
+ updateCellsByID={updateCellsByID}
612
+ onClose={() => setShowMenu(false)}
613
+ updateTableNode={updateTableNode}
614
+ cellCoordMap={cellCoordMap}
615
+ rows={rows}
616
+ setSortingOptions={setSortingOptions}
617
+ sortingOptions={sortingOptions}
618
+ />,
619
+ document.body,
620
+ )}
621
+ {isSorted && <div className={theme.tableCellSortedIndicator} />}
622
+ </CellComponent>
623
+ );
624
+ }
625
+
626
+ export default function TableComponent({
627
+ nodeKey,
628
+ rows: rawRows,
629
+ theme,
630
+ }: {
631
+ nodeKey: NodeKey;
632
+ rows: Rows;
633
+ theme: EditorThemeClasses;
634
+ }) {
635
+ const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
636
+ const resizeMeasureRef = useRef<{ size: number; point: number }>({
637
+ point: 0,
638
+ size: 0,
639
+ });
640
+ const [sortingOptions, setSortingOptions] = useState<null | SortOptions>(null);
641
+ const addRowsRef = useRef(null);
642
+ const lastCellIDRef = useRef<string | null>(null);
643
+ const tableResizerRulerRef = useRef<null | HTMLDivElement>(null);
644
+ const { cellEditorConfig } = useContext(CellContext);
645
+ const [isEditing, setIsEditing] = useState(false);
646
+ const [showAddColumns, setShowAddColumns] = useState(false);
647
+ const [showAddRows, setShowAddRows] = useState(false);
648
+ const [editor] = useLexicalComposerContext();
649
+ const mouseDownRef = useRef(false);
650
+ const [resizingID, setResizingID] = useState<null | string>(null);
651
+ const tableRef = useRef<null | HTMLTableElement>(null);
652
+ const cellCoordMap = useMemo(() => {
653
+ const map = new Map();
654
+
655
+ for (let y = 0; y < rawRows.length; y++) {
656
+ const row = rawRows[y];
657
+ const cells = row.cells;
658
+ for (let x = 0; x < cells.length; x++) {
659
+ const cell = cells[x];
660
+ map.set(cell.id, [x, y]);
661
+ }
662
+ }
663
+ return map;
664
+ }, [rawRows]);
665
+ const rows = useMemo(() => {
666
+ if (sortingOptions === null) {
667
+ return rawRows;
668
+ }
669
+ const _rows = rawRows.slice(1);
670
+ _rows.sort((a, b) => {
671
+ const aCells = a.cells;
672
+ const bCells = b.cells;
673
+ const x = sortingOptions.x;
674
+ const aContent = cellTextContentCache.get(aCells[x].json) || '';
675
+ const bContent = cellTextContentCache.get(bCells[x].json) || '';
676
+ if (aContent === '' || bContent === '') {
677
+ return 1;
678
+ }
679
+ if (sortingOptions.type === 'ascending') {
680
+ return aContent.localeCompare(bContent);
681
+ }
682
+ return bContent.localeCompare(aContent);
683
+ });
684
+ _rows.unshift(rawRows[0]);
685
+ return _rows;
686
+ }, [rawRows, sortingOptions]);
687
+ const [primarySelectedCellID, setPrimarySelectedCellID] = useState<null | string>(null);
688
+ const cellEditor = useMemo<null | LexicalEditor>(() => {
689
+ if (cellEditorConfig === null) {
690
+ return null;
691
+ }
692
+ const _cellEditor = createEditor({
693
+ namespace: cellEditorConfig.namespace,
694
+ nodes: cellEditorConfig.nodes,
695
+ onError: error => cellEditorConfig.onError(error, _cellEditor),
696
+ theme: cellEditorConfig.theme,
697
+ });
698
+ return _cellEditor;
699
+ }, [cellEditorConfig]);
700
+ const [selectedCellIDs, setSelectedCellIDs] = useState<Array<string>>([]);
701
+ const selectedCellSet = useMemo<Set<string>>(() => new Set(selectedCellIDs), [selectedCellIDs]);
702
+
703
+ useEffect(() => {
704
+ const tableElem = tableRef.current;
705
+ if (isSelected && document.activeElement === document.body && tableElem !== null) {
706
+ tableElem.focus();
707
+ }
708
+ }, [isSelected]);
709
+
710
+ const updateTableNode = useCallback(
711
+ (fn: (tableNode: TableNode) => void) => {
712
+ editor.update(() => {
713
+ const tableNode = $getNodeByKey(nodeKey);
714
+ if ($isTableNode(tableNode)) {
715
+ fn(tableNode);
716
+ }
717
+ });
718
+ },
719
+ [editor, nodeKey],
720
+ );
721
+
722
+ const addColumns = () => {
723
+ updateTableNode(tableNode => {
724
+ $addUpdateTag('history-push');
725
+ tableNode.addColumns(1);
726
+ });
727
+ };
728
+
729
+ const addRows = () => {
730
+ updateTableNode(tableNode => {
731
+ $addUpdateTag('history-push');
732
+ tableNode.addRows(1);
733
+ });
734
+ };
735
+
736
+ const modifySelectedCells = useCallback(
737
+ (x: number, y: number, extend: boolean) => {
738
+ const id = rows[y].cells[x].id;
739
+ lastCellIDRef.current = id;
740
+ if (extend) {
741
+ const selectedIDs = getSelectedIDs(rows, primarySelectedCellID as string, id, cellCoordMap);
742
+ setSelectedCellIDs(selectedIDs);
743
+ } else {
744
+ setPrimarySelectedCellID(id);
745
+ setSelectedCellIDs(NO_CELLS);
746
+ focusCell(tableRef.current as HTMLElement, id);
747
+ }
748
+ },
749
+ [cellCoordMap, primarySelectedCellID, rows],
750
+ );
751
+
752
+ const saveEditorToJSON = useCallback(() => {
753
+ if (cellEditor !== null && primarySelectedCellID !== null) {
754
+ const json = JSON.stringify(cellEditor.getEditorState());
755
+ updateTableNode(tableNode => {
756
+ const coords = cellCoordMap.get(primarySelectedCellID);
757
+ if (coords === undefined) {
758
+ return;
759
+ }
760
+ $addUpdateTag('history-push');
761
+ const [x, y] = coords;
762
+ tableNode.updateCellJSON(x, y, json);
763
+ });
764
+ }
765
+ }, [cellCoordMap, cellEditor, primarySelectedCellID, updateTableNode]);
766
+
767
+ const selectTable = useCallback(() => {
768
+ setTimeout(() => {
769
+ const parentRootElement = editor.getRootElement();
770
+ if (parentRootElement !== null) {
771
+ parentRootElement.focus({ preventScroll: true });
772
+ window.getSelection()?.removeAllRanges();
773
+ }
774
+ }, 20);
775
+ }, [editor]);
776
+
777
+ useEffect(() => {
778
+ const tableElem = tableRef.current;
779
+ if (tableElem === null) {
780
+ return;
781
+ }
782
+ const doc = getCurrentDocument(editor);
783
+
784
+ const isAtEdgeOfTable = (event: PointerEvent) => {
785
+ const x = event.clientX - tableRect.x;
786
+ const y = event.clientY - tableRect.y;
787
+ return x < 5 || y < 5;
788
+ };
789
+
790
+ const handlePointerDown = (event: PointerEvent) => {
791
+ const possibleID = getCellID(event.target as HTMLElement);
792
+ if (possibleID !== null && editor.isEditable() && tableElem.contains(event.target as HTMLElement)) {
793
+ if (isAtEdgeOfTable(event)) {
794
+ setSelected(true);
795
+ setPrimarySelectedCellID(null);
796
+ selectTable();
797
+ return;
798
+ }
799
+ setSelected(false);
800
+ if (isStartingResize(event.target as HTMLElement)) {
801
+ setResizingID(possibleID);
802
+ tableElem.style.userSelect = 'none';
803
+ resizeMeasureRef.current = {
804
+ point: event.clientX,
805
+ size: getTableCellWidth(event.target as HTMLElement),
806
+ };
807
+ return;
808
+ }
809
+ mouseDownRef.current = true;
810
+ if (primarySelectedCellID !== possibleID) {
811
+ if (isEditing) {
812
+ saveEditorToJSON();
813
+ }
814
+ setPrimarySelectedCellID(possibleID);
815
+ setIsEditing(false);
816
+ lastCellIDRef.current = possibleID;
817
+ } else {
818
+ lastCellIDRef.current = null;
819
+ }
820
+ setSelectedCellIDs(NO_CELLS);
821
+ } else if (primarySelectedCellID !== null && !isTargetOnPossibleUIControl(event.target as HTMLElement)) {
822
+ setSelected(false);
823
+ mouseDownRef.current = false;
824
+ if (isEditing) {
825
+ saveEditorToJSON();
826
+ }
827
+ setPrimarySelectedCellID(null);
828
+ setSelectedCellIDs(NO_CELLS);
829
+ setIsEditing(false);
830
+ lastCellIDRef.current = null;
831
+ }
832
+ };
833
+
834
+ const tableRect = tableElem.getBoundingClientRect();
835
+
836
+ const handlePointerMove = (event: PointerEvent) => {
837
+ if (resizingID !== null) {
838
+ const tableResizerRulerElem = tableResizerRulerRef.current;
839
+ if (tableResizerRulerElem !== null) {
840
+ const { size, point } = resizeMeasureRef.current;
841
+ const diff = event.clientX - point;
842
+ const newWidth = size + diff;
843
+ let x = event.clientX - tableRect.x;
844
+ if (x < 10) {
845
+ x = 10;
846
+ } else if (x > tableRect.width - 10) {
847
+ x = tableRect.width - 10;
848
+ } else if (newWidth < 20) {
849
+ x = point - size + 20 - tableRect.x;
850
+ }
851
+ tableResizerRulerElem.style.left = `${x}px`;
852
+ }
853
+ return;
854
+ }
855
+ if (!isEditing) {
856
+ const { clientX, clientY } = event;
857
+ const { width, x, y, height } = tableRect;
858
+ const isOnRightEdge = clientX > x + width * 0.9 && clientX < x + width + 40 && !mouseDownRef.current;
859
+ setShowAddColumns(isOnRightEdge);
860
+ const isOnBottomEdge =
861
+ event.target === addRowsRef.current ||
862
+ (clientY > y + height * 0.85 && clientY < y + height + 5 && !mouseDownRef.current);
863
+ setShowAddRows(isOnBottomEdge);
864
+ }
865
+ if (isEditing || !mouseDownRef.current || primarySelectedCellID === null) {
866
+ return;
867
+ }
868
+ const possibleID = getCellID(event.target as HTMLElement);
869
+ if (possibleID !== null && possibleID !== lastCellIDRef.current) {
870
+ if (selectedCellIDs.length === 0) {
871
+ tableElem.style.userSelect = 'none';
872
+ }
873
+ const selectedIDs = getSelectedIDs(rows, primarySelectedCellID, possibleID, cellCoordMap);
874
+ if (selectedIDs.length === 1) {
875
+ setSelectedCellIDs(NO_CELLS);
876
+ } else {
877
+ setSelectedCellIDs(selectedIDs);
878
+ }
879
+ lastCellIDRef.current = possibleID;
880
+ }
881
+ };
882
+
883
+ const handlePointerUp = (event: PointerEvent) => {
884
+ if (resizingID !== null) {
885
+ const { size, point } = resizeMeasureRef.current;
886
+ const diff = event.clientX - point;
887
+ let newWidth = size + diff;
888
+ if (newWidth < 10) {
889
+ newWidth = 10;
890
+ }
891
+ updateTableNode(tableNode => {
892
+ const [x] = cellCoordMap.get(resizingID) as [number, number];
893
+ $addUpdateTag('history-push');
894
+ tableNode.updateColumnWidth(x, newWidth);
895
+ });
896
+ setResizingID(null);
897
+ }
898
+ if (tableElem !== null && selectedCellIDs.length > 1 && mouseDownRef.current) {
899
+ tableElem.style.userSelect = 'text';
900
+ window.getSelection()?.removeAllRanges();
901
+ }
902
+ mouseDownRef.current = false;
903
+ };
904
+
905
+ doc.addEventListener('pointerdown', handlePointerDown);
906
+ doc.addEventListener('pointermove', handlePointerMove);
907
+ doc.addEventListener('pointerup', handlePointerUp);
908
+
909
+ return () => {
910
+ doc.removeEventListener('pointerdown', handlePointerDown);
911
+ doc.removeEventListener('pointermove', handlePointerMove);
912
+ doc.removeEventListener('pointerup', handlePointerUp);
913
+ };
914
+ }, [
915
+ cellEditor,
916
+ editor,
917
+ isEditing,
918
+ rows,
919
+ saveEditorToJSON,
920
+ primarySelectedCellID,
921
+ selectedCellSet,
922
+ selectedCellIDs,
923
+ cellCoordMap,
924
+ resizingID,
925
+ updateTableNode,
926
+ setSelected,
927
+ selectTable,
928
+ ]);
929
+
930
+ useEffect(() => {
931
+ if (!isEditing && primarySelectedCellID !== null) {
932
+ const doc = getCurrentDocument(editor);
933
+
934
+ const loadContentIntoCell = (cell: Cell | null) => {
935
+ if (cell !== null && cellEditor !== null) {
936
+ const editorStateJSON = cell.json;
937
+ const editorState = cellEditor.parseEditorState(editorStateJSON);
938
+ cellEditor.setEditorState(editorState);
939
+ }
940
+ };
941
+
942
+ const handleDblClick = (event: MouseEvent) => {
943
+ const possibleID = getCellID(event.target as HTMLElement);
944
+ if (possibleID === primarySelectedCellID && editor.isEditable()) {
945
+ const cell = getCell(rows, possibleID, cellCoordMap);
946
+ loadContentIntoCell(cell);
947
+ setIsEditing(true);
948
+ setSelectedCellIDs(NO_CELLS);
949
+ }
950
+ };
951
+
952
+ const handleKeyDown = (event: KeyboardEvent) => {
953
+ // Ignore arrow keys, escape or tab
954
+ const keyCode = event.keyCode;
955
+ if (
956
+ keyCode === 16 ||
957
+ keyCode === 27 ||
958
+ keyCode === 9 ||
959
+ keyCode === 37 ||
960
+ keyCode === 38 ||
961
+ keyCode === 39 ||
962
+ keyCode === 40 ||
963
+ keyCode === 8 ||
964
+ keyCode === 46 ||
965
+ !editor.isEditable()
966
+ ) {
967
+ return;
968
+ }
969
+ if (keyCode === 13) {
970
+ event.preventDefault();
971
+ }
972
+ if (
973
+ !isEditing &&
974
+ primarySelectedCellID !== null &&
975
+ editor.getEditorState().read(() => $getSelection() === null) &&
976
+ (event.target as HTMLElement).contentEditable !== 'true'
977
+ ) {
978
+ if (isCopy(keyCode, event.shiftKey, event.metaKey, event.ctrlKey)) {
979
+ editor.dispatchCommand(COPY_COMMAND, event);
980
+ return;
981
+ }
982
+ if (isCut(keyCode, event.shiftKey, event.metaKey, event.ctrlKey)) {
983
+ editor.dispatchCommand(CUT_COMMAND, event);
984
+ return;
985
+ }
986
+ if (isPaste(keyCode, event.shiftKey, event.metaKey, event.ctrlKey)) {
987
+ editor.dispatchCommand(PASTE_COMMAND, event);
988
+ return;
989
+ }
990
+ }
991
+ if (event.metaKey || event.ctrlKey || event.altKey) {
992
+ return;
993
+ }
994
+ const cell = getCell(rows, primarySelectedCellID, cellCoordMap);
995
+ loadContentIntoCell(cell);
996
+ setIsEditing(true);
997
+ setSelectedCellIDs(NO_CELLS);
998
+ };
999
+
1000
+ doc.addEventListener('dblclick', handleDblClick);
1001
+ doc.addEventListener('keydown', handleKeyDown);
1002
+
1003
+ return () => {
1004
+ doc.removeEventListener('dblclick', handleDblClick);
1005
+ doc.removeEventListener('keydown', handleKeyDown);
1006
+ };
1007
+ }
1008
+ }, [cellEditor, editor, isEditing, rows, primarySelectedCellID, cellCoordMap]);
1009
+
1010
+ const updateCellsByID = useCallback(
1011
+ (ids: Array<string>, fn: () => void) => {
1012
+ $updateCells(rows, ids, cellCoordMap, cellEditor, updateTableNode, fn);
1013
+ },
1014
+ [cellCoordMap, cellEditor, rows, updateTableNode],
1015
+ );
1016
+
1017
+ const clearCellsCommand = useCallback((): boolean => {
1018
+ if (primarySelectedCellID !== null && !isEditing) {
1019
+ updateCellsByID([primarySelectedCellID, ...selectedCellIDs], () => {
1020
+ const root = $getRoot();
1021
+ root.clear();
1022
+ root.append($createParagraphNode());
1023
+ });
1024
+ return true;
1025
+ } else if (isSelected) {
1026
+ updateTableNode(tableNode => {
1027
+ $addUpdateTag('history-push');
1028
+ tableNode.selectNext();
1029
+ tableNode.remove();
1030
+ });
1031
+ }
1032
+ return false;
1033
+ }, [isEditing, isSelected, primarySelectedCellID, selectedCellIDs, updateCellsByID, updateTableNode]);
1034
+
1035
+ useEffect(() => {
1036
+ const tableElem = tableRef.current;
1037
+ if (tableElem === null) {
1038
+ return;
1039
+ }
1040
+
1041
+ const copyDataToClipboard = (
1042
+ event: ClipboardEvent,
1043
+ htmlString: string,
1044
+ lexicalString: string,
1045
+ plainTextString: string,
1046
+ ) => {
1047
+ const clipboardData = event instanceof KeyboardEvent ? null : event.clipboardData;
1048
+ event.preventDefault();
1049
+
1050
+ if (clipboardData != null) {
1051
+ clipboardData.setData('text/html', htmlString);
1052
+ clipboardData.setData('text/plain', plainTextString);
1053
+ clipboardData.setData('application/x-lexical-editor', lexicalString);
1054
+ } else {
1055
+ const clipboard = navigator.clipboard;
1056
+ if (clipboard != null) {
1057
+ // Most browsers only support a single item in the clipboard at one time.
1058
+ // So we optimize by only putting in HTML.
1059
+ const data = [
1060
+ new ClipboardItem({
1061
+ 'text/html': new Blob([htmlString as BlobPart], {
1062
+ type: 'text/html',
1063
+ }),
1064
+ }),
1065
+ ];
1066
+ clipboard.write(data);
1067
+ }
1068
+ }
1069
+ };
1070
+
1071
+ const getTypeFromObject = async (clipboardData: DataTransfer | ClipboardItem, type: string): Promise<string> => {
1072
+ try {
1073
+ return clipboardData instanceof DataTransfer
1074
+ ? clipboardData.getData(type)
1075
+ : clipboardData instanceof ClipboardItem
1076
+ ? await (await clipboardData.getType(type)).text()
1077
+ : '';
1078
+ } catch {
1079
+ return '';
1080
+ }
1081
+ };
1082
+
1083
+ const pasteContent = async (event: ClipboardEvent) => {
1084
+ let clipboardData: null | DataTransfer | ClipboardItem =
1085
+ (event instanceof InputEvent ? null : event.clipboardData) || null;
1086
+
1087
+ if (primarySelectedCellID !== null && cellEditor !== null) {
1088
+ event.preventDefault();
1089
+
1090
+ if (clipboardData === null) {
1091
+ try {
1092
+ const items = await navigator.clipboard.read();
1093
+ clipboardData = items[0];
1094
+ } catch {
1095
+ // NO-OP
1096
+ }
1097
+ }
1098
+ const lexicalString =
1099
+ clipboardData !== null ? await getTypeFromObject(clipboardData, 'application/x-lexical-editor') : '';
1100
+
1101
+ if (lexicalString) {
1102
+ try {
1103
+ const payload = JSON.parse(lexicalString);
1104
+ if (payload.namespace === editor._config.namespace && Array.isArray(payload.nodes)) {
1105
+ $updateCells(rows, [primarySelectedCellID], cellCoordMap, cellEditor, updateTableNode, () => {
1106
+ const root = $getRoot();
1107
+ root.clear();
1108
+ root.append($createParagraphNode());
1109
+ root.selectEnd();
1110
+ const nodes = $generateNodesFromSerializedNodes(payload.nodes);
1111
+ const sel = $getSelection();
1112
+ if ($isRangeSelection(sel)) {
1113
+ $insertGeneratedNodes(cellEditor, nodes, sel);
1114
+ }
1115
+ });
1116
+ return;
1117
+ }
1118
+ // eslint-disable-next-line no-empty
1119
+ } catch {}
1120
+ }
1121
+ const htmlString = clipboardData !== null ? await getTypeFromObject(clipboardData, 'text/html') : '';
1122
+
1123
+ if (htmlString) {
1124
+ try {
1125
+ const parser = new DOMParser();
1126
+ const dom = parser.parseFromString(htmlString, 'text/html');
1127
+ const possibleTableElement = dom.querySelector('table');
1128
+
1129
+ if (possibleTableElement != null) {
1130
+ const pasteRows = extractRowsFromHTML(possibleTableElement);
1131
+ updateTableNode(tableNode => {
1132
+ const [x, y] = cellCoordMap.get(primarySelectedCellID) as [number, number];
1133
+ $addUpdateTag('history-push');
1134
+ tableNode.mergeRows(x, y, pasteRows);
1135
+ });
1136
+ return;
1137
+ }
1138
+ $updateCells(rows, [primarySelectedCellID], cellCoordMap, cellEditor, updateTableNode, () => {
1139
+ const root = $getRoot();
1140
+ root.clear();
1141
+ root.append($createParagraphNode());
1142
+ root.selectEnd();
1143
+ const nodes = $generateNodesFromDOM(editor, dom);
1144
+ const sel = $getSelection();
1145
+ if ($isRangeSelection(sel)) {
1146
+ $insertGeneratedNodes(cellEditor, nodes, sel);
1147
+ }
1148
+ });
1149
+ return;
1150
+ // eslint-disable-next-line no-empty
1151
+ } catch {}
1152
+ }
1153
+
1154
+ // Multi-line plain text in rich text mode pasted as separate paragraphs
1155
+ // instead of single paragraph with linebreaks.
1156
+ const text = clipboardData !== null ? await getTypeFromObject(clipboardData, 'text/plain') : '';
1157
+
1158
+ if (text != null) {
1159
+ $updateCells(rows, [primarySelectedCellID], cellCoordMap, cellEditor, updateTableNode, () => {
1160
+ const root = $getRoot();
1161
+ root.clear();
1162
+ root.selectEnd();
1163
+ const sel = $getSelection();
1164
+ if (sel !== null) {
1165
+ sel.insertRawText(text);
1166
+ }
1167
+ });
1168
+ }
1169
+ }
1170
+ };
1171
+
1172
+ const copyPrimaryCell = (event: ClipboardEvent) => {
1173
+ if (primarySelectedCellID !== null && cellEditor !== null) {
1174
+ const cell = getCell(rows, primarySelectedCellID, cellCoordMap) as Cell;
1175
+ const json = cell.json;
1176
+ const htmlString = cellHTMLCache.get(json) || null;
1177
+ if (htmlString === null) {
1178
+ return;
1179
+ }
1180
+ const editorState = cellEditor.parseEditorState(json);
1181
+ const plainTextString = editorState.read(() => $getRoot().getTextContent());
1182
+ const lexicalString = editorState.read(() => {
1183
+ return JSON.stringify($generateJSONFromSelectedNodes(cellEditor, null));
1184
+ });
1185
+
1186
+ copyDataToClipboard(event, htmlString, lexicalString, plainTextString);
1187
+ }
1188
+ };
1189
+
1190
+ const copyCellRange = (event: ClipboardEvent) => {
1191
+ const lastCellID = lastCellIDRef.current;
1192
+ if (primarySelectedCellID !== null && cellEditor !== null && lastCellID !== null) {
1193
+ const rect = getSelectedRect(primarySelectedCellID, lastCellID, cellCoordMap);
1194
+ if (rect === null) {
1195
+ return;
1196
+ }
1197
+ const dom = exportTableCellsToHTML(rows, rect);
1198
+ const htmlString = dom.outerHTML;
1199
+ const plainTextString = dom.outerText;
1200
+ const tableNodeJSON = editor.getEditorState().read(() => {
1201
+ const tableNode = $getNodeByKey(nodeKey) as TableNode;
1202
+ return tableNode.exportJSON();
1203
+ });
1204
+ tableNodeJSON.rows = extractCellsFromRows(rows, rect);
1205
+ const lexicalJSON = {
1206
+ namespace: cellEditor._config.namespace,
1207
+ nodes: [tableNodeJSON],
1208
+ };
1209
+ const lexicalString = JSON.stringify(lexicalJSON);
1210
+ copyDataToClipboard(event, htmlString, lexicalString, plainTextString);
1211
+ }
1212
+ };
1213
+
1214
+ const handlePaste = (event: ClipboardEvent, activeEditor: LexicalEditor) => {
1215
+ const selection = $getSelection();
1216
+ if (primarySelectedCellID !== null && !isEditing && selection === null && activeEditor === editor) {
1217
+ pasteContent(event);
1218
+ mouseDownRef.current = false;
1219
+ setSelectedCellIDs(NO_CELLS);
1220
+ return true;
1221
+ }
1222
+ return false;
1223
+ };
1224
+
1225
+ const handleCopy = (event: ClipboardEvent, activeEditor: LexicalEditor) => {
1226
+ const selection = $getSelection();
1227
+ if (primarySelectedCellID !== null && !isEditing && selection === null && activeEditor === editor) {
1228
+ if (selectedCellIDs.length === 0) {
1229
+ copyPrimaryCell(event);
1230
+ } else {
1231
+ copyCellRange(event);
1232
+ }
1233
+ return true;
1234
+ }
1235
+ return false;
1236
+ };
1237
+
1238
+ return mergeRegister(
1239
+ editor.registerCommand(
1240
+ CLICK_COMMAND,
1241
+ () => {
1242
+ const selection = $getSelection();
1243
+ if ($isNodeSelection(selection)) {
1244
+ return true;
1245
+ }
1246
+ return false;
1247
+ },
1248
+ COMMAND_PRIORITY_LOW,
1249
+ ),
1250
+ editor.registerCommand<ClipboardEvent>(PASTE_COMMAND, handlePaste, COMMAND_PRIORITY_LOW),
1251
+ editor.registerCommand<ClipboardEvent>(COPY_COMMAND, handleCopy, COMMAND_PRIORITY_LOW),
1252
+ editor.registerCommand<ClipboardEvent>(
1253
+ CUT_COMMAND,
1254
+ (event: ClipboardEvent, activeEditor) => {
1255
+ if (handleCopy(event, activeEditor)) {
1256
+ clearCellsCommand();
1257
+ return true;
1258
+ }
1259
+ return false;
1260
+ },
1261
+ COMMAND_PRIORITY_LOW,
1262
+ ),
1263
+ editor.registerCommand<KeyboardEvent>(KEY_BACKSPACE_COMMAND, clearCellsCommand, COMMAND_PRIORITY_LOW),
1264
+ editor.registerCommand<KeyboardEvent>(KEY_DELETE_COMMAND, clearCellsCommand, COMMAND_PRIORITY_LOW),
1265
+ editor.registerCommand<TextFormatType>(
1266
+ FORMAT_TEXT_COMMAND,
1267
+ payload => {
1268
+ if (primarySelectedCellID !== null && !isEditing) {
1269
+ $updateCells(
1270
+ rows,
1271
+ [primarySelectedCellID, ...selectedCellIDs],
1272
+ cellCoordMap,
1273
+ cellEditor,
1274
+ updateTableNode,
1275
+ () => {
1276
+ const sel = $createSelectAll();
1277
+ sel.formatText(payload);
1278
+ },
1279
+ );
1280
+ return true;
1281
+ }
1282
+ return false;
1283
+ },
1284
+ COMMAND_PRIORITY_LOW,
1285
+ ),
1286
+ editor.registerCommand<KeyboardEvent>(
1287
+ KEY_ENTER_COMMAND,
1288
+ (event, targetEditor) => {
1289
+ const selection = $getSelection();
1290
+ if (
1291
+ primarySelectedCellID === null &&
1292
+ !isEditing &&
1293
+ $isNodeSelection(selection) &&
1294
+ selection.has(nodeKey) &&
1295
+ selection.getNodes().length === 1 &&
1296
+ targetEditor === editor
1297
+ ) {
1298
+ const firstCellID = rows[0].cells[0].id;
1299
+ setPrimarySelectedCellID(firstCellID);
1300
+ focusCell(tableElem, firstCellID);
1301
+ event.preventDefault();
1302
+ event.stopPropagation();
1303
+ clearSelection();
1304
+ return true;
1305
+ }
1306
+ return false;
1307
+ },
1308
+ COMMAND_PRIORITY_LOW,
1309
+ ),
1310
+ editor.registerCommand<KeyboardEvent>(
1311
+ KEY_TAB_COMMAND,
1312
+ event => {
1313
+ const selection = $getSelection();
1314
+ if (!isEditing && selection === null && primarySelectedCellID !== null) {
1315
+ const isBackward = event.shiftKey;
1316
+ const [x, y] = cellCoordMap.get(primarySelectedCellID) as [number, number];
1317
+ event.preventDefault();
1318
+ let nextX = null;
1319
+ let nextY = null;
1320
+ if (x === 0 && isBackward) {
1321
+ if (y !== 0) {
1322
+ nextY = y - 1;
1323
+ nextX = rows[nextY].cells.length - 1;
1324
+ }
1325
+ } else if (x === rows[y].cells.length - 1 && !isBackward) {
1326
+ if (y !== rows.length - 1) {
1327
+ nextY = y + 1;
1328
+ nextX = 0;
1329
+ }
1330
+ } else if (!isBackward) {
1331
+ nextX = x + 1;
1332
+ nextY = y;
1333
+ } else {
1334
+ nextX = x - 1;
1335
+ nextY = y;
1336
+ }
1337
+ if (nextX !== null && nextY !== null) {
1338
+ modifySelectedCells(nextX, nextY, false);
1339
+ return true;
1340
+ }
1341
+ }
1342
+ return false;
1343
+ },
1344
+ COMMAND_PRIORITY_LOW,
1345
+ ),
1346
+ editor.registerCommand<KeyboardEvent>(
1347
+ KEY_ARROW_UP_COMMAND,
1348
+ (event, targetEditor) => {
1349
+ const selection = $getSelection();
1350
+ if (!isEditing && selection === null) {
1351
+ const extend = event.shiftKey;
1352
+ const cellID = extend ? lastCellIDRef.current || primarySelectedCellID : primarySelectedCellID;
1353
+ if (cellID !== null) {
1354
+ const [x, y] = cellCoordMap.get(cellID) as [number, number];
1355
+ if (y !== 0) {
1356
+ modifySelectedCells(x, y - 1, extend);
1357
+ return true;
1358
+ }
1359
+ }
1360
+ }
1361
+ if (!$isRangeSelection(selection) || targetEditor !== cellEditor) {
1362
+ return false;
1363
+ }
1364
+ if (
1365
+ selection.isCollapsed() &&
1366
+ selection.anchor.getNode().getTopLevelElementOrThrow().getPreviousSibling() === null
1367
+ ) {
1368
+ event.preventDefault();
1369
+ return true;
1370
+ }
1371
+ return false;
1372
+ },
1373
+ COMMAND_PRIORITY_LOW,
1374
+ ),
1375
+ editor.registerCommand<KeyboardEvent>(
1376
+ KEY_ARROW_DOWN_COMMAND,
1377
+ (event, targetEditor) => {
1378
+ const selection = $getSelection();
1379
+ if (!isEditing && selection === null) {
1380
+ const extend = event.shiftKey;
1381
+ const cellID = extend ? lastCellIDRef.current || primarySelectedCellID : primarySelectedCellID;
1382
+ if (cellID !== null) {
1383
+ const [x, y] = cellCoordMap.get(cellID) as [number, number];
1384
+ if (y !== rows.length - 1) {
1385
+ modifySelectedCells(x, y + 1, extend);
1386
+ return true;
1387
+ }
1388
+ }
1389
+ }
1390
+ if (!$isRangeSelection(selection) || targetEditor !== cellEditor) {
1391
+ return false;
1392
+ }
1393
+ if (
1394
+ selection.isCollapsed() &&
1395
+ selection.anchor.getNode().getTopLevelElementOrThrow().getNextSibling() === null
1396
+ ) {
1397
+ event.preventDefault();
1398
+ return true;
1399
+ }
1400
+ return false;
1401
+ },
1402
+ COMMAND_PRIORITY_LOW,
1403
+ ),
1404
+ editor.registerCommand<KeyboardEvent>(
1405
+ KEY_ARROW_LEFT_COMMAND,
1406
+ (event, targetEditor) => {
1407
+ const selection = $getSelection();
1408
+ if (!isEditing && selection === null) {
1409
+ const extend = event.shiftKey;
1410
+ const cellID = extend ? lastCellIDRef.current || primarySelectedCellID : primarySelectedCellID;
1411
+ if (cellID !== null) {
1412
+ const [x, y] = cellCoordMap.get(cellID) as [number, number];
1413
+ if (x !== 0) {
1414
+ modifySelectedCells(x - 1, y, extend);
1415
+ return true;
1416
+ }
1417
+ }
1418
+ }
1419
+ if (!$isRangeSelection(selection) || targetEditor !== cellEditor) {
1420
+ return false;
1421
+ }
1422
+ if (selection.isCollapsed() && selection.anchor.offset === 0) {
1423
+ event.preventDefault();
1424
+ return true;
1425
+ }
1426
+ return false;
1427
+ },
1428
+ COMMAND_PRIORITY_LOW,
1429
+ ),
1430
+ editor.registerCommand<KeyboardEvent>(
1431
+ KEY_ARROW_RIGHT_COMMAND,
1432
+ (event, targetEditor) => {
1433
+ const selection = $getSelection();
1434
+ if (!isEditing && selection === null) {
1435
+ const extend = event.shiftKey;
1436
+ const cellID = extend ? lastCellIDRef.current || primarySelectedCellID : primarySelectedCellID;
1437
+ if (cellID !== null) {
1438
+ const [x, y] = cellCoordMap.get(cellID) as [number, number];
1439
+ if (x !== rows[y].cells.length - 1) {
1440
+ modifySelectedCells(x + 1, y, extend);
1441
+ return true;
1442
+ }
1443
+ }
1444
+ }
1445
+ if (!$isRangeSelection(selection) || targetEditor !== cellEditor) {
1446
+ return false;
1447
+ }
1448
+ if (selection.isCollapsed()) {
1449
+ const anchor = selection.anchor;
1450
+ if (
1451
+ (anchor.type === 'text' && anchor.offset === anchor.getNode().getTextContentSize()) ||
1452
+ (anchor.type === 'element' && anchor.offset === anchor.getNode().getChildrenSize())
1453
+ ) {
1454
+ event.preventDefault();
1455
+ return true;
1456
+ }
1457
+ }
1458
+ return false;
1459
+ },
1460
+ COMMAND_PRIORITY_LOW,
1461
+ ),
1462
+ editor.registerCommand<KeyboardEvent>(
1463
+ KEY_ESCAPE_COMMAND,
1464
+ (event, targetEditor) => {
1465
+ const selection = $getSelection();
1466
+ if (!isEditing && selection === null && targetEditor === editor) {
1467
+ setSelected(true);
1468
+ setPrimarySelectedCellID(null);
1469
+ selectTable();
1470
+ return true;
1471
+ }
1472
+ if (!$isRangeSelection(selection)) {
1473
+ return false;
1474
+ }
1475
+ if (isEditing) {
1476
+ saveEditorToJSON();
1477
+ setIsEditing(false);
1478
+ if (primarySelectedCellID !== null) {
1479
+ setTimeout(() => {
1480
+ focusCell(tableElem, primarySelectedCellID);
1481
+ }, 20);
1482
+ }
1483
+ return true;
1484
+ }
1485
+ return false;
1486
+ },
1487
+ COMMAND_PRIORITY_LOW,
1488
+ ),
1489
+ );
1490
+ }, [
1491
+ cellCoordMap,
1492
+ cellEditor,
1493
+ clearCellsCommand,
1494
+ clearSelection,
1495
+ editor,
1496
+ isEditing,
1497
+ modifySelectedCells,
1498
+ nodeKey,
1499
+ primarySelectedCellID,
1500
+ rows,
1501
+ saveEditorToJSON,
1502
+ selectTable,
1503
+ selectedCellIDs,
1504
+ setSelected,
1505
+ updateTableNode,
1506
+ ]);
1507
+
1508
+ if (cellEditor === null) {
1509
+ return;
1510
+ }
1511
+
1512
+ return (
1513
+ <div style={{ position: 'relative' }}>
1514
+ <table className={`${theme.table} ${isSelected ? theme.tableSelected : ''}`} ref={tableRef} tabIndex={-1}>
1515
+ <tbody>
1516
+ {rows.map(row => (
1517
+ <tr key={row.id} className={theme.tableRow}>
1518
+ {row.cells.map(cell => {
1519
+ const { id } = cell;
1520
+ return (
1521
+ <TableCell
1522
+ key={id}
1523
+ cell={cell}
1524
+ theme={theme}
1525
+ isSelected={selectedCellSet.has(id)}
1526
+ isPrimarySelected={primarySelectedCellID === id}
1527
+ isEditing={isEditing}
1528
+ sortingOptions={sortingOptions}
1529
+ cellEditor={cellEditor}
1530
+ updateCellsByID={updateCellsByID}
1531
+ updateTableNode={updateTableNode}
1532
+ cellCoordMap={cellCoordMap}
1533
+ rows={rows}
1534
+ setSortingOptions={setSortingOptions}
1535
+ />
1536
+ );
1537
+ })}
1538
+ </tr>
1539
+ ))}
1540
+ </tbody>
1541
+ </table>
1542
+ {showAddColumns && <button className={theme.tableAddColumns} onClick={addColumns} />}
1543
+ {showAddRows && <button className={theme.tableAddRows} onClick={addRows} ref={addRowsRef} />}
1544
+ {resizingID !== null && <div className={theme.tableResizeRuler} ref={tableResizerRulerRef} />}
1545
+ </div>
1546
+ );
1547
+ }