@lexical/table 0.20.1-nightly.20241121.0 → 0.20.1-nightly.20241125.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.
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { addClassNamesToElement, $findMatchingParent, removeClassNamesFromElement, objectKlassEquals, isHTMLElement } from '@lexical/utils';
10
- import { ElementNode, $createParagraphNode, $isElementNode, $isLineBreakNode, $isTextNode, $applyNodeReplacement, createCommand, $createTextNode, $getSelection, $isRangeSelection, $createPoint, $isParagraphNode, $normalizeSelection__EXPERIMENTAL, $getNodeByKey, isCurrentlyReadOnlyMode, TEXT_TYPE_TO_FORMAT, $getEditor, $setSelection, SELECTION_CHANGE_COMMAND, $getNearestNodeFromDOMNode, $createRangeSelection, $getRoot, KEY_ARROW_DOWN_COMMAND, COMMAND_PRIORITY_HIGH, KEY_ARROW_UP_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ESCAPE_COMMAND, DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND, COMMAND_PRIORITY_CRITICAL, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, CUT_COMMAND, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_TAB_COMMAND, FOCUS_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, $getPreviousSelection, $createRangeSelectionFromDom, INSERT_PARAGRAPH_COMMAND, $isRootOrShadowRoot, $isDecoratorNode, setDOMUnmanaged } from 'lexical';
10
+ import { ElementNode, $createParagraphNode, $isElementNode, $isLineBreakNode, $isTextNode, $applyNodeReplacement, createCommand, $createTextNode, $getSelection, $isRangeSelection, $createPoint, $isParagraphNode, $normalizeSelection__EXPERIMENTAL, isCurrentlyReadOnlyMode, TEXT_TYPE_TO_FORMAT, $getNodeByKey, $getEditor, $setSelection, SELECTION_CHANGE_COMMAND, getDOMSelection, $getNearestNodeFromDOMNode, $createRangeSelection, $getRoot, COMMAND_PRIORITY_HIGH, KEY_ESCAPE_COMMAND, COMMAND_PRIORITY_CRITICAL, CUT_COMMAND, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, CONTROLLED_TEXT_INSERTION_COMMAND, KEY_TAB_COMMAND, FOCUS_COMMAND, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, $getPreviousSelection, $createRangeSelectionFromDom, INSERT_PARAGRAPH_COMMAND, $isRootOrShadowRoot, $isDecoratorNode, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, setDOMUnmanaged } from 'lexical';
11
11
  import { copyToClipboard, $getClipboardDataFromSelection } from '@lexical/clipboard';
12
12
 
13
13
  /**
@@ -104,31 +104,24 @@ class TableCellNode extends ElementNode {
104
104
  return element;
105
105
  }
106
106
  exportDOM(editor) {
107
- const {
108
- element
109
- } = super.exportDOM(editor);
110
- if (element) {
111
- const element_ = element;
112
- element_.style.border = '1px solid black';
107
+ const output = super.exportDOM(editor);
108
+ if (output.element) {
109
+ const element = output.element;
110
+ element.style.border = '1px solid black';
113
111
  if (this.__colSpan > 1) {
114
- element_.colSpan = this.__colSpan;
112
+ element.colSpan = this.__colSpan;
115
113
  }
116
114
  if (this.__rowSpan > 1) {
117
- element_.rowSpan = this.__rowSpan;
115
+ element.rowSpan = this.__rowSpan;
118
116
  }
119
- element_.style.width = `${this.getWidth() || COLUMN_WIDTH}px`;
120
- element_.style.verticalAlign = 'top';
121
- element_.style.textAlign = 'start';
122
- const backgroundColor = this.getBackgroundColor();
123
- if (backgroundColor !== null) {
124
- element_.style.backgroundColor = backgroundColor;
125
- } else if (this.hasHeader()) {
126
- element_.style.backgroundColor = '#f2f3f5';
117
+ element.style.width = `${this.getWidth() || COLUMN_WIDTH}px`;
118
+ element.style.verticalAlign = 'top';
119
+ element.style.textAlign = 'start';
120
+ if (this.__backgroundColor === null && this.hasHeader()) {
121
+ element.style.backgroundColor = '#f2f3f5';
127
122
  }
128
123
  }
129
- return {
130
- element
131
- };
124
+ return output;
132
125
  }
133
126
  exportJSON() {
134
127
  return {
@@ -296,6 +289,18 @@ const INSERT_TABLE_COMMAND = createCommand('INSERT_TABLE_COMMAND');
296
289
 
297
290
  const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
298
291
 
292
+ /**
293
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
294
+ *
295
+ * This source code is licensed under the MIT license found in the
296
+ * LICENSE file in the root directory of this source tree.
297
+ *
298
+ */
299
+
300
+ const documentMode = CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;
301
+ const IS_FIREFOX = CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
302
+ CAN_USE_DOM && 'InputEvent' in window && !documentMode ? 'getTargetRanges' in new window.InputEvent('input') : false;
303
+
299
304
  /**
300
305
  * Copyright (c) Meta Platforms, Inc. and affiliates.
301
306
  *
@@ -980,8 +985,8 @@ function $unmergeCell() {
980
985
  cell.setRowSpan(1);
981
986
  }
982
987
  }
983
- function $computeTableMap(grid, cellA, cellB) {
984
- const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(grid, cellA, cellB);
988
+ function $computeTableMap(tableNode, cellA, cellB) {
989
+ const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(tableNode, cellA, cellB);
985
990
  if (!(cellAValue !== null)) {
986
991
  throw Error(`Anchor not found in Table`);
987
992
  }
@@ -990,7 +995,7 @@ function $computeTableMap(grid, cellA, cellB) {
990
995
  }
991
996
  return [tableMap, cellAValue, cellBValue];
992
997
  }
993
- function $computeTableMapSkipCellCheck(grid, cellA, cellB) {
998
+ function $computeTableMapSkipCellCheck(tableNode, cellA, cellB) {
994
999
  const tableMap = [];
995
1000
  let cellAValue = null;
996
1001
  let cellBValue = null;
@@ -1001,7 +1006,7 @@ function $computeTableMapSkipCellCheck(grid, cellA, cellB) {
1001
1006
  }
1002
1007
  return row;
1003
1008
  }
1004
- const gridChildren = grid.getChildren();
1009
+ const gridChildren = tableNode.getChildren();
1005
1010
  for (let rowIdx = 0; rowIdx < gridChildren.length; rowIdx++) {
1006
1011
  const row = gridChildren[rowIdx];
1007
1012
  if (!$isTableRowNode(row)) {
@@ -1077,6 +1082,99 @@ function $getNodeTriplet(source) {
1077
1082
  }
1078
1083
  return [cell, row, grid];
1079
1084
  }
1085
+ function $computeTableCellRectSpans(map, boundary) {
1086
+ const {
1087
+ minColumn,
1088
+ maxColumn,
1089
+ minRow,
1090
+ maxRow
1091
+ } = boundary;
1092
+ let topSpan = 1;
1093
+ let leftSpan = 1;
1094
+ let rightSpan = 1;
1095
+ let bottomSpan = 1;
1096
+ const topRow = map[minRow];
1097
+ const bottomRow = map[maxRow];
1098
+ for (let col = minColumn; col <= maxColumn; col++) {
1099
+ topSpan = Math.max(topSpan, topRow[col].cell.__rowSpan);
1100
+ bottomSpan = Math.max(bottomSpan, bottomRow[col].cell.__rowSpan);
1101
+ }
1102
+ for (let row = minRow; row <= maxRow; row++) {
1103
+ leftSpan = Math.max(leftSpan, map[row][minColumn].cell.__colSpan);
1104
+ rightSpan = Math.max(rightSpan, map[row][maxColumn].cell.__colSpan);
1105
+ }
1106
+ return {
1107
+ bottomSpan,
1108
+ leftSpan,
1109
+ rightSpan,
1110
+ topSpan
1111
+ };
1112
+ }
1113
+ function $computeTableCellRectBoundary(map, cellAMap, cellBMap) {
1114
+ let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn);
1115
+ let minRow = Math.min(cellAMap.startRow, cellBMap.startRow);
1116
+ let maxColumn = Math.max(cellAMap.startColumn + cellAMap.cell.__colSpan - 1, cellBMap.startColumn + cellBMap.cell.__colSpan - 1);
1117
+ let maxRow = Math.max(cellAMap.startRow + cellAMap.cell.__rowSpan - 1, cellBMap.startRow + cellBMap.cell.__rowSpan - 1);
1118
+ let exploredMinColumn = minColumn;
1119
+ let exploredMinRow = minRow;
1120
+ let exploredMaxColumn = minColumn;
1121
+ let exploredMaxRow = minRow;
1122
+ function expandBoundary(mapValue) {
1123
+ const {
1124
+ cell,
1125
+ startColumn: cellStartColumn,
1126
+ startRow: cellStartRow
1127
+ } = mapValue;
1128
+ minColumn = Math.min(minColumn, cellStartColumn);
1129
+ minRow = Math.min(minRow, cellStartRow);
1130
+ maxColumn = Math.max(maxColumn, cellStartColumn + cell.__colSpan - 1);
1131
+ maxRow = Math.max(maxRow, cellStartRow + cell.__rowSpan - 1);
1132
+ }
1133
+ while (minColumn < exploredMinColumn || minRow < exploredMinRow || maxColumn > exploredMaxColumn || maxRow > exploredMaxRow) {
1134
+ if (minColumn < exploredMinColumn) {
1135
+ // Expand on the left
1136
+ const rowDiff = exploredMaxRow - exploredMinRow;
1137
+ const previousColumn = exploredMinColumn - 1;
1138
+ for (let i = 0; i <= rowDiff; i++) {
1139
+ expandBoundary(map[exploredMinRow + i][previousColumn]);
1140
+ }
1141
+ exploredMinColumn = previousColumn;
1142
+ }
1143
+ if (minRow < exploredMinRow) {
1144
+ // Expand on top
1145
+ const columnDiff = exploredMaxColumn - exploredMinColumn;
1146
+ const previousRow = exploredMinRow - 1;
1147
+ for (let i = 0; i <= columnDiff; i++) {
1148
+ expandBoundary(map[previousRow][exploredMinColumn + i]);
1149
+ }
1150
+ exploredMinRow = previousRow;
1151
+ }
1152
+ if (maxColumn > exploredMaxColumn) {
1153
+ // Expand on the right
1154
+ const rowDiff = exploredMaxRow - exploredMinRow;
1155
+ const nextColumn = exploredMaxColumn + 1;
1156
+ for (let i = 0; i <= rowDiff; i++) {
1157
+ expandBoundary(map[exploredMinRow + i][nextColumn]);
1158
+ }
1159
+ exploredMaxColumn = nextColumn;
1160
+ }
1161
+ if (maxRow > exploredMaxRow) {
1162
+ // Expand on the bottom
1163
+ const columnDiff = exploredMaxColumn - exploredMinColumn;
1164
+ const nextRow = exploredMaxRow + 1;
1165
+ for (let i = 0; i <= columnDiff; i++) {
1166
+ expandBoundary(map[nextRow][exploredMinColumn + i]);
1167
+ }
1168
+ exploredMaxRow = nextRow;
1169
+ }
1170
+ }
1171
+ return {
1172
+ maxColumn,
1173
+ maxRow,
1174
+ minColumn,
1175
+ minRow
1176
+ };
1177
+ }
1080
1178
  function $getTableCellNodeRect(tableCellNode) {
1081
1179
  const [cellNode,, gridNode] = $getNodeTriplet(tableCellNode);
1082
1180
  const rows = gridNode.getChildren();
@@ -1131,6 +1229,38 @@ function $getTableCellNodeRect(tableCellNode) {
1131
1229
  *
1132
1230
  */
