@lexical/table 0.20.1-nightly.20241112.0 → 0.20.1-nightly.20241114.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1429,6 +1429,20 @@ function $getChildrenRecursively(node) {
1429
1429
  *
1430
1430
  */
1431
1431
 
1432
+ function $getTableAndElementByKey(tableNodeKey, editor = lexical.$getEditor()) {
1433
+ const tableNode = lexical.$getNodeByKey(tableNodeKey);
1434
+ if (!$isTableNode(tableNode)) {
1435
+ throw Error(`TableObserver: Expected tableNodeKey ${tableNodeKey} to be a TableNode`);
1436
+ }
1437
+ const tableElement = getTableElement(tableNode, editor.getElementByKey(tableNodeKey));
1438
+ if (!(tableElement !== null)) {
1439
+ throw Error(`TableObserver: Expected to find TableElement in DOM for key ${tableNodeKey}`);
1440
+ }
1441
+ return {
1442
+ tableElement,
1443
+ tableNode
1444
+ };
1445
+ }
1432
1446
  class TableObserver {
1433
1447
  constructor(editor, tableNodeKey) {
1434
1448
  this.isHighlightingCells = false;
@@ -1450,12 +1464,13 @@ class TableObserver {
1450
1464
  this.anchorCell = null;
1451
1465
  this.focusCell = null;
1452
1466
  this.hasHijackedSelectionStyles = false;
1453
- this.trackTable();
1454
1467
  this.isSelecting = false;
1468
+ this.shouldCheckSelection = false;
1455
1469
  this.abortController = new AbortController();
1456
1470
  this.listenerOptions = {
1457
1471
  signal: this.abortController.signal
1458
1472
  };
1473
+ this.trackTable();
1459
1474
  }
1460
1475
  getTable() {
1461
1476
  return this.table;
@@ -1465,9 +1480,12 @@ class TableObserver {
1465
1480
  Array.from(this.listenersToRemove).forEach(removeListener => removeListener());
1466
1481
  this.listenersToRemove.clear();
1467
1482
  }
1483
+ $lookup() {
1484
+ return $getTableAndElementByKey(this.tableNodeKey, this.editor);
1485
+ }
1468
1486
  trackTable() {
1469
1487
  const observer = new MutationObserver(records => {
1470
- this.editor.update(() => {
1488
+ this.editor.getEditorState().read(() => {
1471
1489
  let gridNeedsRedraw = false;
1472
1490
  for (let i = 0; i < records.length; i++) {
1473
1491
  const record = records[i];
@@ -1481,24 +1499,28 @@ class TableObserver {
1481
1499
  if (!gridNeedsRedraw) {
1482
1500
  return;
1483
1501
  }
1484
- const tableElement = this.editor.getElementByKey(this.tableNodeKey);
1485
- if (!tableElement) {
1486
- throw new Error('Expected to find TableElement in DOM');
1487
- }
1488
- this.table = getTable(tableElement);
1502
+ const {
1503
+ tableNode,
1504
+ tableElement
1505
+ } = this.$lookup();
1506
+ this.table = getTable(tableNode, tableElement);
1507
+ }, {
1508
+ editor: this.editor
1489
1509
  });
1490
1510
  });
1491
- this.editor.update(() => {
1492
- const tableElement = this.editor.getElementByKey(this.tableNodeKey);
1493
- if (!tableElement) {
1494
- throw new Error('Expected to find TableElement in DOM');
1495
- }
1496
- this.table = getTable(tableElement);
1511
+ this.editor.getEditorState().read(() => {
1512
+ const {
1513
+ tableNode,
1514
+ tableElement
1515
+ } = this.$lookup();
1516
+ this.table = getTable(tableNode, tableElement);
1497
1517
  observer.observe(tableElement, {
1498
1518
  attributes: true,
1499
1519
  childList: true,
1500
1520
  subtree: true
1501
1521
  });
1522
+ }, {
1523
+ editor: this.editor
1502
1524
  });
1503
1525
  }
1504
1526
  clearHighlight() {
@@ -1516,15 +1538,11 @@ class TableObserver {
1516
1538
  this.hasHijackedSelectionStyles = false;
1517
1539
  this.enableHighlightStyle();
1518
1540
  editor.update(() => {
1519
- const tableNode = lexical.$getNodeByKey(this.tableNodeKey);
1520
- if (!$isTableNode(tableNode)) {
1521
- throw new Error('Expected TableNode.');
1522
- }
1523
- const tableElement = editor.getElementByKey(this.tableNodeKey);
1524
- if (!tableElement) {
1525
- throw new Error('Expected to find TableElement in DOM');
1526
- }
1527
- const grid = getTable(tableElement);
1541
+ const {
1542
+ tableNode,
1543
+ tableElement
1544
+ } = this.$lookup();
1545
+ const grid = getTable(tableNode, tableElement);
1528
1546
  $updateDOMForSelection(editor, grid, null);
1529
1547
  lexical.$setSelection(null);
1530
1548
  editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
@@ -1532,25 +1550,27 @@ class TableObserver {
1532
1550
  }
1533
1551
  enableHighlightStyle() {
1534
1552
  const editor = this.editor;
1535
- editor.update(() => {
1536
- const tableElement = editor.getElementByKey(this.tableNodeKey);
1537
- if (!tableElement) {
1538
- throw new Error('Expected to find TableElement in DOM');
1539
- }
1553
+ editor.getEditorState().read(() => {
1554
+ const {
1555
+ tableElement
1556
+ } = this.$lookup();
1540
1557
  utils.removeClassNamesFromElement(tableElement, editor._config.theme.tableSelection);
1541
1558
  tableElement.classList.remove('disable-selection');
1542
1559
  this.hasHijackedSelectionStyles = false;
1560
+ }, {
1561
+ editor
1543
1562
  });
1544
1563
  }
1545
1564
  disableHighlightStyle() {
1546
1565
  const editor = this.editor;
1547
- editor.update(() => {
1548
- const tableElement = editor.getElementByKey(this.tableNodeKey);
1549
- if (!tableElement) {
1550
- throw new Error('Expected to find TableElement in DOM');
1551
- }
1566
+ editor.getEditorState().read(() => {
1567
+ const {
1568
+ tableElement
1569
+ } = this.$lookup();
1552
1570
  utils.addClassNamesToElement(tableElement, editor._config.theme.tableSelection);
1553
1571
  this.hasHijackedSelectionStyles = true;
1572
+ }, {
1573
+ editor
1554
1574
  });
1555
1575
  }
1556
1576
  updateTableTableSelection(selection) {
@@ -1567,17 +1587,35 @@ class TableObserver {
1567
1587
  this.updateTableTableSelection(selection);
1568
1588
  }
1569
1589
  }
1590
+
1591
+ /**
1592
+ * @internal
1593
+ * Firefox has a strange behavior where pressing the down arrow key from
1594
+ * above the table will move the caret after the table and then lexical
1595
+ * will select the last cell instead of the first.
1596
+ * We do still want to let the browser handle caret movement but we will
1597
+ * use this property to "tag" the update so that we can recheck the
1598
+ * selection after the event is processed.
1599
+ */
1600
+ setShouldCheckSelection() {
1601
+ this.shouldCheckSelection = true;
1602
+ }
1603
+ /**
1604
+ * @internal
1605
+ */
1606
+ getAndClearShouldCheckSelection() {
1607
+ if (this.shouldCheckSelection) {
1608
+ this.shouldCheckSelection = false;
1609
+ return true;
1610
+ }
1611
+ return false;
1612
+ }
1570
1613
  setFocusCellForSelection(cell, ignoreStart = false) {
1571
1614
  const editor = this.editor;
1572
1615
  editor.update(() => {
1573
- const tableNode = lexical.$getNodeByKey(this.tableNodeKey);
1574
- if (!$isTableNode(tableNode)) {
1575
- throw new Error('Expected TableNode.');
1576
- }
1577
- const tableElement = editor.getElementByKey(this.tableNodeKey);
1578
- if (!tableElement) {
1579
- throw new Error('Expected to find TableElement in DOM');
1580
- }
1616
+ const {
1617
+ tableNode
1618
+ } = this.$lookup();
1581
1619
  const cellX = cell.x;
1582
1620
  const cellY = cell.y;
1583
1621
  this.focusCell = cell;
@@ -1702,15 +1740,26 @@ const getDOMSelection = targetWindow => CAN_USE_DOM ? (targetWindow || window).g
1702
1740
  const isMouseDownOnEvent = event => {
1703
1741
  return (event.buttons & 1) === 1;
1704
1742
  };
1705
- function applyTableHandlers(tableNode, tableElement, editor, hasTabHandler) {
1743
+ function getTableElement(tableNode, dom) {
1744
+ if (!dom) {
1745
+ return dom;
1746
+ }
1747
+ const element = dom.nodeName === 'TABLE' ? dom : tableNode.getDOMSlot(dom).element;
1748
+ if (!(element.nodeName === 'TABLE')) {
1749
+ throw Error(`getTableElement: Expecting table in as DOM node for TableNode, not ${dom.nodeName}`);
1750
+ }
1751
+ return element;
1752
+ }
1753
+ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1706
1754
  const rootElement = editor.getRootElement();
1707
1755
  if (rootElement === null) {
1708
1756
  throw new Error('No root element.');
1709
1757
  }
1710
1758
  const tableObserver = new TableObserver(editor, tableNode.getKey());
1711
1759
  const editorWindow = editor._window || window;
1760
+ const tableElement = getTableElement(tableNode, element);
1712
1761
  attachTableObserverToTableElement(tableElement, tableObserver);
1713
- tableObserver.listenersToRemove.add(() => deatatchTableObserverFromTableElement(tableElement, tableObserver));
1762
+ tableObserver.listenersToRemove.add(() => detatchTableObserverFromTableElement(tableElement, tableObserver));
1714
1763
  const createMouseHandlers = () => {
1715
1764
  const onMouseUp = () => {
1716
1765
  tableObserver.isSelecting = false;
@@ -2052,6 +2101,21 @@ function applyTableHandlers(tableNode, tableElement, editor, hasTabHandler) {
2052
2101
  tableObserver.listenersToRemove.add(editor.registerCommand(lexical.SELECTION_CHANGE_COMMAND, () => {
2053
2102
  const selection = lexical.$getSelection();
2054
2103
  const prevSelection = lexical.$getPreviousSelection();
2104
+ // If they pressed the down arrow with the selection outside of the
2105
+ // table, and then the selection ends up in the table but not in the
2106
+ // first cell, then move the selection to the first cell.
2107
+ if (tableObserver.getAndClearShouldCheckSelection() && lexical.$isRangeSelection(prevSelection) && lexical.$isRangeSelection(selection) && selection.isCollapsed()) {
2108
+ const anchor = selection.anchor.getNode();
2109
+ const firstRow = tableNode.getFirstChild();
2110
+ const anchorCell = $findCellNode(anchor);
2111
+ if (anchorCell !== null && $isTableRowNode(firstRow)) {
2112
+ const firstCell = firstRow.getFirstChild();
2113
+ if ($isTableCellNode(firstCell) && !utils.$findMatchingParent(anchorCell, node => node.is(firstCell))) {
2114
+ firstCell.selectStart();
2115
+ return true;
2116
+ }
2117
+ }
2118
+ }
2055
2119
  if (lexical.$isRangeSelection(selection)) {
2056
2120
  const {
2057
2121
  anchor,
@@ -2155,7 +2219,7 @@ function applyTableHandlers(tableNode, tableElement, editor, hasTabHandler) {
2155
2219
  }, lexical.COMMAND_PRIORITY_CRITICAL));
2156
2220
  return tableObserver;
2157
2221
  }
2158
- function deatatchTableObserverFromTableElement(tableElement, tableObserver) {
2222
+ function detatchTableObserverFromTableElement(tableElement, tableObserver) {
2159
2223
  if (getTableObserverFromTableElement(tableElement) === tableObserver) {
2160
2224
  delete tableElement[LEXICAL_ELEMENT_KEY];
2161
2225
  }
@@ -2185,7 +2249,8 @@ function getDOMCellFromTarget(node) {
2185
2249
  }
2186
2250
  return null;
2187
2251
  }
2188
- function getTable(tableElement) {
2252
+ function getTable(tableNode, dom) {
2253
+ const tableElement = getTableElement(tableNode, dom);
2189
2254
  const domRows = [];
2190
2255
  const grid = {
2191
2256
  columns: 0,
@@ -2527,6 +2592,10 @@ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
2527
2592
  }
2528
2593
  }
2529
2594
  }
2595
+ if (direction === 'down' && $isScrollableTablesActive(editor)) {
2596
+ // Enable Firefox workaround
2597
+ tableObserver.setShouldCheckSelection();
2598
+ }
2530
2599
  return false;
2531
2600
  }
2532
2601
  if (lexical.$isRangeSelection(selection) && selection.isCollapsed()) {
@@ -2541,9 +2610,9 @@ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
2541
2610
  }
2542
2611
  const anchorCellTable = $findTableNode(anchorCellNode);
2543
2612
  if (anchorCellTable !== tableNode && anchorCellTable != null) {
2544
- const anchorCellTableElement = editor.getElementByKey(anchorCellTable.getKey());
2613
+ const anchorCellTableElement = getTableElement(anchorCellTable, editor.getElementByKey(anchorCellTable.getKey()));
2545
2614
  if (anchorCellTableElement != null) {
2546
- tableObserver.table = getTable(anchorCellTableElement);
2615
+ tableObserver.table = getTable(anchorCellTable, anchorCellTableElement);
2547
2616
  return $handleArrowKey(editor, event, direction, anchorCellTable, tableObserver);
2548
2617
  }
2549
2618
  }
@@ -2609,12 +2678,15 @@ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
2609
2678
  const anchorCellNode = utils.$findMatchingParent(anchor.getNode(), $isTableCellNode);
2610
2679
  const focusCellNode = utils.$findMatchingParent(focus.getNode(), $isTableCellNode);
2611
2680
  const [tableNodeFromSelection] = selection.getNodes();
2612
- const tableElement = editor.getElementByKey(tableNodeFromSelection.getKey());
2681
+ if (!$isTableNode(tableNodeFromSelection)) {
2682
+ throw Error(`$handleArrowKey: TableSelection.getNodes()[0] expected to be TableNode`);
2683
+ }
2684
+ const tableElement = getTableElement(tableNodeFromSelection, editor.getElementByKey(tableNodeFromSelection.getKey()));
2613
2685
  if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableNode(tableNodeFromSelection) || tableElement == null) {
2614
2686
  return false;
2615
2687
  }
2616
2688
  tableObserver.updateTableTableSelection(selection);
2617
- const grid = getTable(tableElement);
2689
+ const grid = getTable(tableNodeFromSelection, tableElement);
2618
2690
  const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid);
2619
2691
  const anchorCell = tableNode.getDOMCellFromCordsOrThrow(cordsAnchor.x, cordsAnchor.y, grid);
2620
2692
  tableObserver.setAnchorCellForSelection(anchorCell);
@@ -2710,14 +2782,21 @@ function $getTableEdgeCursorPosition(editor, selection, tableNode) {
2710
2782
  if (!tableNodeParent) {
2711
2783
  return undefined;
2712
2784
  }
2713
- const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey());
2714
- if (!tableNodeParentDOM) {
2715
- return undefined;
2716
- }
2717
2785
 
2718
2786
  // TODO: Add support for nested tables
2719
2787
  const domSelection = window.getSelection();
2720
- if (!domSelection || domSelection.anchorNode !== tableNodeParentDOM) {
2788
+ if (!domSelection) {
2789
+ return undefined;
2790
+ }
2791
+ const domAnchorNode = domSelection.anchorNode;
2792
+ const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey());
2793
+ const tableElement = getTableElement(tableNode, editor.getElementByKey(tableNode.getKey()));
2794
+ // We are only interested in the scenario where the
2795
+ // native selection anchor is:
2796
+ // - at or inside the table's parent DOM
2797
+ // - and NOT at or inside the table DOM
2798
+ // It may be adjacent to the table DOM (e.g. in a wrapper)
2799
+ if (!domAnchorNode || !tableNodeParentDOM || !tableElement || !tableNodeParentDOM.contains(domAnchorNode) || tableElement.contains(domAnchorNode)) {
2721
2800
  return undefined;
2722
2801
  }
2723
2802
  const anchorCellNode = utils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
@@ -2779,6 +2858,20 @@ function setRowStriping(dom, config, rowStriping) {
2779
2858
  dom.removeAttribute('data-lexical-row-striping');
2780
2859
  }
2781
2860
  }
2861
+ const scrollableEditors = new WeakSet();
2862
+ function $isScrollableTablesActive(editor = lexical.$getEditor()) {
2863
+ return scrollableEditors.has(editor);
2864
+ }
2865
+ function setScrollableTablesActive(editor, active) {
2866
+ if (active) {
2867
+ if (!editor._config.theme.tableScrollableWrapper) {
2868
+ console.warn('TableNode: hasHorizontalScroll is active but theme.tableScrollableWrapper is not defined.');
2869
+ }
2870
+ scrollableEditors.add(editor);
2871
+ } else {
2872
+ scrollableEditors.delete(editor);
2873
+ }
2874
+ }
2782
2875
 
2783
2876
  /** @noInheritDoc */
2784
2877
  class TableNode extends lexical.ElementNode {
@@ -2832,15 +2925,34 @@ class TableNode extends lexical.ElementNode {
2832
2925
  version: 1
2833
2926
  };
2834
2927
  }
2928
+ getDOMSlot(element) {
2929
+ const tableElement = element.nodeName !== 'TABLE' && element.querySelector('table') || element;
2930
+ if (!(tableElement.nodeName === 'TABLE')) {
2931
+ throw Error(`TableNode.getDOMSlot: createDOM() did not return a table`);
2932
+ }
2933
+ return super.getDOMSlot(tableElement).withAfter(tableElement.querySelector('colgroup'));
2934
+ }
2835
2935
  createDOM(config, editor) {
2836
2936
  const tableElement = document.createElement('table');
2837
2937
  const colGroup = document.createElement('colgroup');
2838
2938
  tableElement.appendChild(colGroup);
2839
2939
  updateColgroup(tableElement, config, this.getColumnCount(), this.getColWidths());
2940
+ lexical.setDOMUnmanaged(colGroup);
2840
2941
  utils.addClassNamesToElement(tableElement, config.theme.table);
2841
2942
  if (this.__rowStriping) {
2842
2943
  setRowStriping(tableElement, config, true);
2843
2944
  }
2945
+ if ($isScrollableTablesActive(editor)) {
2946
+ const wrapperElement = document.createElement('div');
2947
+ const classes = config.theme.tableScrollableWrapper;
2948
+ if (classes) {
2949
+ utils.addClassNamesToElement(wrapperElement, classes);
2950
+ } else {
2951
+ wrapperElement.style.cssText = 'overflow-x: auto;';
2952
+ }
2953
+ wrapperElement.appendChild(tableElement);
2954
+ return wrapperElement;
2955
+ }
2844
2956
  return tableElement;
2845
2957
  }
2846
2958
  updateDOM(prevNode, dom, config) {
@@ -2854,19 +2966,20 @@ class TableNode extends lexical.ElementNode {
2854
2966
  return {
2855
2967
  ...super.exportDOM(editor),
2856
2968
  after: tableElement => {
2857
- if (tableElement) {
2858
- const newElement = tableElement.cloneNode();
2859
- const colGroup = document.createElement('colgroup');
2969
+ if (tableElement && utils.isHTMLElement(tableElement) && tableElement.nodeName !== 'TABLE') {
2970
+ tableElement = tableElement.querySelector('table');
2971
+ }
2972
+ if (!tableElement || !utils.isHTMLElement(tableElement)) {
2973
+ return null;
2974
+ }
2975
+ // Wrap direct descendant rows in a tbody for export
2976
+ const rows = tableElement.querySelectorAll(':scope > tr');
2977
+ if (rows.length > 0) {
2860
2978
  const tBody = document.createElement('tbody');
2861
- if (utils.isHTMLElement(tableElement)) {
2862
- const cols = tableElement.querySelectorAll('col');
2863
- colGroup.append(...cols);
2864
- const rows = tableElement.querySelectorAll('tr');
2865
- tBody.append(...rows);
2866
- }
2867
- newElement.replaceChildren(colGroup, tBody);
2868
- return newElement;
2979
+ tBody.append(...rows);
2980
+ tableElement.append(tBody);
2869
2981
  }
2982
+ return tableElement;
2870
2983
  }
2871
2984
  };
2872
2985
  }
@@ -2973,10 +3086,10 @@ class TableNode extends lexical.ElementNode {
2973
3086
  }
2974
3087
  function $getElementForTableNode(editor, tableNode) {
2975
3088
  const tableElement = editor.getElementByKey(tableNode.getKey());
2976
- if (tableElement == null) {
2977
- throw new Error('Table Element Not Found');
3089
+ if (!(tableElement !== null)) {
3090
+ throw Error(`$getElementForTableNode: Table Element Not Found`);
2978
3091
  }
2979
- return getTable(tableElement);
3092
+ return getTable(tableNode, tableElement);
2980
3093
  }
2981
3094
  function $convertTableElement(domNode) {
2982
3095
  const tableNode = $createTableNode();
@@ -3023,6 +3136,7 @@ exports.$findCellNode = $findCellNode;
3023
3136
  exports.$findTableNode = $findTableNode;
3024
3137
  exports.$getElementForTableNode = $getElementForTableNode;
3025
3138
  exports.$getNodeTriplet = $getNodeTriplet;
3139
+ exports.$getTableAndElementByKey = $getTableAndElementByKey;
3026
3140
  exports.$getTableCellNodeFromLexicalNode = $getTableCellNodeFromLexicalNode;
3027
3141
  exports.$getTableCellNodeRect = $getTableCellNodeRect;
3028
3142
  exports.$getTableColumnIndexFromTableCellNode = $getTableColumnIndexFromTableCellNode;
@@ -3033,6 +3147,7 @@ exports.$insertTableColumn = $insertTableColumn;
3033
3147
  exports.$insertTableColumn__EXPERIMENTAL = $insertTableColumn__EXPERIMENTAL;
3034
3148
  exports.$insertTableRow = $insertTableRow;
3035
3149
  exports.$insertTableRow__EXPERIMENTAL = $insertTableRow__EXPERIMENTAL;
3150
+ exports.$isScrollableTablesActive = $isScrollableTablesActive;
3036
3151
  exports.$isTableCellNode = $isTableCellNode;
3037
3152
  exports.$isTableNode = $isTableNode;
3038
3153
  exports.$isTableRowNode = $isTableRowNode;
@@ -3047,4 +3162,6 @@ exports.TableObserver = TableObserver;
3047
3162
  exports.TableRowNode = TableRowNode;
3048
3163
  exports.applyTableHandlers = applyTableHandlers;
3049
3164
  exports.getDOMCellFromTarget = getDOMCellFromTarget;
3165
+ exports.getTableElement = getTableElement;
3050
3166
  exports.getTableObserverFromTableElement = getTableObserverFromTableElement;
3167
+ exports.setScrollableTablesActive = setScrollableTablesActive;