@lexical/table 0.17.2-nightly.20240906.0 → 0.17.2-nightly.20240909.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.
@@ -56,10 +56,12 @@ class TableCellNode extends lexical.ElementNode {
56
56
  return 'tablecell';
57
57
  }
58
58
  static clone(node) {
59
- const cellNode = new TableCellNode(node.__headerState, node.__colSpan, node.__width, node.__key);
60
- cellNode.__rowSpan = node.__rowSpan;
61
- cellNode.__backgroundColor = node.__backgroundColor;
62
- return cellNode;
59
+ return new TableCellNode(node.__headerState, node.__colSpan, node.__width, node.__key);
60
+ }
61
+ afterCloneFrom(node) {
62
+ super.afterCloneFrom(node);
63
+ this.__rowSpan = node.__rowSpan;
64
+ this.__backgroundColor = node.__backgroundColor;
63
65
  }
64
66
  static importDOM() {
65
67
  return {
@@ -76,10 +78,7 @@ class TableCellNode extends lexical.ElementNode {
76
78
  static importJSON(serializedNode) {
77
79
  const colSpan = serializedNode.colSpan || 1;
78
80
  const rowSpan = serializedNode.rowSpan || 1;
79
- const cellNode = $createTableCellNode(serializedNode.headerState, colSpan, serializedNode.width || undefined);
80
- cellNode.__rowSpan = rowSpan;
81
- cellNode.__backgroundColor = serializedNode.backgroundColor || null;
82
- return cellNode;
81
+ return $createTableCellNode(serializedNode.headerState, colSpan, serializedNode.width || undefined).setRowSpan(rowSpan).setBackgroundColor(serializedNode.backgroundColor || null);
83
82
  }
84
83
  constructor(headerState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width, key) {
85
84
  super(key);
@@ -145,26 +144,28 @@ class TableCellNode extends lexical.ElementNode {
145
144
  };
146
145
  }
147
146
  getColSpan() {
148
- return this.__colSpan;
147
+ return this.getLatest().__colSpan;
149
148
  }
150
149
  setColSpan(colSpan) {
151
- this.getWritable().__colSpan = colSpan;
152
- return this;
150
+ const self = this.getWritable();
151
+ self.__colSpan = colSpan;
152
+ return self;
153
153
  }
154
154
  getRowSpan() {
155
- return this.__rowSpan;
155
+ return this.getLatest().__rowSpan;
156
156
  }
157
157
  setRowSpan(rowSpan) {
158
- this.getWritable().__rowSpan = rowSpan;
159
- return this;
158
+ const self = this.getWritable();
159
+ self.__rowSpan = rowSpan;
160
+ return self;
160
161
  }
161
162
  getTag() {
162
163
  return this.hasHeader() ? 'th' : 'td';
163
164
  }
164
- setHeaderStyles(headerState) {
165
+ setHeaderStyles(headerState, mask = TableCellHeaderStates.BOTH) {
165
166
  const self = this.getWritable();
166
- self.__headerState = headerState;
167
- return this.__headerState;
167
+ self.__headerState = headerState & mask | self.__headerState & ~mask;
168
+ return self;
168
169
  }
169
170
  getHeaderStyles() {
170
171
  return this.getLatest().__headerState;
@@ -172,7 +173,7 @@ class TableCellNode extends lexical.ElementNode {
172
173
  setWidth(width) {
173
174
  const self = this.getWritable();
174
175
  self.__width = width;
175
- return this.__width;
176
+ return self;
176
177
  }
177
178
  getWidth() {
178
179
  return this.getLatest().__width;
@@ -181,7 +182,9 @@ class TableCellNode extends lexical.ElementNode {
181
182
  return this.getLatest().__backgroundColor;
182
183
  }
183
184
  setBackgroundColor(newBackgroundColor) {
184
- this.getWritable().__backgroundColor = newBackgroundColor;
185
+ const self = this.getWritable();
186
+ self.__backgroundColor = newBackgroundColor;
187
+ return self;
185
188
  }
186
189
  toggleHeaderStyle(headerStateToToggle) {
187
190
  const self = this.getWritable();
@@ -769,7 +772,7 @@ function $deleteTableRow__EXPERIMENTAL() {
769
772
  }
770
773
  const rowNode = grid.getChildAtIndex(row);
771
774
  if (!$isTableRowNode(rowNode)) {
772
- throw Error(`Expected GridNode childAtIndex(${String(row)}) to be RowNode`);
775
+ throw Error(`Expected TableNode childAtIndex(${String(row)}) to be RowNode`);
773
776
  }
774
777
  rowNode.remove();
775
778
  }
@@ -879,18 +882,44 @@ function $unmergeCell() {
879
882
  const [cell, row, grid] = $getNodeTriplet(anchor);
880
883
  const colSpan = cell.__colSpan;
881
884
  const rowSpan = cell.__rowSpan;
885
+ if (colSpan === 1 && rowSpan === 1) {
886
+ return;
887
+ }
888
+ const [map, cellMap] = $computeTableMap(grid, cell, cell);
889
+ const {
890
+ startColumn,
891
+ startRow
892
+ } = cellMap;
893
+ // Create a heuristic for what the style of the unmerged cells should be
894
+ // based on whether every row or column already had that state before the
895
+ // unmerge.
896
+ const baseColStyle = cell.__headerState & TableCellHeaderStates.COLUMN;
897
+ const colStyles = Array.from({
898
+ length: colSpan
899
+ }, (_v, i) => {
900
+ let colStyle = baseColStyle;
901
+ for (let rowIdx = 0; colStyle !== 0 && rowIdx < map.length; rowIdx++) {
902
+ colStyle &= map[rowIdx][i + startColumn].cell.__headerState;
903
+ }
904
+ return colStyle;
905
+ });
906
+ const baseRowStyle = cell.__headerState & TableCellHeaderStates.ROW;
907
+ const rowStyles = Array.from({
908
+ length: rowSpan
909
+ }, (_v, i) => {
910
+ let rowStyle = baseRowStyle;
911
+ for (let colIdx = 0; rowStyle !== 0 && colIdx < map[0].length; colIdx++) {
912
+ rowStyle &= map[i + startRow][colIdx].cell.__headerState;
913
+ }
914
+ return rowStyle;
915
+ });
882
916
  if (colSpan > 1) {
883
917
  for (let i = 1; i < colSpan; i++) {
884
- cell.insertAfter($createTableCellNode(TableCellHeaderStates.NO_STATUS).append(lexical.$createParagraphNode()));
918
+ cell.insertAfter($createTableCellNode(colStyles[i] | rowStyles[0]).append(lexical.$createParagraphNode()));
885
919
  }
886
920
  cell.setColSpan(1);
887
921
  }
888
922
  if (rowSpan > 1) {
889
- const [map, cellMap] = $computeTableMap(grid, cell, cell);
890
- const {
891
- startColumn,
892
- startRow
893
- } = cellMap;
894
923
  let currentRowNode;
895
924
  for (let i = 1; i < rowSpan; i++) {
896
925
  const currentRow = startRow + i;
@@ -911,12 +940,12 @@ function $unmergeCell() {
911
940
  }
912
941
  }
913
942
  if (insertAfterCell === null) {
914
- for (let j = 0; j < colSpan; j++) {
915
- $insertFirst(currentRowNode, $createTableCellNode(TableCellHeaderStates.NO_STATUS).append(lexical.$createParagraphNode()));
943
+ for (let j = colSpan - 1; j >= 0; j--) {
944
+ $insertFirst(currentRowNode, $createTableCellNode(colStyles[j] | rowStyles[i]).append(lexical.$createParagraphNode()));
916
945
  }
917
946
  } else {
918
- for (let j = 0; j < colSpan; j++) {
919
- insertAfterCell.insertAfter($createTableCellNode(TableCellHeaderStates.NO_STATUS).append(lexical.$createParagraphNode()));
947
+ for (let j = colSpan - 1; j >= 0; j--) {
948
+ insertAfterCell.insertAfter($createTableCellNode(colStyles[j] | rowStyles[i]).append(lexical.$createParagraphNode()));
920
949
  }
921
950
  }
922
951
  }
@@ -926,10 +955,10 @@ function $unmergeCell() {
926
955
  function $computeTableMap(grid, cellA, cellB) {
927
956
  const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(grid, cellA, cellB);
928
957
  if (!(cellAValue !== null)) {
929
- throw Error(`Anchor not found in Grid`);
958
+ throw Error(`Anchor not found in Table`);
930
959
  }
931
960
  if (!(cellBValue !== null)) {
932
- throw Error(`Focus not found in Grid`);
961
+ throw Error(`Focus not found in Table`);
933
962
  }
934
963
  return [tableMap, cellAValue, cellBValue];
935
964
  }
@@ -937,49 +966,58 @@ function $computeTableMapSkipCellCheck(grid, cellA, cellB) {
937
966
  const tableMap = [];
938
967
  let cellAValue = null;
939
968
  let cellBValue = null;
940
- function write(startRow, startColumn, cell) {
941
- const value = {
942
- cell,
943
- startColumn,
944
- startRow
945
- };
946
- const rowSpan = cell.__rowSpan;
947
- const colSpan = cell.__colSpan;
948
- for (let i = 0; i < rowSpan; i++) {
949
- if (tableMap[startRow + i] === undefined) {
950
- tableMap[startRow + i] = [];
951
- }
952
- for (let j = 0; j < colSpan; j++) {
953
- tableMap[startRow + i][startColumn + j] = value;
954
- }
955
- }
956
- if (cellA !== null && cellA.is(cell)) {
957
- cellAValue = value;
958
- }
959
- if (cellB !== null && cellB.is(cell)) {
960
- cellBValue = value;
969
+ function getMapRow(i) {
970
+ let row = tableMap[i];
971
+ if (row === undefined) {
972
+ tableMap[i] = row = [];
961
973
  }
962
- }
963
- function isEmpty(row, column) {
964
- return tableMap[row] === undefined || tableMap[row][column] === undefined;
974
+ return row;
965
975
  }
966
976
  const gridChildren = grid.getChildren();
967
- for (let i = 0; i < gridChildren.length; i++) {
968
- const row = gridChildren[i];
977
+ for (let rowIdx = 0; rowIdx < gridChildren.length; rowIdx++) {
978
+ const row = gridChildren[rowIdx];
969
979
  if (!$isTableRowNode(row)) {
970
- throw Error(`Expected GridNode children to be TableRowNode`);
980
+ throw Error(`Expected TableNode children to be TableRowNode`);
971
981
  }
972
- const rowChildren = row.getChildren();
973
- let j = 0;
974
- for (const cell of rowChildren) {
982
+ for (let cell = row.getFirstChild(), colIdx = 0; cell != null; cell = cell.getNextSibling()) {
975
983
  if (!$isTableCellNode(cell)) {
976
984
  throw Error(`Expected TableRowNode children to be TableCellNode`);
985
+ } // Skip past any columns that were merged from a higher row
986
+ const startMapRow = getMapRow(rowIdx);
987
+ while (startMapRow[colIdx] !== undefined) {
988
+ colIdx++;
977
989
  }
978
- while (!isEmpty(i, j)) {
979
- j++;
990
+ const value = {
991
+ cell,
992
+ startColumn: colIdx,
993
+ startRow: rowIdx
994
+ };
995
+ const {
996
+ __rowSpan: rowSpan,
997
+ __colSpan: colSpan
998
+ } = cell;
999
+ for (let j = 0; j < rowSpan; j++) {
1000
+ if (rowIdx + j >= gridChildren.length) {
1001
+ // The table is non-rectangular with a rowSpan
1002
+ // below the last <tr> in the table.
1003
+ // We should probably handle this with a node transform
1004
+ // to ensure that tables are always rectangular but this
1005
+ // will avoid crashes such as #6584
1006
+ // Note that there are probably still latent bugs
1007
+ // regarding colSpan or general cell count mismatches.
1008
+ break;
1009
+ }
1010
+ const mapRow = getMapRow(rowIdx + j);
1011
+ for (let i = 0; i < colSpan; i++) {
1012
+ mapRow[colIdx + i] = value;
1013
+ }
1014
+ }
1015
+ if (cellA !== null && cellAValue === null && cellA.is(cell)) {
1016
+ cellAValue = value;
1017
+ }
1018
+ if (cellB !== null && cellBValue === null && cellB.is(cell)) {
1019
+ cellBValue = value;
980
1020
  }
981
- write(i, j, cell);
982
- j += cell.__colSpan;
983
1021
  }
984
1022
  }
985
1023
  return [tableMap, cellAValue, cellBValue];
@@ -1007,7 +1045,7 @@ function $getNodeTriplet(source) {
1007
1045
  }
1008
1046
  const grid = row.getParent();
1009
1047
  if (!$isTableNode(grid)) {
1010
- throw Error(`Expected TableRowNode to have a parent GridNode`);
1048
+ throw Error(`Expected TableRowNode to have a parent TableNode`);
1011
1049
  }
1012
1050
  return [cell, row, grid];
1013
1051
  }
@@ -1149,9 +1187,9 @@ class TableSelection {
1149
1187
  throw Error(`getCellRect: expected to find focusCellNode`);
1150
1188
  }
1151
1189
  const startX = Math.min(anchorCellNodeRect.columnIndex, focusCellNodeRect.columnIndex);
1152
- const stopX = Math.max(anchorCellNodeRect.columnIndex, focusCellNodeRect.columnIndex);
1190
+ const stopX = Math.max(anchorCellNodeRect.columnIndex + anchorCellNodeRect.colSpan - 1, focusCellNodeRect.columnIndex + focusCellNodeRect.colSpan - 1);
1153
1191
  const startY = Math.min(anchorCellNodeRect.rowIndex, focusCellNodeRect.rowIndex);
1154
- const stopY = Math.max(anchorCellNodeRect.rowIndex, focusCellNodeRect.rowIndex);
1192
+ const stopY = Math.max(anchorCellNodeRect.rowIndex + anchorCellNodeRect.rowSpan - 1, focusCellNodeRect.rowIndex + focusCellNodeRect.rowSpan - 1);
1155
1193
  return {
1156
1194
  fromX: Math.min(startX, stopX),
1157
1195
  fromY: Math.min(startY, stopY),
@@ -1265,7 +1303,10 @@ class TableSelection {
1265
1303
  exploredMaxRow = nextRow;
1266
1304
  }
1267
1305
  }
1268
- const nodes = [tableNode];
1306
+
1307
+ // We use a Map here because merged cells in the grid would otherwise
1308
+ // show up multiple times in the nodes array
1309
+ const nodeMap = new Map([[tableNode.getKey(), tableNode]]);
1269
1310
  let lastRow = null;
1270
1311
  for (let i = minRow; i <= maxRow; i++) {
1271
1312
  for (let j = minColumn; j <= maxColumn; j++) {
@@ -1277,12 +1318,16 @@ class TableSelection {
1277
1318
  throw Error(`Expected TableCellNode parent to be a TableRowNode`);
1278
1319
  }
1279
1320
  if (currentRow !== lastRow) {
1280
- nodes.push(currentRow);
1321
+ nodeMap.set(currentRow.getKey(), currentRow);
1322
+ }
1323
+ nodeMap.set(cell.getKey(), cell);
1324
+ for (const child of $getChildrenRecursively(cell)) {
1325
+ nodeMap.set(child.getKey(), child);
1281
1326
  }
1282
- nodes.push(cell, ...$getChildrenRecursively(cell));
1283
1327
  lastRow = currentRow;
1284
1328
  }
1285
1329
  }
1330
+ const nodes = Array.from(nodeMap.values());
1286
1331
  if (!lexical.isCurrentlyReadOnlyMode()) {
1287
1332
  this._cachedNodes = nodes;
1288
1333
  }
@@ -54,10 +54,12 @@ class TableCellNode extends ElementNode {
54
54
  return 'tablecell';
55
55
  }
56
56
  static clone(node) {
57
- const cellNode = new TableCellNode(node.__headerState, node.__colSpan, node.__width, node.__key);
58
- cellNode.__rowSpan = node.__rowSpan;
59
- cellNode.__backgroundColor = node.__backgroundColor;
60
- return cellNode;
57
+ return new TableCellNode(node.__headerState, node.__colSpan, node.__width, node.__key);
58
+ }
59
+ afterCloneFrom(node) {
60
+ super.afterCloneFrom(node);
61
+ this.__rowSpan = node.__rowSpan;
62
+ this.__backgroundColor = node.__backgroundColor;
61
63
  }
62
64
  static importDOM() {
63
65
  return {
@@ -74,10 +76,7 @@ class TableCellNode extends ElementNode {
74
76
  static importJSON(serializedNode) {
75
77
  const colSpan = serializedNode.colSpan || 1;
76
78
  const rowSpan = serializedNode.rowSpan || 1;
77
- const cellNode = $createTableCellNode(serializedNode.headerState, colSpan, serializedNode.width || undefined);
78
- cellNode.__rowSpan = rowSpan;
79
- cellNode.__backgroundColor = serializedNode.backgroundColor || null;
80
- return cellNode;
79
+ return $createTableCellNode(serializedNode.headerState, colSpan, serializedNode.width || undefined).setRowSpan(rowSpan).setBackgroundColor(serializedNode.backgroundColor || null);
81
80
  }
82
81
  constructor(headerState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width, key) {
83
82
  super(key);
@@ -143,26 +142,28 @@ class TableCellNode extends ElementNode {
143
142
  };
144
143
  }
145
144
  getColSpan() {
146
- return this.__colSpan;
145
+ return this.getLatest().__colSpan;
147
146
  }
148
147
  setColSpan(colSpan) {
149
- this.getWritable().__colSpan = colSpan;
150
- return this;
148
+ const self = this.getWritable();
149
+ self.__colSpan = colSpan;
150
+ return self;
151
151
  }
152
152
  getRowSpan() {
153
- return this.__rowSpan;
153
+ return this.getLatest().__rowSpan;
154
154
  }
155
155
  setRowSpan(rowSpan) {
156
- this.getWritable().__rowSpan = rowSpan;
157
- return this;
156
+ const self = this.getWritable();
157
+ self.__rowSpan = rowSpan;
158
+ return self;
158
159
  }
159
160
  getTag() {
160
161
  return this.hasHeader() ? 'th' : 'td';
161
162
  }
162
- setHeaderStyles(headerState) {
163
+ setHeaderStyles(headerState, mask = TableCellHeaderStates.BOTH) {
163
164
  const self = this.getWritable();
164
- self.__headerState = headerState;
165
- return this.__headerState;
165
+ self.__headerState = headerState & mask | self.__headerState & ~mask;
166
+ return self;
166
167
  }
167
168
  getHeaderStyles() {
168
169
  return this.getLatest().__headerState;
@@ -170,7 +171,7 @@ class TableCellNode extends ElementNode {
170
171
  setWidth(width) {
171
172
  const self = this.getWritable();
172
173
  self.__width = width;
173
- return this.__width;
174
+ return self;
174
175
  }
175
176
  getWidth() {
176
177
  return this.getLatest().__width;
@@ -179,7 +180,9 @@ class TableCellNode extends ElementNode {
179
180
  return this.getLatest().__backgroundColor;
180
181
  }
181
182
  setBackgroundColor(newBackgroundColor) {
182
- this.getWritable().__backgroundColor = newBackgroundColor;
183
+ const self = this.getWritable();
184
+ self.__backgroundColor = newBackgroundColor;
185
+ return self;
183
186
  }
184
187
  toggleHeaderStyle(headerStateToToggle) {
185
188
  const self = this.getWritable();
@@ -767,7 +770,7 @@ function $deleteTableRow__EXPERIMENTAL() {
767
770
  }
768
771
  const rowNode = grid.getChildAtIndex(row);
769
772
  if (!$isTableRowNode(rowNode)) {
770
- throw Error(`Expected GridNode childAtIndex(${String(row)}) to be RowNode`);
773
+ throw Error(`Expected TableNode childAtIndex(${String(row)}) to be RowNode`);
771
774
  }
772
775
  rowNode.remove();
773
776
  }
@@ -877,18 +880,44 @@ function $unmergeCell() {
877
880
  const [cell, row, grid] = $getNodeTriplet(anchor);
878
881
  const colSpan = cell.__colSpan;
879
882
  const rowSpan = cell.__rowSpan;
883
+ if (colSpan === 1 && rowSpan === 1) {
884
+ return;
885
+ }
886
+ const [map, cellMap] = $computeTableMap(grid, cell, cell);
887
+ const {
888
+ startColumn,
889
+ startRow
890
+ } = cellMap;
891
+ // Create a heuristic for what the style of the unmerged cells should be
892
+ // based on whether every row or column already had that state before the
893
+ // unmerge.
894
+ const baseColStyle = cell.__headerState & TableCellHeaderStates.COLUMN;
895
+ const colStyles = Array.from({
896
+ length: colSpan
897
+ }, (_v, i) => {
898
+ let colStyle = baseColStyle;
899
+ for (let rowIdx = 0; colStyle !== 0 && rowIdx < map.length; rowIdx++) {
900
+ colStyle &= map[rowIdx][i + startColumn].cell.__headerState;
901
+ }
902
+ return colStyle;
903
+ });
904
+ const baseRowStyle = cell.__headerState & TableCellHeaderStates.ROW;
905
+ const rowStyles = Array.from({
906
+ length: rowSpan
907
+ }, (_v, i) => {
908
+ let rowStyle = baseRowStyle;
909
+ for (let colIdx = 0; rowStyle !== 0 && colIdx < map[0].length; colIdx++) {
910
+ rowStyle &= map[i + startRow][colIdx].cell.__headerState;
911
+ }
912
+ return rowStyle;
913
+ });
880
914
  if (colSpan > 1) {
881
915
  for (let i = 1; i < colSpan; i++) {
882
- cell.insertAfter($createTableCellNode(TableCellHeaderStates.NO_STATUS).append($createParagraphNode()));
916
+ cell.insertAfter($createTableCellNode(colStyles[i] | rowStyles[0]).append($createParagraphNode()));
883
917
  }
884
918
  cell.setColSpan(1);
885
919
  }
886
920
  if (rowSpan > 1) {
887
- const [map, cellMap] = $computeTableMap(grid, cell, cell);
888
- const {
889
- startColumn,
890
- startRow
891
- } = cellMap;
892
921
  let currentRowNode;
893
922
  for (let i = 1; i < rowSpan; i++) {
894
923
  const currentRow = startRow + i;
@@ -909,12 +938,12 @@ function $unmergeCell() {
909
938
  }
910
939
  }
911
940
  if (insertAfterCell === null) {
912
- for (let j = 0; j < colSpan; j++) {
913
- $insertFirst(currentRowNode, $createTableCellNode(TableCellHeaderStates.NO_STATUS).append($createParagraphNode()));
941
+ for (let j = colSpan - 1; j >= 0; j--) {
942
+ $insertFirst(currentRowNode, $createTableCellNode(colStyles[j] | rowStyles[i]).append($createParagraphNode()));
914
943
  }
915
944
  } else {
916
- for (let j = 0; j < colSpan; j++) {
917
- insertAfterCell.insertAfter($createTableCellNode(TableCellHeaderStates.NO_STATUS).append($createParagraphNode()));
945
+ for (let j = colSpan - 1; j >= 0; j--) {
946
+ insertAfterCell.insertAfter($createTableCellNode(colStyles[j] | rowStyles[i]).append($createParagraphNode()));
918
947
  }
919
948
  }
920
949
  }
@@ -924,10 +953,10 @@ function $unmergeCell() {
924
953
  function $computeTableMap(grid, cellA, cellB) {
925
954
  const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(grid, cellA, cellB);
926
955
  if (!(cellAValue !== null)) {
927
- throw Error(`Anchor not found in Grid`);
956
+ throw Error(`Anchor not found in Table`);
928
957
  }
929
958
  if (!(cellBValue !== null)) {
930
- throw Error(`Focus not found in Grid`);
959
+ throw Error(`Focus not found in Table`);
931
960
  }
932
961
  return [tableMap, cellAValue, cellBValue];
933
962
  }
@@ -935,49 +964,58 @@ function $computeTableMapSkipCellCheck(grid, cellA, cellB) {
935
964
  const tableMap = [];
936
965
  let cellAValue = null;
937
966
  let cellBValue = null;
938
- function write(startRow, startColumn, cell) {
939
- const value = {
940
- cell,
941
- startColumn,
942
- startRow
943
- };
944
- const rowSpan = cell.__rowSpan;
945
- const colSpan = cell.__colSpan;
946
- for (let i = 0; i < rowSpan; i++) {
947
- if (tableMap[startRow + i] === undefined) {
948
- tableMap[startRow + i] = [];
949
- }
950
- for (let j = 0; j < colSpan; j++) {
951
- tableMap[startRow + i][startColumn + j] = value;
952
- }
953
- }
954
- if (cellA !== null && cellA.is(cell)) {
955
- cellAValue = value;
956
- }
957
- if (cellB !== null && cellB.is(cell)) {
958
- cellBValue = value;
967
+ function getMapRow(i) {
968
+ let row = tableMap[i];
969
+ if (row === undefined) {
970
+ tableMap[i] = row = [];
959
971
  }
960
- }
961
- function isEmpty(row, column) {
962
- return tableMap[row] === undefined || tableMap[row][column] === undefined;
972
+ return row;
963
973
  }
964
974
  const gridChildren = grid.getChildren();
965
- for (let i = 0; i < gridChildren.length; i++) {
966
- const row = gridChildren[i];
975
+ for (let rowIdx = 0; rowIdx < gridChildren.length; rowIdx++) {
976
+ const row = gridChildren[rowIdx];
967
977
  if (!$isTableRowNode(row)) {
968
- throw Error(`Expected GridNode children to be TableRowNode`);
978
+ throw Error(`Expected TableNode children to be TableRowNode`);
969
979
  }
970
- const rowChildren = row.getChildren();
971
- let j = 0;
972
- for (const cell of rowChildren) {
980
+ for (let cell = row.getFirstChild(), colIdx = 0; cell != null; cell = cell.getNextSibling()) {
973
981
  if (!$isTableCellNode(cell)) {
974
982
  throw Error(`Expected TableRowNode children to be TableCellNode`);
983
+ } // Skip past any columns that were merged from a higher row
984
+ const startMapRow = getMapRow(rowIdx);
985
+ while (startMapRow[colIdx] !== undefined) {
986
+ colIdx++;
975
987
  }
976
- while (!isEmpty(i, j)) {
977
- j++;
988
+ const value = {
989
+ cell,
990
+ startColumn: colIdx,
991
+ startRow: rowIdx
992
+ };
993
+ const {
994
+ __rowSpan: rowSpan,
995
+ __colSpan: colSpan
996
+ } = cell;
997
+ for (let j = 0; j < rowSpan; j++) {
998
+ if (rowIdx + j >= gridChildren.length) {
999
+ // The table is non-rectangular with a rowSpan
1000
+ // below the last <tr> in the table.
1001
+ // We should probably handle this with a node transform
1002
+ // to ensure that tables are always rectangular but this
1003
+ // will avoid crashes such as #6584
1004
+ // Note that there are probably still latent bugs
1005
+ // regarding colSpan or general cell count mismatches.
1006
+ break;
1007
+ }
1008
+ const mapRow = getMapRow(rowIdx + j);
1009
+ for (let i = 0; i < colSpan; i++) {
1010
+ mapRow[colIdx + i] = value;
1011
+ }
1012
+ }
1013
+ if (cellA !== null && cellAValue === null && cellA.is(cell)) {
1014
+ cellAValue = value;
1015
+ }
1016
+ if (cellB !== null && cellBValue === null && cellB.is(cell)) {
1017
+ cellBValue = value;
978
1018
  }
979
- write(i, j, cell);
980
- j += cell.__colSpan;
981
1019
  }
982
1020
  }
983
1021
  return [tableMap, cellAValue, cellBValue];
@@ -1005,7 +1043,7 @@ function $getNodeTriplet(source) {
1005
1043
  }
1006
1044
  const grid = row.getParent();
1007
1045
  if (!$isTableNode(grid)) {
1008
- throw Error(`Expected TableRowNode to have a parent GridNode`);
1046
+ throw Error(`Expected TableRowNode to have a parent TableNode`);
1009
1047
  }
1010
1048
  return [cell, row, grid];
1011
1049
  }
@@ -1147,9 +1185,9 @@ class TableSelection {
1147
1185
  throw Error(`getCellRect: expected to find focusCellNode`);
1148
1186
  }
1149
1187
  const startX = Math.min(anchorCellNodeRect.columnIndex, focusCellNodeRect.columnIndex);
1150
- const stopX = Math.max(anchorCellNodeRect.columnIndex, focusCellNodeRect.columnIndex);
1188
+ const stopX = Math.max(anchorCellNodeRect.columnIndex + anchorCellNodeRect.colSpan - 1, focusCellNodeRect.columnIndex + focusCellNodeRect.colSpan - 1);
1151
1189
  const startY = Math.min(anchorCellNodeRect.rowIndex, focusCellNodeRect.rowIndex);
1152
- const stopY = Math.max(anchorCellNodeRect.rowIndex, focusCellNodeRect.rowIndex);
1190
+ const stopY = Math.max(anchorCellNodeRect.rowIndex + anchorCellNodeRect.rowSpan - 1, focusCellNodeRect.rowIndex + focusCellNodeRect.rowSpan - 1);
1153
1191
  return {
1154
1192
  fromX: Math.min(startX, stopX),
1155
1193
  fromY: Math.min(startY, stopY),
@@ -1263,7 +1301,10 @@ class TableSelection {
1263
1301
  exploredMaxRow = nextRow;
1264
1302
  }
1265
1303
  }
1266
- const nodes = [tableNode];
1304
+
1305
+ // We use a Map here because merged cells in the grid would otherwise
1306
+ // show up multiple times in the nodes array
1307
+ const nodeMap = new Map([[tableNode.getKey(), tableNode]]);
1267
1308
  let lastRow = null;
1268
1309
  for (let i = minRow; i <= maxRow; i++) {
1269
1310
  for (let j = minColumn; j <= maxColumn; j++) {
@@ -1275,12 +1316,16 @@ class TableSelection {
1275
1316
  throw Error(`Expected TableCellNode parent to be a TableRowNode`);
1276
1317
  }
1277
1318
  if (currentRow !== lastRow) {
1278
- nodes.push(currentRow);
1319
+ nodeMap.set(currentRow.getKey(), currentRow);
1320
+ }
1321
+ nodeMap.set(cell.getKey(), cell);
1322
+ for (const child of $getChildrenRecursively(cell)) {
1323
+ nodeMap.set(child.getKey(), child);
1279
1324
  }
1280
- nodes.push(cell, ...$getChildrenRecursively(cell));
1281
1325
  lastRow = currentRow;
1282
1326
  }
1283
1327
  }
1328
+ const nodes = Array.from(nodeMap.values());
1284
1329
  if (!isCurrentlyReadOnlyMode()) {
1285
1330
  this._cachedNodes = nodes;
1286
1331
  }
@@ -62,12 +62,12 @@ declare export class TableCellNode extends ElementNode {
62
62
  getRowSpan(): number;
63
63
  setRowSpan(rowSpan: number): this;
64
64
  getTag(): string;
65
- setHeaderStyles(headerState: TableCellHeaderState): TableCellHeaderState;
65
+ setHeaderStyles(headerState: TableCellHeaderState, mask?: TableCellHeaderState): TableCellNode;
66
66
  getHeaderStyles(): TableCellHeaderState;
67
- setWidth(width: number): ?number;
67
+ setWidth(width: number): TableCellNode;
68
68
  getWidth(): ?number;
69
69
  getBackgroundColor(): null | string;
70
- setBackgroundColor(newBackgroundColor: null | string): void;
70
+ setBackgroundColor(newBackgroundColor: null | string): TableCellNode;
71
71
  toggleHeaderStyle(headerState: TableCellHeaderState): TableCellNode;
72
72
  hasHeader(): boolean;
73
73
  updateDOM(prevNode: TableCellNode): boolean;