1133
1231
 
1232
+ function $getCellNodes(tableSelection) {
1233
+ const [[anchorNode, anchorCell, anchorRow, anchorTable], [focusNode, focusCell, focusRow, focusTable]] = ['anchor', 'focus'].map(k => {
1234
+ const node = tableSelection[k].getNode();
1235
+ const cellNode = $findMatchingParent(node, $isTableCellNode);
1236
+ if (!$isTableCellNode(cellNode)) {
1237
+ throw Error(`Expected TableSelection ${k} to be (or a child of) TableCellNode, got key ${node.getKey()} of type ${node.getType()}`);
1238
+ }
1239
+ const rowNode = cellNode.getParent();
1240
+ if (!$isTableRowNode(rowNode)) {
1241
+ throw Error(`Expected TableSelection ${k} cell parent to be a TableRowNode`);
1242
+ }
1243
+ const tableNode = rowNode.getParent();
1244
+ if (!$isTableNode(tableNode)) {
1245
+ throw Error(`Expected TableSelection ${k} row parent to be a TableNode`);
1246
+ }
1247
+ return [node, cellNode, rowNode, tableNode];
1248
+ });
1249
+ // TODO: nested tables may violate this
1250
+ if (!anchorTable.is(focusTable)) {
1251
+ throw Error(`Expected TableSelection anchor and focus to be in the same table`);
1252
+ }
1253
+ return {
1254
+ anchorCell,
1255
+ anchorNode,
1256
+ anchorRow,
1257
+ anchorTable,
1258
+ focusCell,
1259
+ focusNode,
1260
+ focusRow,
1261
+ focusTable
1262
+ };
1263
+ }
1134
1264
  class TableSelection {
1135
1265
  constructor(tableKey, anchor, focus) {
1136
1266
  this.anchor = anchor;
@@ -1145,6 +1275,17 @@ class TableSelection {
1145
1275
  return [this.anchor, this.focus];
1146
1276
  }
1147
1277
 
1278
+ /**
1279
+ * {@link $createTableSelection} unfortunately makes it very easy to create
1280
+ * nonsense selections, so we have a method to see if the selection probably
1281
+ * makes sense.
1282
+ *
1283
+ * @returns true if the TableSelection is (probably) valid
1284
+ */
1285
+ isValid() {
1286
+ return this.tableKey !== 'root' && this.anchor.key !== 'root' && this.anchor.type === 'element' && this.focus.key !== 'root' && this.focus.type === 'element';
1287
+ }
1288
+
1148
1289
  /**
1149
1290
  * Returns whether the Selection is "backwards", meaning the focus
1150
1291
  * logically precedes the anchor in the EditorState.
@@ -1160,20 +1301,18 @@ class TableSelection {
1160
1301
  this._cachedNodes = nodes;
1161
1302
  }
1162
1303
  is(selection) {
1163
- if (!$isTableSelection(selection)) {
1164
- return false;
1165
- }
1166
- return this.tableKey === selection.tableKey && this.anchor.is(selection.anchor) && this.focus.is(selection.focus);
1304
+ return $isTableSelection(selection) && this.tableKey === selection.tableKey && this.anchor.is(selection.anchor) && this.focus.is(selection.focus);
1167
1305
  }
1168
1306
  set(tableKey, anchorCellKey, focusCellKey) {
1169
- this.dirty = true;
1307
+ // note: closure compiler's acorn does not support ||=
1308
+ this.dirty = this.dirty || tableKey !== this.tableKey || anchorCellKey !== this.anchor.key || focusCellKey !== this.focus.key;
1170
1309
  this.tableKey = tableKey;
1171
1310
  this.anchor.key = anchorCellKey;
1172
1311
  this.focus.key = focusCellKey;
1173
1312
  this._cachedNodes = null;
1174
1313
  }
1175
1314
  clone() {
1176
- return new TableSelection(this.tableKey, this.anchor, this.focus);
1315
+ return new TableSelection(this.tableKey, $createPoint(this.anchor.key, this.anchor.offset, this.anchor.type), $createPoint(this.focus.key, this.focus.offset, this.focus.type));
1177
1316
  }
1178
1317
  isCollapsed() {
1179
1318
  return false;
@@ -1218,19 +1357,15 @@ class TableSelection {
1218
1357
 
1219
1358
  // TODO Deprecate this method. It's confusing when used with colspan|rowspan
1220
1359
  getShape() {
1221
- const anchorCellNode = $getNodeByKey(this.anchor.key);
1222
- if (!$isTableCellNode(anchorCellNode)) {
1223
- throw Error(`Expected TableSelection anchor to be (or a child of) TableCellNode`);
1224
- }
1225
- const anchorCellNodeRect = $getTableCellNodeRect(anchorCellNode);
1360
+ const {
1361
+ anchorCell,
1362
+ focusCell
1363
+ } = $getCellNodes(this);
1364
+ const anchorCellNodeRect = $getTableCellNodeRect(anchorCell);
1226
1365
  if (!(anchorCellNodeRect !== null)) {
1227
1366
  throw Error(`getCellRect: expected to find AnchorNode`);
1228
1367
  }
1229
- const focusCellNode = $getNodeByKey(this.focus.key);
1230
- if (!$isTableCellNode(focusCellNode)) {
1231
- throw Error(`Expected TableSelection focus to be (or a child of) TableCellNode`);
1232
- }
1233
- const focusCellNodeRect = $getTableCellNodeRect(focusCellNode);
1368
+ const focusCellNodeRect = $getTableCellNodeRect(focusCell);
1234
1369
  if (!(focusCellNodeRect !== null)) {
1235
1370
  throw Error(`getCellRect: expected to find focusCellNode`);
1236
1371
  }
@@ -1246,29 +1381,18 @@ class TableSelection {
1246
1381
  };
1247
1382
  }
1248
1383
  getNodes() {
1384
+ if (!this.isValid()) {
1385
+ return [];
1386
+ }
1249
1387
  const cachedNodes = this._cachedNodes;
1250
1388
  if (cachedNodes !== null) {
1251
1389
  return cachedNodes;
1252
1390
  }
1253
- const anchorNode = this.anchor.getNode();
1254
- const focusNode = this.focus.getNode();
1255
- const anchorCell = $findMatchingParent(anchorNode, $isTableCellNode);
1256
- // todo replace with triplet
1257
- const focusCell = $findMatchingParent(focusNode, $isTableCellNode);
1258
- if (!$isTableCellNode(anchorCell)) {
1259
- throw Error(`Expected TableSelection anchor to be (or a child of) TableCellNode`);
1260
- }
1261
- if (!$isTableCellNode(focusCell)) {
1262
- throw Error(`Expected TableSelection focus to be (or a child of) TableCellNode`);
1263
- }
1264
- const anchorRow = anchorCell.getParent();
1265
- if (!$isTableRowNode(anchorRow)) {
1266
- throw Error(`Expected anchorCell to have a parent TableRowNode`);
1267
- }
1268
- const tableNode = anchorRow.getParent();
1269
- if (!$isTableNode(tableNode)) {
1270
- throw Error(`Expected tableNode to have a parent TableNode`);
1271
- }
1391
+ const {
1392
+ anchorTable: tableNode,
1393
+ anchorCell,
1394
+ focusCell
1395
+ } = $getCellNodes(this);
1272
1396
  const focusCellGrid = focusCell.getParents()[1];
1273
1397
  if (focusCellGrid !== tableNode) {
1274
1398
  if (!tableNode.isParentOf(focusCell)) {
@@ -1294,63 +1418,12 @@ class TableSelection {
1294
1418
  // ability to store a state. Killing TableSelection and moving the logic to the plugin would make
1295
1419
  // this possible.
1296
1420
  const [map, cellAMap, cellBMap] = $computeTableMap(tableNode, anchorCell, focusCell);
1297
- let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn);
1298
- let minRow = Math.min(cellAMap.startRow, cellBMap.startRow);
1299
- let maxColumn = Math.max(cellAMap.startColumn + cellAMap.cell.__colSpan - 1, cellBMap.startColumn + cellBMap.cell.__colSpan - 1);
1300
- let maxRow = Math.max(cellAMap.startRow + cellAMap.cell.__rowSpan - 1, cellBMap.startRow + cellBMap.cell.__rowSpan - 1);
1301
- let exploredMinColumn = minColumn;
1302
- let exploredMinRow = minRow;
1303
- let exploredMaxColumn = minColumn;
1304
- let exploredMaxRow = minRow;
1305
- function expandBoundary(mapValue) {
1306
- const {
1307
- cell,
1308
- startColumn: cellStartColumn,
1309
- startRow: cellStartRow
1310
- } = mapValue;
1311
- minColumn = Math.min(minColumn, cellStartColumn);
1312
- minRow = Math.min(minRow, cellStartRow);
1313
- maxColumn = Math.max(maxColumn, cellStartColumn + cell.__colSpan - 1);
1314
- maxRow = Math.max(maxRow, cellStartRow + cell.__rowSpan - 1);
1315
- }
1316
- while (minColumn < exploredMinColumn || minRow < exploredMinRow || maxColumn > exploredMaxColumn || maxRow > exploredMaxRow) {
1317
- if (minColumn < exploredMinColumn) {
1318
- // Expand on the left
1319
- const rowDiff = exploredMaxRow - exploredMinRow;
1320
- const previousColumn = exploredMinColumn - 1;
1321
- for (let i = 0; i <= rowDiff; i++) {
1322
- expandBoundary(map[exploredMinRow + i][previousColumn]);
1323
- }
1324
- exploredMinColumn = previousColumn;
1325
- }
1326
- if (minRow < exploredMinRow) {
1327
- // Expand on top
1328
- const columnDiff = exploredMaxColumn - exploredMinColumn;
1329
- const previousRow = exploredMinRow - 1;
1330
- for (let i = 0; i <= columnDiff; i++) {
1331
- expandBoundary(map[previousRow][exploredMinColumn + i]);
1332
- }
1333
- exploredMinRow = previousRow;
1334
- }
1335
- if (maxColumn > exploredMaxColumn) {
1336
- // Expand on the right
1337
- const rowDiff = exploredMaxRow - exploredMinRow;
1338
- const nextColumn = exploredMaxColumn + 1;
1339
- for (let i = 0; i <= rowDiff; i++) {
1340
- expandBoundary(map[exploredMinRow + i][nextColumn]);
1341
- }
1342
- exploredMaxColumn = nextColumn;
1343
- }
1344
- if (maxRow > exploredMaxRow) {
1345
- // Expand on the bottom
1346
- const columnDiff = exploredMaxColumn - exploredMinColumn;
1347
- const nextRow = exploredMaxRow + 1;
1348
- for (let i = 0; i <= columnDiff; i++) {
1349
- expandBoundary(map[nextRow][exploredMinColumn + i]);
1350
- }
1351
- exploredMaxRow = nextRow;
1352
- }
1353
- }
1421
+ const {
1422
+ minColumn,
1423
+ maxColumn,
1424
+ minRow,
1425
+ maxRow
1426
+ } = $computeTableCellRectBoundary(map, cellAMap, cellBMap);
1354
1427
 
1355
1428
  // We use a Map here because merged cells in the grid would otherwise
1356
1429
  // show up multiple times in the nodes array
@@ -1367,12 +1440,13 @@ class TableSelection {
1367
1440
  }
1368
1441
  if (currentRow !== lastRow) {
1369
1442
  nodeMap.set(currentRow.getKey(), currentRow);
1443
+ lastRow = currentRow;
1370
1444
  }
1371
- nodeMap.set(cell.getKey(), cell);
1372
- for (const child of $getChildrenRecursively(cell)) {
1373
- nodeMap.set(child.getKey(), child);
1445
+ if (!nodeMap.has(cell.getKey())) {
1446
+ $visitRecursively(cell, childNode => {
1447
+ nodeMap.set(childNode.getKey(), childNode);
1448
+ });
1374
1449
  }
1375
- lastRow = currentRow;
1376
1450
  }
1377
1451
  }
1378
1452
  const nodes = Array.from(nodeMap.values());
@@ -1397,26 +1471,51 @@ function $isTableSelection(x) {
1397
1471
  return x instanceof TableSelection;
1398
1472
  }
1399
1473
  function $createTableSelection() {
1474
+ // TODO this is a suboptimal design, it doesn't make sense to have
1475
+ // a table selection that isn't associated with a table. This
1476
+ // constructor should have required argumnets and in true we
1477
+ // should check that they point to a table and are element points to
1478
+ // cell nodes of that table.
1400
1479
  const anchor = $createPoint('root', 0, 'element');
1401
1480
  const focus = $createPoint('root', 0, 'element');
1402
1481
  return new TableSelection('root', anchor, focus);
1403
1482
  }
1404
- function $getChildrenRecursively(node) {
1405
- const nodes = [];
1406
- const stack = [node];
1407
- while (stack.length > 0) {
1408
- const currentNode = stack.pop();
1409
- if (!(currentNode !== undefined)) {
1410
- throw Error(`Stack.length > 0; can't be undefined`);
1483
+ function $createTableSelectionFrom(tableNode, anchorCell, focusCell) {
1484
+ const tableNodeKey = tableNode.getKey();
1485
+ const anchorCellKey = anchorCell.getKey();
1486
+ const focusCellKey = focusCell.getKey();
1487
+ {
1488
+ if (!tableNode.isAttached()) {
1489
+ throw Error(`$createTableSelectionFrom: tableNode ${tableNodeKey} is not attached`);
1411
1490
  }
1412
- if ($isElementNode(currentNode)) {
1413
- stack.unshift(...currentNode.getChildren());
1491
+ if (!tableNode.is($findTableNode(anchorCell))) {
1492
+ throw Error(`$createTableSelectionFrom: anchorCell ${anchorCellKey} is not in table ${tableNodeKey}`);
1414
1493
  }
1415
- if (currentNode !== node) {
1416
- nodes.push(currentNode);
1494
+ if (!tableNode.is($findTableNode(focusCell))) {
1495
+ throw Error(`$createTableSelectionFrom: focusCell ${focusCellKey} is not in table ${tableNodeKey}`);
1496
+ } // TODO: Check for rectangular grid
1497
+ }
1498
+ const prevSelection = $getSelection();
1499
+ const nextSelection = $isTableSelection(prevSelection) ? prevSelection.clone() : $createTableSelection();
1500
+ nextSelection.set(tableNode.getKey(), anchorCell.getKey(), focusCell.getKey());
1501
+ return nextSelection;
1502
+ }
1503
+
1504
+ /**
1505
+ * Depth first visitor
1506
+ * @param node The starting node
1507
+ * @param $visit The function to call for each node. If the function returns false, then children of this node will not be explored
1508
+ */
1509
+ function $visitRecursively(node, $visit) {
1510
+ const stack = [[node]];
1511
+ for (let currentArray = stack.at(-1); currentArray !== undefined && stack.length > 0; currentArray = stack.at(-1)) {
1512
+ const currentNode = currentArray.pop();
1513
+ if (currentNode === undefined) {
1514
+ stack.pop();
1515
+ } else if ($visit(currentNode) !== false && $isElementNode(currentNode)) {
1516
+ stack.push(currentNode.getChildren());
1417
1517
  }
1418
1518
  }
1419
- return nodes;
1420
1519
  }
1421
1520
 
1422
1521
  /**
@@ -1468,6 +1567,7 @@ class TableObserver {
1468
1567
  this.listenerOptions = {
1469
1568
  signal: this.abortController.signal
1470
1569
  };
1570
+ this.nextFocus = null;
1471
1571
  this.trackTable();
1472
1572
  }
1473
1573
  getTable() {
@@ -1521,7 +1621,7 @@ class TableObserver {
1521
1621
  editor: this.editor
1522
1622
  });
1523
1623
  }
1524
- clearHighlight() {
1624
+ $clearHighlight() {
1525
1625
  const editor = this.editor;
1526
1626
  this.isHighlightingCells = false;
1527
1627
  this.anchorX = -1;
@@ -1534,55 +1634,47 @@ class TableObserver {
1534
1634
  this.anchorCell = null;
1535
1635
  this.focusCell = null;
1536
1636
  this.hasHijackedSelectionStyles = false;
1537
- this.enableHighlightStyle();
1538
- editor.update(() => {
1539
- const {
1540
- tableNode,
1541
- tableElement
1542
- } = this.$lookup();
1543
- const grid = getTable(tableNode, tableElement);
1544
- $updateDOMForSelection(editor, grid, null);
1637
+ this.$enableHighlightStyle();
1638
+ const {
1639
+ tableNode,
1640
+ tableElement
1641
+ } = this.$lookup();
1642
+ const grid = getTable(tableNode, tableElement);
1643
+ $updateDOMForSelection(editor, grid, null);
1644
+ if ($getSelection() !== null) {
1545
1645
  $setSelection(null);
1546
1646
  editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1547
- });
1647
+ }
1548
1648
  }
1549
- enableHighlightStyle() {
1649
+ $enableHighlightStyle() {
1550
1650
  const editor = this.editor;
1551
- editor.getEditorState().read(() => {
1552
- const {
1553
- tableElement
1554
- } = this.$lookup();
1555
- removeClassNamesFromElement(tableElement, editor._config.theme.tableSelection);
1556
- tableElement.classList.remove('disable-selection');
1557
- this.hasHijackedSelectionStyles = false;
1558
- }, {
1559
- editor
1560
- });
1651
+ const {
1652
+ tableElement
1653
+ } = this.$lookup();
1654
+ removeClassNamesFromElement(tableElement, editor._config.theme.tableSelection);
1655
+ tableElement.classList.remove('disable-selection');
1656
+ this.hasHijackedSelectionStyles = false;
1561
1657
  }
1562
- disableHighlightStyle() {
1563
- const editor = this.editor;
1564
- editor.getEditorState().read(() => {
1565
- const {
1566
- tableElement
1567
- } = this.$lookup();
1568
- addClassNamesToElement(tableElement, editor._config.theme.tableSelection);
1569
- this.hasHijackedSelectionStyles = true;
1570
- }, {
1571
- editor
1572
- });
1658
+ $disableHighlightStyle() {
1659
+ const {
1660
+ tableElement
1661
+ } = this.$lookup();
1662
+ addClassNamesToElement(tableElement, this.editor._config.theme.tableSelection);
1663
+ this.hasHijackedSelectionStyles = true;
1573
1664
  }
1574
- updateTableTableSelection(selection) {
1575
- if (selection !== null && selection.tableKey === this.tableNodeKey) {
1665
+ $updateTableTableSelection(selection) {
1666
+ if (selection !== null) {
1667
+ if (!(selection.tableKey === this.tableNodeKey)) {
1668
+ throw Error(`TableObserver.$updateTableTableSelection: selection.tableKey !== this.tableNodeKey ('${selection.tableKey}' !== '${this.tableNodeKey}')`);
1669
+ }
1576
1670
  const editor = this.editor;
1577
1671
  this.tableSelection = selection;
1578
1672
  this.isHighlightingCells = true;
1579
- this.disableHighlightStyle();
1673
+ this.$disableHighlightStyle();
1674
+ this.updateDOMSelection();
1580
1675
  $updateDOMForSelection(editor, this.table, this.tableSelection);
1581
- } else if (selection == null) {
1582
- this.clearHighlight();
1583
1676
  } else {
1584
- this.tableNodeKey = selection.tableKey;
1585
- this.updateTableTableSelection(selection);
1677
+ this.$clearHighlight();
1586
1678
  }
1587
1679
  }
1588
1680
 
@@ -1608,120 +1700,165 @@ class TableObserver {
1608
1700
  }
1609
1701
  return false;
1610
1702
  }
1611
- setFocusCellForSelection(cell, ignoreStart = false) {
1612
- const editor = this.editor;
1613
- editor.update(() => {
1614
- const {
1615
- tableNode
1616
- } = this.$lookup();
1617
- const cellX = cell.x;
1618
- const cellY = cell.y;
1619
- this.focusCell = cell;
1620
- if (this.anchorCell !== null) {
1621
- const domSelection = getDOMSelection(editor._window);
1622
- // Collapse the selection
1623
- if (domSelection) {
1624
- domSelection.setBaseAndExtent(this.anchorCell.elem, 0, this.focusCell.elem, 0);
1625
- }
1626
- }
1627
- if (!this.isHighlightingCells && (this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart)) {
1628
- this.isHighlightingCells = true;
1629
- this.disableHighlightStyle();
1630
- } else if (cellX === this.focusX && cellY === this.focusY) {
1631
- return;
1703
+
1704
+ /**
1705
+ * @internal
1706
+ * When handling mousemove events we track what the focus cell should be, but
1707
+ * the DOM selection may end up somewhere else entirely. We don't have an elegant
1708
+ * way to handle this after the DOM selection has been resolved in a
1709
+ * SELECTION_CHANGE_COMMAND callback.
1710
+ */
1711
+ setNextFocus(nextFocus) {
1712
+ this.nextFocus = nextFocus;
1713
+ }
1714
+
1715
+ /** @internal */
1716
+ getAndClearNextFocus() {
1717
+ const {
1718
+ nextFocus
1719
+ } = this;
1720
+ if (nextFocus !== null) {
1721
+ this.nextFocus = null;
1722
+ }
1723
+ return nextFocus;
1724
+ }
1725
+
1726
+ /** @internal */
1727
+ updateDOMSelection() {
1728
+ if (this.anchorCell !== null && this.focusCell !== null) {
1729
+ const domSelection = getDOMSelection(this.editor._window);
1730
+ // We are not using a native selection for tables, and if we
1731
+ // set one then the reconciler will undo it.
1732
+ // TODO - it would make sense to have one so that native
1733
+ // copy/paste worked. Right now we have to emulate with
1734
+ // keyboard events but it won't fire if trigged from the menu
1735
+ if (domSelection && domSelection.rangeCount > 0) {
1736
+ domSelection.removeAllRanges();
1632
1737
  }
1633
- this.focusX = cellX;
1634
- this.focusY = cellY;
1635
- if (this.isHighlightingCells) {
1636
- const focusTableCellNode = $getNearestNodeFromDOMNode(cell.elem);
1637
- if (this.tableSelection != null && this.anchorCellNodeKey != null && $isTableCellNode(focusTableCellNode) && tableNode.is($findTableNode(focusTableCellNode))) {
1638
- const focusNodeKey = focusTableCellNode.getKey();
1639
- this.tableSelection = this.tableSelection.clone() || $createTableSelection();
1640
- this.focusCellNodeKey = focusNodeKey;
1641
- this.tableSelection.set(this.tableNodeKey, this.anchorCellNodeKey, this.focusCellNodeKey);
1642
- $setSelection(this.tableSelection);
1643
- editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1644
- $updateDOMForSelection(editor, this.table, this.tableSelection);
1645
- }
1738
+ }
1739
+ }
1740
+ $setFocusCellForSelection(cell, ignoreStart = false) {
1741
+ const editor = this.editor;
1742
+ const {
1743
+ tableNode
1744
+ } = this.$lookup();
1745
+ const cellX = cell.x;
1746
+ const cellY = cell.y;
1747
+ this.focusCell = cell;
1748
+ if (!this.isHighlightingCells && (this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart)) {
1749
+ this.isHighlightingCells = true;
1750
+ this.$disableHighlightStyle();
1751
+ } else if (cellX === this.focusX && cellY === this.focusY) {
1752
+ return false;
1753
+ }
1754
+ this.focusX = cellX;
1755
+ this.focusY = cellY;
1756
+ if (this.isHighlightingCells) {
1757
+ const focusTableCellNode = $getNearestNodeFromDOMNode(cell.elem);
1758
+ if (this.tableSelection != null && this.anchorCellNodeKey != null && $isTableCellNode(focusTableCellNode) && tableNode.is($findTableNode(focusTableCellNode))) {
1759
+ this.focusCellNodeKey = focusTableCellNode.getKey();
1760
+ this.tableSelection = $createTableSelectionFrom(tableNode, this.$getAnchorTableCellOrThrow(), focusTableCellNode);
1761
+ $setSelection(this.tableSelection);
1762
+ editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1763
+ $updateDOMForSelection(editor, this.table, this.tableSelection);
1764
+ return true;
1646
1765
  }
1647
- });
1766
+ }
1767
+ return false;
1768
+ }
1769
+ $getAnchorTableCell() {
1770
+ return this.anchorCellNodeKey ? $getNodeByKey(this.anchorCellNodeKey) : null;
1771
+ }
1772
+ $getAnchorTableCellOrThrow() {
1773
+ const anchorTableCell = this.$getAnchorTableCell();
1774
+ if (!(anchorTableCell !== null)) {
1775
+ throw Error(`TableObserver anchorTableCell is null`);
1776
+ }
1777
+ return anchorTableCell;
1778
+ }
1779
+ $getFocusTableCell() {
1780
+ return this.focusCellNodeKey ? $getNodeByKey(this.focusCellNodeKey) : null;
1781
+ }
1782
+ $getFocusTableCellOrThrow() {
1783
+ const focusTableCell = this.$getFocusTableCell();
1784
+ if (!(focusTableCell !== null)) {
1785
+ throw Error(`TableObserver focusTableCell is null`);
1786
+ }
1787
+ return focusTableCell;
1648
1788
  }
1649
- setAnchorCellForSelection(cell) {
1789
+ $setAnchorCellForSelection(cell) {
1650
1790
  this.isHighlightingCells = false;
1651
1791
  this.anchorCell = cell;
1652
1792
  this.anchorX = cell.x;
1653
1793
  this.anchorY = cell.y;
1654
- this.editor.update(() => {
1655
- const anchorTableCellNode = $getNearestNodeFromDOMNode(cell.elem);
1656
- if ($isTableCellNode(anchorTableCellNode)) {
1657
- const anchorNodeKey = anchorTableCellNode.getKey();
1658
- this.tableSelection = this.tableSelection != null ? this.tableSelection.clone() : $createTableSelection();
1659
- this.anchorCellNodeKey = anchorNodeKey;
1660
- }
1661
- });
1794
+ const anchorTableCellNode = $getNearestNodeFromDOMNode(cell.elem);
1795
+ if ($isTableCellNode(anchorTableCellNode)) {
1796
+ const anchorNodeKey = anchorTableCellNode.getKey();
1797
+ this.tableSelection = this.tableSelection != null ? this.tableSelection.clone() : $createTableSelection();
1798
+ this.anchorCellNodeKey = anchorNodeKey;
1799
+ }
1662
1800
  }
1663
- formatCells(type) {
1664
- this.editor.update(() => {
1665
- const selection = $getSelection();
1666
- if (!$isTableSelection(selection)) {
1667
- {
1668
- throw Error(`Expected grid selection`);
1669
- }
1670
- }
1671
- const formatSelection = $createRangeSelection();
1672
- const anchor = formatSelection.anchor;
1673
- const focus = formatSelection.focus;
1674
- const cellNodes = selection.getNodes().filter($isTableCellNode);
1675
- const paragraph = cellNodes[0].getFirstChild();
1676
- const alignFormatWith = $isParagraphNode(paragraph) ? paragraph.getFormatFlags(type, null) : null;
1677
- cellNodes.forEach(cellNode => {
1678
- anchor.set(cellNode.getKey(), 0, 'element');
1679
- focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element');
1680
- formatSelection.formatText(type, alignFormatWith);
1681
- });
1682
- $setSelection(selection);
1683
- this.editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1801
+ $formatCells(type) {
1802
+ const selection = $getSelection();
1803
+ if (!$isTableSelection(selection)) {
1804
+ throw Error(`Expected Table selection`);
1805
+ }
1806
+ const formatSelection = $createRangeSelection();
1807
+ const anchor = formatSelection.anchor;
1808
+ const focus = formatSelection.focus;
1809
+ const cellNodes = selection.getNodes().filter($isTableCellNode);
1810
+ if (!(cellNodes.length > 0)) {
1811
+ throw Error(`No table cells present`);
1812
+ }
1813
+ const paragraph = cellNodes[0].getFirstChild();
1814
+ const alignFormatWith = $isParagraphNode(paragraph) ? paragraph.getFormatFlags(type, null) : null;
1815
+ cellNodes.forEach(cellNode => {
1816
+ anchor.set(cellNode.getKey(), 0, 'element');
1817
+ focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element');
1818
+ formatSelection.formatText(type, alignFormatWith);
1684
1819
  });
1820
+ $setSelection(selection);
1821
+ this.editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1685
1822
  }
1686
- clearText() {
1687
- const editor = this.editor;
1688
- editor.update(() => {
1689
- const tableNode = $getNodeByKey(this.tableNodeKey);
1690
- if (!$isTableNode(tableNode)) {
1691
- throw new Error('Expected TableNode.');
1692
- }
1693
- const selection = $getSelection();
1694
- if (!$isTableSelection(selection)) {
1695
- {
1696
- throw Error(`Expected grid selection`);
1697
- }
1823
+ $clearText() {
1824
+ const {
1825
+ editor
1826
+ } = this;
1827
+ const tableNode = $getNodeByKey(this.tableNodeKey);
1828
+ if (!$isTableNode(tableNode)) {
1829
+ throw new Error('Expected TableNode.');
1830
+ }
1831
+ const selection = $getSelection();
1832
+ if (!$isTableSelection(selection)) {
1833
+ {
1834
+ throw Error(`Expected grid selection`);
1698
1835
  }
1699
- const selectedNodes = selection.getNodes().filter($isTableCellNode);
1700
- if (selectedNodes.length === this.table.columns * this.table.rows) {
1701
- tableNode.selectPrevious();
1702
- // Delete entire table
1703
- tableNode.remove();
1704
- const rootNode = $getRoot();
1705
- rootNode.selectStart();
1706
- return;
1836
+ }
1837
+ const selectedNodes = selection.getNodes().filter($isTableCellNode);
1838
+ if (selectedNodes.length === this.table.columns * this.table.rows) {
1839
+ tableNode.selectPrevious();
1840
+ // Delete entire table
1841
+ tableNode.remove();
1842
+ const rootNode = $getRoot();
1843
+ rootNode.selectStart();
1844
+ return;
1845
+ }
1846
+ selectedNodes.forEach(cellNode => {
1847
+ if ($isElementNode(cellNode)) {
1848
+ const paragraphNode = $createParagraphNode();
1849
+ const textNode = $createTextNode();
1850
+ paragraphNode.append(textNode);
1851
+ cellNode.append(paragraphNode);
1852
+ cellNode.getChildren().forEach(child => {
1853
+ if (child !== paragraphNode) {
1854
+ child.remove();
1855
+ }
1856
+ });
1707
1857
  }
1708
- selectedNodes.forEach(cellNode => {
1709
- if ($isElementNode(cellNode)) {
1710
- const paragraphNode = $createParagraphNode();
1711
- const textNode = $createTextNode();
1712
- paragraphNode.append(textNode);
1713
- cellNode.append(paragraphNode);
1714
- cellNode.getChildren().forEach(child => {
1715
- if (child !== paragraphNode) {
1716
- child.remove();
1717
- }
1718
- });
1719
- }
1720
- });
1721
- $updateDOMForSelection(editor, this.table, null);
1722
- $setSelection(null);
1723
- editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1724
1858
  });
1859
+ $updateDOMForSelection(editor, this.table, null);
1860
+ $setSelection(null);
1861
+ editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1725
1862
  }
1726
1863
  }
1727
1864
 
@@ -1734,7 +1871,6 @@ class TableObserver {
1734
1871
  */
1735
1872
 
1736
1873
  const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
1737
- const getDOMSelection = targetWindow => CAN_USE_DOM ? (targetWindow || window).getSelection() : null;
1738
1874
  const isMouseDownOnEvent = event => {
1739
1875
  return (event.buttons & 1) === 1;
1740
1876
  };
@@ -1748,64 +1884,103 @@ function getTableElement(tableNode, dom) {
1748
1884
  }
1749
1885
  return element;
1750
1886
  }
1887
+ function getEditorWindow(editor) {
1888
+ return editor._window;
1889
+ }
1890
+ function $findParentTableCellNodeInTable(tableNode, node) {
1891
+ for (let currentNode = node, lastTableCellNode = null; currentNode !== null; currentNode = currentNode.getParent()) {
1892
+ if (tableNode.is(currentNode)) {
1893
+ return lastTableCellNode;
1894
+ } else if ($isTableCellNode(currentNode)) {
1895
+ lastTableCellNode = currentNode;
1896
+ }
1897
+ }
1898
+ return null;
1899
+ }
1900
+ const ARROW_KEY_COMMANDS_WITH_DIRECTION = [[KEY_ARROW_DOWN_COMMAND, 'down'], [KEY_ARROW_UP_COMMAND, 'up'], [KEY_ARROW_LEFT_COMMAND, 'backward'], [KEY_ARROW_RIGHT_COMMAND, 'forward']];
1901
+ const DELETE_TEXT_COMMANDS = [DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND];
1902
+ const DELETE_KEY_COMMANDS = [KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND];
1751
1903
  function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1752
1904
  const rootElement = editor.getRootElement();
1753
- if (rootElement === null) {
1754
- throw new Error('No root element.');
1905
+ const editorWindow = getEditorWindow(editor);
1906
+ if (!(rootElement !== null && editorWindow !== null)) {
1907
+ throw Error(`applyTableHandlers: editor has no root element set`);
1755
1908
  }
1756
1909
  const tableObserver = new TableObserver(editor, tableNode.getKey());
1757
- const editorWindow = editor._window || window;
1758
1910
  const tableElement = getTableElement(tableNode, element);
1759
1911
  attachTableObserverToTableElement(tableElement, tableObserver);
1760
1912
  tableObserver.listenersToRemove.add(() => detatchTableObserverFromTableElement(tableElement, tableObserver));
1761
1913
  const createMouseHandlers = () => {
1914
+ if (tableObserver.isSelecting) {
1915
+ return;
1916
+ }
1762
1917
  const onMouseUp = () => {
1763
1918
  tableObserver.isSelecting = false;
1764
1919
  editorWindow.removeEventListener('mouseup', onMouseUp);
1765
1920
  editorWindow.removeEventListener('mousemove', onMouseMove);
1766
1921
  };
1767
1922
  const onMouseMove = moveEvent => {
1768
- // delaying mousemove handler to allow selectionchange handler from LexicalEvents.ts to be executed first
1769
- setTimeout(() => {
1770
- if (!isMouseDownOnEvent(moveEvent) && tableObserver.isSelecting) {
1771
- tableObserver.isSelecting = false;
1772
- editorWindow.removeEventListener('mouseup', onMouseUp);
1773
- editorWindow.removeEventListener('mousemove', onMouseMove);
1774
- return;
1775
- }
1776
- const focusCell = getDOMCellFromTarget(moveEvent.target);
1777
- if (focusCell !== null && (tableObserver.anchorX !== focusCell.x || tableObserver.anchorY !== focusCell.y)) {
1778
- moveEvent.preventDefault();
1779
- tableObserver.setFocusCellForSelection(focusCell);
1780
- }
1781
- }, 0);
1782
- };
1783
- return {
1784
- onMouseMove,
1785
- onMouseUp
1786
- };
1787
- };
1788
- const onMouseDown = event => {
1789
- setTimeout(() => {
1790
- if (event.button !== 0) {
1923
+ if (!isMouseDownOnEvent(moveEvent) && tableObserver.isSelecting) {
1924
+ tableObserver.isSelecting = false;
1925
+ editorWindow.removeEventListener('mouseup', onMouseUp);
1926
+ editorWindow.removeEventListener('mousemove', onMouseMove);
1791
1927
  return;
1792
1928
  }
1793
- if (!editorWindow) {
1794
- return;
1929
+ const override = !tableElement.contains(moveEvent.target);
1930
+ let focusCell = null;
1931
+ if (!override) {
1932
+ focusCell = getDOMCellFromTarget(moveEvent.target);
1933
+ } else {
1934
+ for (const el of document.elementsFromPoint(moveEvent.clientX, moveEvent.clientY)) {
1935
+ focusCell = tableElement.contains(el) ? getDOMCellFromTarget(el) : null;
1936
+ if (focusCell) {
1937
+ break;
1938
+ }
1939
+ }
1795
1940
  }
1796
- const anchorCell = getDOMCellFromTarget(event.target);
1797
- if (anchorCell !== null) {
1798
- stopEvent(event);
1799
- tableObserver.setAnchorCellForSelection(anchorCell);
1941
+ if (focusCell && (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem)) {
1942
+ tableObserver.setNextFocus({
1943
+ focusCell,
1944
+ override
1945
+ });
1946
+ editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
1800
1947
  }
1801
- const {
1802
- onMouseUp,
1803
- onMouseMove
1804
- } = createMouseHandlers();
1805
- tableObserver.isSelecting = true;
1806
- editorWindow.addEventListener('mouseup', onMouseUp, tableObserver.listenerOptions);
1807
- editorWindow.addEventListener('mousemove', onMouseMove, tableObserver.listenerOptions);
1808
- }, 0);
1948
+ };
1949
+ tableObserver.isSelecting = true;
1950
+ editorWindow.addEventListener('mouseup', onMouseUp, tableObserver.listenerOptions);
1951
+ editorWindow.addEventListener('mousemove', onMouseMove, tableObserver.listenerOptions);
1952
+ };
1953
+ const onMouseDown = event => {
1954
+ if (event.button !== 0) {
1955
+ return;
1956
+ }
1957
+ if (!editorWindow) {
1958
+ return;
1959
+ }
1960
+ const targetCell = getDOMCellFromTarget(event.target);
1961
+ if (targetCell !== null) {
1962
+ editor.update(() => {
1963
+ const prevSelection = $getPreviousSelection();
1964
+ // We can't trust Firefox to do the right thing with the selection and
1965
+ // we don't have a proper state machine to do this "correctly" but
1966
+ // if we go ahead and make the table selection now it will work
1967
+ if (IS_FIREFOX && event.shiftKey && $isSelectionInTable(prevSelection, tableNode) && ($isRangeSelection(prevSelection) || $isTableSelection(prevSelection))) {
1968
+ const prevAnchorNode = prevSelection.anchor.getNode();
1969
+ const prevAnchorCell = $findParentTableCellNodeInTable(tableNode, prevSelection.anchor.getNode());
1970
+ if (prevAnchorCell) {
1971
+ tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, prevAnchorCell));
1972
+ tableObserver.$setFocusCellForSelection(targetCell);
1973
+ stopEvent(event);
1974
+ } else {
1975
+ const newSelection = tableNode.isBefore(prevAnchorNode) ? tableNode.selectStart() : tableNode.selectEnd();
1976
+ newSelection.anchor.set(prevSelection.anchor.key, prevSelection.anchor.offset, prevSelection.anchor.type);
1977
+ }
1978
+ } else {
1979
+ tableObserver.$setAnchorCellForSelection(targetCell);
1980
+ }
1981
+ });
1982
+ }
1983
+ createMouseHandlers();
1809
1984
  };
1810
1985
  tableElement.addEventListener('mousedown', onMouseDown, tableObserver.listenerOptions);
1811
1986
 
@@ -1818,20 +1993,19 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1818
1993
  const selection = $getSelection();
1819
1994
  const target = event.target;
1820
1995
  if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey && rootElement.contains(target)) {
1821
- tableObserver.clearHighlight();
1996
+ tableObserver.$clearHighlight();
1822
1997
  }
1823
1998
  });
1824
1999
  };
1825
2000
  editorWindow.addEventListener('mousedown', mouseDownCallback, tableObserver.listenerOptions);
1826
- tableObserver.listenersToRemove.add(editor.registerCommand(KEY_ARROW_DOWN_COMMAND, event => $handleArrowKey(editor, event, 'down', tableNode, tableObserver), COMMAND_PRIORITY_HIGH));
1827
- tableObserver.listenersToRemove.add(editor.registerCommand(KEY_ARROW_UP_COMMAND, event => $handleArrowKey(editor, event, 'up', tableNode, tableObserver), COMMAND_PRIORITY_HIGH));
1828
- tableObserver.listenersToRemove.add(editor.registerCommand(KEY_ARROW_LEFT_COMMAND, event => $handleArrowKey(editor, event, 'backward', tableNode, tableObserver), COMMAND_PRIORITY_HIGH));
1829
- tableObserver.listenersToRemove.add(editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, event => $handleArrowKey(editor, event, 'forward', tableNode, tableObserver), COMMAND_PRIORITY_HIGH));
2001
+ for (const [command, direction] of ARROW_KEY_COMMANDS_WITH_DIRECTION) {
2002
+ tableObserver.listenersToRemove.add(editor.registerCommand(command, event => $handleArrowKey(editor, event, direction, tableNode, tableObserver), COMMAND_PRIORITY_HIGH));
2003
+ }
1830
2004
  tableObserver.listenersToRemove.add(editor.registerCommand(KEY_ESCAPE_COMMAND, event => {
1831
2005
  const selection = $getSelection();
1832
2006
  if ($isTableSelection(selection)) {
1833
- const focusCellNode = $findMatchingParent(selection.focus.getNode(), $isTableCellNode);
1834
- if ($isTableCellNode(focusCellNode)) {
2007
+ const focusCellNode = $findParentTableCellNodeInTable(tableNode, selection.focus.getNode());
2008
+ if (focusCellNode !== null) {
1835
2009
  stopEvent(event);
1836
2010
  focusCellNode.selectEnd();
1837
2011
  return true;
@@ -1845,10 +2019,10 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1845
2019
  return false;
1846
2020
  }
1847
2021
  if ($isTableSelection(selection)) {
1848
- tableObserver.clearText();
2022
+ tableObserver.$clearText();
1849
2023
  return true;
1850
2024
  } else if ($isRangeSelection(selection)) {
1851
- const tableCellNode = $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
2025
+ const tableCellNode = $findParentTableCellNodeInTable(tableNode, selection.anchor.getNode());
1852
2026
  if (!$isTableCellNode(tableCellNode)) {
1853
2027
  return false;
1854
2028
  }
@@ -1858,7 +2032,7 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1858
2032
  const isFocusInside = tableNode.isParentOf(focusNode);
1859
2033
  const selectionContainsPartialTable = isAnchorInside && !isFocusInside || isFocusInside && !isAnchorInside;
1860
2034
  if (selectionContainsPartialTable) {
1861
- tableObserver.clearText();
2035
+ tableObserver.$clearText();
1862
2036
  return true;
1863
2037
  }
1864
2038
  const nearestElementNode = $findMatchingParent(selection.anchor.getNode(), n => $isElementNode(n));
@@ -1873,9 +2047,9 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1873
2047
  }
1874
2048
  return false;
1875
2049
  };
1876
- [DELETE_WORD_COMMAND, DELETE_LINE_COMMAND, DELETE_CHARACTER_COMMAND].forEach(command => {
2050
+ for (const command of DELETE_TEXT_COMMANDS) {
1877
2051
  tableObserver.listenersToRemove.add(editor.registerCommand(command, deleteTextHandler(command), COMMAND_PRIORITY_CRITICAL));
1878
- });
2052
+ }
1879
2053
  const $deleteCellHandler = event => {
1880
2054
  const selection = $getSelection();
1881
2055
  if (!($isTableSelection(selection) || $isRangeSelection(selection))) {
@@ -1908,13 +2082,14 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1908
2082
  event.preventDefault();
1909
2083
  event.stopPropagation();
1910
2084
  }
1911
- tableObserver.clearText();
2085
+ tableObserver.$clearText();
1912
2086
  return true;
1913
2087
  }
1914
2088
  return false;
1915
2089
  };
1916
- tableObserver.listenersToRemove.add(editor.registerCommand(KEY_BACKSPACE_COMMAND, $deleteCellHandler, COMMAND_PRIORITY_CRITICAL));
1917
- tableObserver.listenersToRemove.add(editor.registerCommand(KEY_DELETE_COMMAND, $deleteCellHandler, COMMAND_PRIORITY_CRITICAL));
2090
+ for (const command of DELETE_KEY_COMMANDS) {
2091
+ tableObserver.listenersToRemove.add(editor.registerCommand(command, $deleteCellHandler, COMMAND_PRIORITY_CRITICAL));
2092
+ }
1918
2093
  tableObserver.listenersToRemove.add(editor.registerCommand(CUT_COMMAND, event => {
1919
2094
  const selection = $getSelection();
1920
2095
  if (selection) {
@@ -1939,7 +2114,7 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1939
2114
  return false;
1940
2115
  }
1941
2116
  if ($isTableSelection(selection)) {
1942
- tableObserver.formatCells(payload);
2117
+ tableObserver.$formatCells(payload);
1943
2118
  return true;
1944
2119
  } else if ($isRangeSelection(selection)) {
1945
2120
  const tableCellNode = $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
@@ -1960,13 +2135,18 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1960
2135
  return false;
1961
2136
  }
1962
2137
  const [tableMap, anchorCell, focusCell] = $computeTableMap(tableNode, anchorNode, focusNode);
1963
- const maxRow = Math.max(anchorCell.startRow, focusCell.startRow);
1964
- const maxColumn = Math.max(anchorCell.startColumn, focusCell.startColumn);
2138
+ const maxRow = Math.max(anchorCell.startRow + anchorCell.cell.__rowSpan - 1, focusCell.startRow + focusCell.cell.__rowSpan - 1);
2139
+ const maxColumn = Math.max(anchorCell.startColumn + anchorCell.cell.__colSpan - 1, focusCell.startColumn + focusCell.cell.__colSpan - 1);
1965
2140
  const minRow = Math.min(anchorCell.startRow, focusCell.startRow);
1966
2141
  const minColumn = Math.min(anchorCell.startColumn, focusCell.startColumn);
2142
+ const visited = new Set();
1967
2143
  for (let i = minRow; i <= maxRow; i++) {
1968
2144
  for (let j = minColumn; j <= maxColumn; j++) {
1969
2145
  const cell = tableMap[i][j].cell;
2146
+ if (visited.has(cell)) {
2147
+ continue;
2148
+ }
2149
+ visited.add(cell);
1970
2150
  cell.setFormat(formatType);
1971
2151
  const cellChildren = cell.getChildren();
1972
2152
  for (let k = 0; k < cellChildren.length; k++) {
@@ -1985,7 +2165,7 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
1985
2165
  return false;
1986
2166
  }
1987
2167
  if ($isTableSelection(selection)) {
1988
- tableObserver.clearHighlight();
2168
+ tableObserver.$clearHighlight();
1989
2169
  return false;
1990
2170
  } else if ($isRangeSelection(selection)) {
1991
2171
  const tableCellNode = $findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
@@ -2021,10 +2201,6 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
2021
2201
  tableObserver.listenersToRemove.add(editor.registerCommand(FOCUS_COMMAND, payload => {
2022
2202
  return tableNode.isSelected();
2023
2203
  }, COMMAND_PRIORITY_HIGH));
2024
- function getObserverCellFromCellNode(tableCellNode) {
2025
- const currentCords = tableNode.getCordsFromCellNode(tableCellNode, tableObserver.table);
2026
- return tableNode.getDOMCellFromCordsOrThrow(currentCords.x, currentCords.y, tableObserver.table);
2027
- }
2028
2204
  tableObserver.listenersToRemove.add(editor.registerCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, selectionPayload => {
2029
2205
  const {
2030
2206
  nodes,
@@ -2099,10 +2275,30 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
2099
2275
  tableObserver.listenersToRemove.add(editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
2100
2276
  const selection = $getSelection();
2101
2277
  const prevSelection = $getPreviousSelection();
2278
+ const nextFocus = tableObserver.getAndClearNextFocus();
2279
+ if (nextFocus !== null) {
2280
+ const {
2281
+ focusCell
2282
+ } = nextFocus;
2283
+ if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey) {
2284
+ if (focusCell.x === tableObserver.focusX && focusCell.y === tableObserver.focusY) {
2285
+ // The selection is already the correct table selection
2286
+ return false;
2287
+ } else {
2288
+ tableObserver.$setFocusCellForSelection(focusCell);
2289
+ return true;
2290
+ }
2291
+ } else if (focusCell !== tableObserver.anchorCell && $isSelectionInTable(selection, tableNode)) {
2292
+ // The selection has crossed cells
2293
+ tableObserver.$setFocusCellForSelection(focusCell);
2294
+ return true;
2295
+ }
2296
+ }
2297
+ const shouldCheckSelection = tableObserver.getAndClearShouldCheckSelection();
2102
2298
  // If they pressed the down arrow with the selection outside of the
2103
2299
  // table, and then the selection ends up in the table but not in the
2104
2300
  // first cell, then move the selection to the first cell.
2105
- if (tableObserver.getAndClearShouldCheckSelection() && $isRangeSelection(prevSelection) && $isRangeSelection(selection) && selection.isCollapsed()) {
2301
+ if (shouldCheckSelection && $isRangeSelection(prevSelection) && $isRangeSelection(selection) && selection.isCollapsed()) {
2106
2302
  const anchor = selection.anchor.getNode();
2107
2303
  const firstRow = tableNode.getFirstChild();
2108
2304
  const anchorCell = $findCellNode(anchor);
@@ -2128,10 +2324,10 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
2128
2324
  const focusCellNode = $findCellNode(focusNode);
2129
2325
  const isAnchorInside = !!(anchorCellNode && tableNode.is($findTableNode(anchorCellNode)));
2130
2326
  const isFocusInside = !!(focusCellNode && tableNode.is($findTableNode(focusCellNode)));
2131
- const isPartialyWithinTable = isAnchorInside !== isFocusInside;
2327
+ const isPartiallyWithinTable = isAnchorInside !== isFocusInside;
2132
2328
  const isWithinTable = isAnchorInside && isFocusInside;
2133
2329
  const isBackward = selection.isBackward();
2134
- if (isPartialyWithinTable) {
2330
+ if (isPartiallyWithinTable) {
2135
2331
  const newSelection = selection.clone();
2136
2332
  if (isFocusInside) {
2137
2333
  const [tableMap] = $computeTableMap(tableNode, focusCellNode, focusCellNode);
@@ -2154,26 +2350,15 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
2154
2350
  $addHighlightStyleToTable(editor, tableObserver);
2155
2351
  } else if (isWithinTable) {
2156
2352
  // Handle case when selection spans across multiple cells but still
2157
- // has range selection, then we convert it into grid selection
2353
+ // has range selection, then we convert it into table selection
2158
2354
  if (!anchorCellNode.is(focusCellNode)) {
2159
- tableObserver.setAnchorCellForSelection(getObserverCellFromCellNode(anchorCellNode));
2160
- tableObserver.setFocusCellForSelection(getObserverCellFromCellNode(focusCellNode), true);
2161
- if (!tableObserver.isSelecting) {
2162
- setTimeout(() => {
2163
- const {
2164
- onMouseUp,
2165
- onMouseMove
2166
- } = createMouseHandlers();
2167
- tableObserver.isSelecting = true;
2168
- editorWindow.addEventListener('mouseup', onMouseUp);
2169
- editorWindow.addEventListener('mousemove', onMouseMove);
2170
- }, 0);
2171
- }
2355
+ tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, anchorCellNode));
2356
+ tableObserver.$setFocusCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, focusCellNode), true);
2172
2357
  }
2173
2358
  }
2174
2359
  } else if (selection && $isTableSelection(selection) && selection.is(prevSelection) && selection.tableKey === tableNode.getKey()) {
2175
2360
  // if selection goes outside of the table we need to change it to Range selection
2176
- const domSelection = getDOMSelection(editor._window);
2361
+ const domSelection = getDOMSelection(editorWindow);
2177
2362
  if (domSelection && domSelection.anchorNode && domSelection.focusNode) {
2178
2363
  const focusNode = $getNearestNodeFromDOMNode(domSelection.focusNode);
2179
2364
  const isFocusOutside = focusNode && !tableNode.is($findTableNode(focusNode));
@@ -2191,9 +2376,9 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
2191
2376
  }
2192
2377
  if (selection && !selection.is(prevSelection) && ($isTableSelection(selection) || $isTableSelection(prevSelection)) && tableObserver.tableSelection && !tableObserver.tableSelection.is(prevSelection)) {
2193
2378
  if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey) {
2194
- tableObserver.updateTableTableSelection(selection);
2379
+ tableObserver.$updateTableTableSelection(selection);
2195
2380
  } else if (!$isTableSelection(selection) && $isTableSelection(prevSelection) && prevSelection.tableKey === tableObserver.tableNodeKey) {
2196
- tableObserver.updateTableTableSelection(null);
2381
+ tableObserver.$updateTableTableSelection(null);
2197
2382
  }
2198
2383
  return false;
2199
2384
  }
@@ -2348,14 +2533,14 @@ function $forEachTableCell(grid, cb) {
2348
2533
  }
2349
2534
  }
2350
2535
  function $addHighlightStyleToTable(editor, tableSelection) {
2351
- tableSelection.disableHighlightStyle();
2536
+ tableSelection.$disableHighlightStyle();
2352
2537
  $forEachTableCell(tableSelection.table, cell => {
2353
2538
  cell.highlighted = true;
2354
2539
  $addHighlightToDOM(editor, cell);
2355
2540
  });
2356
2541
  }
2357
2542
  function $removeHighlightStyleToTable(editor, tableObserver) {
2358
- tableObserver.enableHighlightStyle();
2543
+ tableObserver.$enableHighlightStyle();
2359
2544
  $forEachTableCell(tableObserver.table, cell => {
2360
2545
  const elem = cell.elem;
2361
2546
  cell.highlighted = false;
@@ -2400,33 +2585,105 @@ const selectTableNodeInDirection = (tableObserver, tableNode, x, y, direction) =
2400
2585
  return false;
2401
2586
  }
2402
2587
  };
2403
- const adjustFocusNodeInDirection = (tableObserver, tableNode, x, y, direction) => {
2404
- const isForward = direction === 'forward';
2405
- switch (direction) {
2406
- case 'backward':
2407
- case 'forward':
2408
- if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {
2409
- tableObserver.setFocusCellForSelection(tableNode.getDOMCellFromCordsOrThrow(x + (isForward ? 1 : -1), y, tableObserver.table));
2410
- }
2411
- return true;
2412
- case 'up':
2413
- if (y !== 0) {
2414
- tableObserver.setFocusCellForSelection(tableNode.getDOMCellFromCordsOrThrow(x, y - 1, tableObserver.table));
2415
- return true;
2416
- } else {
2417
- return false;
2418
- }
2419
- case 'down':
2420
- if (y !== tableObserver.table.rows - 1) {
2421
- tableObserver.setFocusCellForSelection(tableNode.getDOMCellFromCordsOrThrow(x, y + 1, tableObserver.table));
2422
- return true;
2423
- } else {
2424
- return false;
2425
- }
2426
- default:
2427
- return false;
2588
+ function getCorner(rect, cellValue) {
2589
+ let colName;
2590
+ let rowName;
2591
+ if (cellValue.startColumn === rect.minColumn) {
2592
+ colName = 'minColumn';
2593
+ } else if (cellValue.startColumn + cellValue.cell.__colSpan - 1 === rect.maxColumn) {
2594
+ colName = 'maxColumn';
2595
+ } else {
2596
+ return null;
2428
2597
  }
2429
- };
2598
+ if (cellValue.startRow === rect.minRow) {
2599
+ rowName = 'minRow';
2600
+ } else if (cellValue.startRow + cellValue.cell.__rowSpan - 1 === rect.maxRow) {
2601
+ rowName = 'maxRow';
2602
+ } else {
2603
+ return null;
2604
+ }
2605
+ return [colName, rowName];
2606
+ }
2607
+ function getCornerOrThrow(rect, cellValue) {
2608
+ const corner = getCorner(rect, cellValue);
2609
+ if (!(corner !== null)) {
2610
+ throw Error(`getCornerOrThrow: cell ${cellValue.cell.getKey()} is not at a corner of rect`);
2611
+ }
2612
+ return corner;
2613
+ }
2614
+ function oppositeCorner([colName, rowName]) {
2615
+ return [colName === 'minColumn' ? 'maxColumn' : 'minColumn', rowName === 'minRow' ? 'maxRow' : 'minRow'];
2616
+ }
2617
+ function cellAtCornerOrThrow(tableMap, rect, [colName, rowName]) {
2618
+ const rowNum = rect[rowName];
2619
+ const rowMap = tableMap[rowNum];
2620
+ if (!(rowMap !== undefined)) {
2621
+ throw Error(`cellAtCornerOrThrow: ${rowName} = ${String(rowNum)} missing in tableMap`);
2622
+ }
2623
+ const colNum = rect[colName];
2624
+ const cell = rowMap[colNum];
2625
+ if (!(cell !== undefined)) {
2626
+ throw Error(`cellAtCornerOrThrow: ${colName} = ${String(colNum)} missing in tableMap`);
2627
+ }
2628
+ return cell;
2629
+ }
2630
+ function $extractRectCorners(tableMap, anchorCellValue, newFocusCellValue) {
2631
+ // We are sure that the focus now either contracts or expands the rect
2632
+ // but both the anchor and focus might be moved to ensure a rectangle
2633
+ // given a potentially ragged merge shape
2634
+ const rect = $computeTableCellRectBoundary(tableMap, anchorCellValue, newFocusCellValue);
2635
+ const anchorCorner = getCorner(rect, anchorCellValue);
2636
+ if (anchorCorner) {
2637
+ return [cellAtCornerOrThrow(tableMap, rect, anchorCorner), cellAtCornerOrThrow(tableMap, rect, oppositeCorner(anchorCorner))];
2638
+ }
2639
+ const newFocusCorner = getCorner(rect, newFocusCellValue);
2640
+ if (newFocusCorner) {
2641
+ return [cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newFocusCorner)), cellAtCornerOrThrow(tableMap, rect, newFocusCorner)];
2642
+ }
2643
+ // TODO this doesn't have to be arbitrary, use the closest corner instead
2644
+ const newAnchorCorner = ['minColumn', 'minRow'];
2645
+ return [cellAtCornerOrThrow(tableMap, rect, newAnchorCorner), cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newAnchorCorner))];
2646
+ }
2647
+ function $adjustFocusInDirection(tableObserver, tableMap, anchorCellValue, focusCellValue, direction) {
2648
+ const rect = $computeTableCellRectBoundary(tableMap, anchorCellValue, focusCellValue);
2649
+ const spans = $computeTableCellRectSpans(tableMap, rect);
2650
+ const {
2651
+ topSpan,
2652
+ leftSpan,
2653
+ bottomSpan,
2654
+ rightSpan
2655
+ } = spans;
2656
+ const anchorCorner = getCornerOrThrow(rect, anchorCellValue);
2657
+ const [focusColumn, focusRow] = oppositeCorner(anchorCorner);
2658
+ let fCol = rect[focusColumn];
2659
+ let fRow = rect[focusRow];
2660
+ if (direction === 'forward') {
2661
+ fCol += focusColumn === 'maxColumn' ? 1 : leftSpan;
2662
+ } else if (direction === 'backward') {
2663
+ fCol -= focusColumn === 'minColumn' ? 1 : rightSpan;
2664
+ } else if (direction === 'down') {
2665
+ fRow += focusRow === 'maxRow' ? 1 : topSpan;
2666
+ } else if (direction === 'up') {
2667
+ fRow -= focusRow === 'minRow' ? 1 : bottomSpan;
2668
+ }
2669
+ const targetRowMap = tableMap[fRow];
2670
+ if (targetRowMap === undefined) {
2671
+ return false;
2672
+ }
2673
+ const newFocusCellValue = targetRowMap[fCol];
2674
+ if (newFocusCellValue === undefined) {
2675
+ return false;
2676
+ }
2677
+ // We can be certain that anchorCellValue and newFocusCellValue are
2678
+ // contained within the desired selection, but we are not certain if
2679
+ // they need to be expanded or not to maintain a rectangular shape
2680
+ const [finalAnchorCell, finalFocusCell] = $extractRectCorners(tableMap, anchorCellValue, newFocusCellValue);
2681
+ const anchorDOM = $getObserverCellFromCellNodeOrThrow(tableObserver, finalAnchorCell.cell);
2682
+ const focusDOM = $getObserverCellFromCellNodeOrThrow(tableObserver, finalFocusCell.cell);
2683
+ tableObserver.$setAnchorCellForSelection(anchorDOM);
2684
+ tableObserver.$setFocusCellForSelection(focusDOM, true);
2685
+ return true;
2686
+ }
2430
2687
  function $isSelectionInTable(selection, tableNode) {
2431
2688
  if ($isRangeSelection(selection) || $isTableSelection(selection)) {
2432
2689
  const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
@@ -2442,20 +2699,14 @@ function selectTableCellNode(tableCell, fromStart) {
2442
2699
  tableCell.selectEnd();
2443
2700
  }
2444
2701
  }
2445
- const BROWSER_BLUE_RGB = '172,206,247';
2446
2702
  function $addHighlightToDOM(editor, cell) {
2447
2703
  const element = cell.elem;
2704
+ const editorThemeClasses = editor._config.theme;
2448
2705
  const node = $getNearestNodeFromDOMNode(element);
2449
2706
  if (!$isTableCellNode(node)) {
2450
2707
  throw Error(`Expected to find LexicalNode from Table Cell DOMNode`);
2451
2708
  }
2452
- const backgroundColor = node.getBackgroundColor();
2453
- if (backgroundColor === null) {
2454
- element.style.setProperty('background-color', `rgb(${BROWSER_BLUE_RGB})`);
2455
- } else {
2456
- element.style.setProperty('background-image', `linear-gradient(to right, rgba(${BROWSER_BLUE_RGB},0.85), rgba(${BROWSER_BLUE_RGB},0.85))`);
2457
- }
2458
- element.style.setProperty('caret-color', 'transparent');
2709
+ addClassNamesToElement(element, editorThemeClasses.tableCellSelected);
2459
2710
  }
2460
2711
  function $removeHighlightFromDOM(editor, cell) {
2461
2712
  const element = cell.elem;
@@ -2463,12 +2714,8 @@ function $removeHighlightFromDOM(editor, cell) {
2463
2714
  if (!$isTableCellNode(node)) {
2464
2715
  throw Error(`Expected to find LexicalNode from Table Cell DOMNode`);
2465
2716
  }
2466
- const backgroundColor = node.getBackgroundColor();
2467
- if (backgroundColor === null) {
2468
- element.style.removeProperty('background-color');
2469
- }
2470
- element.style.removeProperty('background-image');
2471
- element.style.removeProperty('caret-color');
2717
+ const editorThemeClasses = editor._config.theme;
2718
+ removeClassNamesFromElement(element, editorThemeClasses.tableCellSelected);
2472
2719
  }
2473
2720
  function $findCellNode(node) {
2474
2721
  const cellNode = $findMatchingParent(node, $isTableCellNode);
@@ -2559,8 +2806,8 @@ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
2559
2806
  const lastCellCoords = tableNode.getCordsFromCellNode(lastCellNode, tableObserver.table);
2560
2807
  const firstCellDOM = tableNode.getDOMCellFromCordsOrThrow(firstCellCoords.x, firstCellCoords.y, tableObserver.table);
2561
2808
  const lastCellDOM = tableNode.getDOMCellFromCordsOrThrow(lastCellCoords.x, lastCellCoords.y, tableObserver.table);
2562
- tableObserver.setAnchorCellForSelection(firstCellDOM);
2563
- tableObserver.setFocusCellForSelection(lastCellDOM, true);
2809
+ tableObserver.$setAnchorCellForSelection(firstCellDOM);
2810
+ tableObserver.$setFocusCellForSelection(lastCellDOM, true);
2564
2811
  return true;
2565
2812
  }
2566
2813
  }
@@ -2627,7 +2874,7 @@ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
2627
2874
  return false;
2628
2875
  }
2629
2876
  if (isExitingTableAnchor(anchorType, anchorOffset, anchorNode, direction)) {
2630
- return $handleTableExit(event, anchorNode, tableNode, direction);
2877
+ return $handleTableExit(event, anchorNode, anchorCellNode, tableNode, direction);
2631
2878
  }
2632
2879
  return false;
2633
2880
  }
@@ -2640,7 +2887,7 @@ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
2640
2887
  if (anchor.type === 'element') {
2641
2888
  edgeSelectionRect = anchorDOM.getBoundingClientRect();
2642
2889
  } else {
2643
- const domSelection = window.getSelection();
2890
+ const domSelection = getDOMSelection(getEditorWindow(editor));
2644
2891
  if (domSelection === null || domSelection.rangeCount === 0) {
2645
2892
  return false;
2646
2893
  }
@@ -2662,8 +2909,8 @@ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
2662
2909
  const cords = tableNode.getCordsFromCellNode(anchorCellNode, tableObserver.table);
2663
2910
  if (event.shiftKey) {
2664
2911
  const cell = tableNode.getDOMCellFromCordsOrThrow(cords.x, cords.y, tableObserver.table);
2665
- tableObserver.setAnchorCellForSelection(cell);
2666
- tableObserver.setFocusCellForSelection(cell, true);
2912
+ tableObserver.$setAnchorCellForSelection(cell);
2913
+ tableObserver.$setFocusCellForSelection(cell, true);
2667
2914
  } else {
2668
2915
  return selectTableNodeInDirection(tableObserver, tableNode, cords.x, cords.y, direction);
2669
2916
  }
@@ -2684,15 +2931,15 @@ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
2684
2931
  if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableNode(tableNodeFromSelection) || tableElement == null) {
2685
2932
  return false;
2686
2933
  }
2687
- tableObserver.updateTableTableSelection(selection);
2934
+ tableObserver.$updateTableTableSelection(selection);
2688
2935
  const grid = getTable(tableNodeFromSelection, tableElement);
2689
2936
  const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid);
2690
2937
  const anchorCell = tableNode.getDOMCellFromCordsOrThrow(cordsAnchor.x, cordsAnchor.y, grid);
2691
- tableObserver.setAnchorCellForSelection(anchorCell);
2938
+ tableObserver.$setAnchorCellForSelection(anchorCell);
2692
2939
  stopEvent(event);
2693
2940
  if (event.shiftKey) {
2694
- const cords = tableNode.getCordsFromCellNode(focusCellNode, grid);
2695
- return adjustFocusNodeInDirection(tableObserver, tableNodeFromSelection, cords.x, cords.y, direction);
2941
+ const [tableMap, anchorValue, focusValue] = $computeTableMap(tableNode, anchorCellNode, focusCellNode);
2942
+ return $adjustFocusInDirection(tableObserver, tableMap, anchorValue, focusValue, direction);
2696
2943
  } else {
2697
2944
  focusCellNode.selectEnd();
2698
2945
  }
@@ -2728,11 +2975,7 @@ function $isExitingTableTextAnchor(type, offset, anchorNode, direction) {
2728
2975
  const hasValidOffset = direction === 'backward' ? offset === 0 : offset === anchorNode.getTextContentSize();
2729
2976
  return type === 'text' && hasValidOffset && (direction === 'backward' ? parentNode.getPreviousSibling() === null : parentNode.getNextSibling() === null);
2730
2977
  }
2731
- function $handleTableExit(event, anchorNode, tableNode, direction) {
2732
- const anchorCellNode = $findMatchingParent(anchorNode, $isTableCellNode);
2733
- if (!$isTableCellNode(anchorCellNode)) {
2734
- return false;
2735
- }
2978
+ function $handleTableExit(event, anchorNode, anchorCellNode, tableNode, direction) {
2736
2979
  const [tableMap, cellValue] = $computeTableMap(tableNode, anchorCellNode, anchorCellNode);
2737
2980
  if (!isExitingCell(tableMap, cellValue, direction)) {
2738
2981
  return false;
@@ -2783,7 +3026,7 @@ function $getTableEdgeCursorPosition(editor, selection, tableNode) {
2783
3026
  }
2784
3027
 
2785
3028
  // TODO: Add support for nested tables
2786
- const domSelection = window.getSelection();
3029
+ const domSelection = getDOMSelection(getEditorWindow(editor));
2787
3030
  if (!domSelection) {
2788
3031
  return undefined;
2789
3032
  }
@@ -2823,6 +3066,13 @@ function $getTableEdgeCursorPosition(editor, selection, tableNode) {
2823
3066
  return undefined;
2824
3067
  }
2825
3068
  }
3069
+ function $getObserverCellFromCellNodeOrThrow(tableObserver, tableCellNode) {
3070
+ const {
3071
+ tableNode
3072
+ } = tableObserver.$lookup();
3073
+ const currentCords = tableNode.getCordsFromCellNode(tableCellNode, tableObserver.table);
3074
+ return tableNode.getDOMCellFromCordsOrThrow(currentCords.x, currentCords.y, tableObserver.table);
3075
+ }
2826
3076
 
2827
3077
  /**
2828
3078
  * Copyright (c) Meta Platforms, Inc. and affiliates.