@crystallize/design-system 1.7.0 → 1.8.0

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 (51) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/index.css +53 -332
  3. package/dist/index.d.ts +15 -8
  4. package/dist/index.js +1723 -4554
  5. package/dist/index.mjs +1643 -2339
  6. package/package.json +1 -1
  7. package/src/action-menu/action-item-separator.tsx +14 -0
  8. package/src/action-menu/action-item.tsx +2 -2
  9. package/src/action-menu/action-menu.css +8 -0
  10. package/src/action-menu/action-menu.tsx +2 -1
  11. package/src/dropdown-menu/dropdown-menu-root.tsx +3 -1
  12. package/src/dropdown-menu/index.ts +5 -2
  13. package/src/iconography/subscription-contracts.tsx +4 -4
  14. package/src/iconography/subscription-plans.tsx +5 -5
  15. package/src/rich-text-editor/model/crystallize-to-lexical.ts +12 -1
  16. package/src/rich-text-editor/model/lexical-to-crystallize.ts +38 -38
  17. package/src/rich-text-editor/nodes/BaseNodes.ts +0 -7
  18. package/src/rich-text-editor/nodes/TableCellNodes.ts +0 -7
  19. package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +1 -1
  20. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +3 -2
  21. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +0 -1
  22. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.css +17 -17
  23. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +1 -1
  24. package/src/rich-text-editor/plugins/MaxLengthPlugin/index.tsx +2 -7
  25. package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +80 -149
  26. package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +2 -2
  27. package/src/rich-text-editor/plugins/ToolbarPlugin/insert-table.tsx +55 -0
  28. package/src/rich-text-editor/rich-text-editor.css +10 -322
  29. package/src/rich-text-editor/rich-text-editor.stories.tsx +35 -5
  30. package/src/rich-text-editor/rich-text-editor.tsx +6 -39
  31. package/src/rich-text-editor/tests/rich-text-editor-code.test.tsx +10 -6
  32. package/src/rich-text-editor/tests/rich-text-editor-model-conversions.test.tsx +19 -7
  33. package/src/rich-text-editor/themes/CrystallizeRTEditorTheme.css +3 -11
  34. package/dist/TableComponent-I2YOOYOU.css +0 -281
  35. package/dist/TableComponent-QINOO453.mjs +0 -1377
  36. package/dist/chevron-down-3FRWSIKS.svg +0 -1
  37. package/dist/chunk-VUXQZRSP.mjs +0 -737
  38. package/dist/markdown-4BGQNLLT.svg +0 -1
  39. package/src/rich-text-editor/nodes/KeywordNode.ts +0 -73
  40. package/src/rich-text-editor/nodes/TableComponent.tsx +0 -1547
  41. package/src/rich-text-editor/nodes/TableNode.tsx +0 -398
  42. package/src/rich-text-editor/plugins/ComponentPickerPlugin/index.tsx +0 -320
  43. package/src/rich-text-editor/plugins/DragDropPastePlugin/index.ts +0 -40
  44. package/src/rich-text-editor/plugins/MarkdownShortcutPlugin/index.tsx +0 -16
  45. package/src/rich-text-editor/plugins/MarkdownTransformers/index.ts +0 -195
  46. package/src/rich-text-editor/plugins/SpeechToTextPlugin/index.ts +0 -113
  47. package/src/rich-text-editor/plugins/TableCellResizer/index.css +0 -12
  48. package/src/rich-text-editor/plugins/TableCellResizer/index.tsx +0 -386
  49. package/src/rich-text-editor/plugins/TablePlugin.tsx +0 -190
  50. package/src/rich-text-editor/plugins/TreeViewPlugin/index.tsx +0 -25
  51. package/src/rich-text-editor/plugins/TypingPerfPlugin/index.ts +0 -117
@@ -1,1547 +0,0 @@
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
- }