@atlaskit/editor-plugin-table 24.2.2 → 24.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cjs/pm-plugins/commands/active-table-menu.js +14 -5
  3. package/dist/cjs/pm-plugins/drag-and-drop/plugin.js +3 -1
  4. package/dist/cjs/pm-plugins/main.js +3 -1
  5. package/dist/cjs/pm-plugins/utils/column-controls.js +119 -2
  6. package/dist/cjs/pm-plugins/utils/dom.js +23 -1
  7. package/dist/cjs/pm-plugins/utils/row-controls.js +43 -1
  8. package/dist/cjs/pm-plugins/utils/selection.js +74 -1
  9. package/dist/cjs/ui/DragHandle/index.js +46 -4
  10. package/dist/cjs/ui/FloatingInsertButton/getPopupOptions.js +19 -5
  11. package/dist/cjs/ui/FloatingInsertButton/index.js +37 -3
  12. package/dist/cjs/ui/FloatingTableMenu/index.js +4 -7
  13. package/dist/cjs/ui/TableFloatingColumnControls/ColumnControls/index.js +26 -15
  14. package/dist/cjs/ui/TableFloatingColumnControls/index.js +1 -1
  15. package/dist/cjs/ui/TableFloatingControls/RowControls/DragControls.js +29 -12
  16. package/dist/cjs/ui/common-styles.js +6 -1
  17. package/dist/cjs/ui/event-handlers.js +61 -8
  18. package/dist/es2019/pm-plugins/commands/active-table-menu.js +14 -5
  19. package/dist/es2019/pm-plugins/drag-and-drop/plugin.js +3 -1
  20. package/dist/es2019/pm-plugins/main.js +1 -1
  21. package/dist/es2019/pm-plugins/utils/column-controls.js +114 -2
  22. package/dist/es2019/pm-plugins/utils/dom.js +20 -0
  23. package/dist/es2019/pm-plugins/utils/row-controls.js +42 -0
  24. package/dist/es2019/pm-plugins/utils/selection.js +73 -0
  25. package/dist/es2019/ui/DragHandle/index.js +47 -4
  26. package/dist/es2019/ui/FloatingInsertButton/getPopupOptions.js +24 -5
  27. package/dist/es2019/ui/FloatingInsertButton/index.js +35 -3
  28. package/dist/es2019/ui/FloatingTableMenu/index.js +4 -7
  29. package/dist/es2019/ui/TableFloatingColumnControls/ColumnControls/index.js +27 -16
  30. package/dist/es2019/ui/TableFloatingColumnControls/index.js +2 -2
  31. package/dist/es2019/ui/TableFloatingControls/RowControls/DragControls.js +30 -12
  32. package/dist/es2019/ui/common-styles.js +26 -1
  33. package/dist/es2019/ui/event-handlers.js +61 -8
  34. package/dist/esm/pm-plugins/commands/active-table-menu.js +14 -5
  35. package/dist/esm/pm-plugins/drag-and-drop/plugin.js +3 -1
  36. package/dist/esm/pm-plugins/main.js +3 -1
  37. package/dist/esm/pm-plugins/utils/column-controls.js +118 -1
  38. package/dist/esm/pm-plugins/utils/dom.js +22 -0
  39. package/dist/esm/pm-plugins/utils/row-controls.js +42 -0
  40. package/dist/esm/pm-plugins/utils/selection.js +73 -0
  41. package/dist/esm/ui/DragHandle/index.js +47 -5
  42. package/dist/esm/ui/FloatingInsertButton/getPopupOptions.js +19 -5
  43. package/dist/esm/ui/FloatingInsertButton/index.js +37 -3
  44. package/dist/esm/ui/FloatingTableMenu/index.js +4 -7
  45. package/dist/esm/ui/TableFloatingColumnControls/ColumnControls/index.js +28 -17
  46. package/dist/esm/ui/TableFloatingColumnControls/index.js +2 -2
  47. package/dist/esm/ui/TableFloatingControls/RowControls/DragControls.js +31 -14
  48. package/dist/esm/ui/common-styles.js +7 -2
  49. package/dist/esm/ui/event-handlers.js +63 -10
  50. package/dist/types/pm-plugins/commands/active-table-menu.d.ts +3 -1
  51. package/dist/types/pm-plugins/utils/column-controls.d.ts +35 -1
  52. package/dist/types/pm-plugins/utils/dom.d.ts +11 -0
  53. package/dist/types/pm-plugins/utils/row-controls.d.ts +16 -0
  54. package/dist/types/pm-plugins/utils/selection.d.ts +20 -0
  55. package/dist/types/ui/DragHandle/index.d.ts +3 -2
  56. package/dist/types/ui/FloatingInsertButton/getPopupOptions.d.ts +1 -1
  57. package/dist/types/ui/FloatingInsertButton/index.d.ts +1 -0
  58. package/dist/types/ui/event-handlers.d.ts +1 -1
  59. package/package.json +4 -4
@@ -29,6 +29,7 @@ var _analytics2 = require("../pm-plugins/utils/analytics");
29
29
  var _columnControls = require("../pm-plugins/utils/column-controls");
30
30
  var _dom = require("../pm-plugins/utils/dom");
31
31
  var _getAllowAddColumnCustomStep = require("../pm-plugins/utils/get-allow-add-column-custom-step");
32
+ var _rowControls = require("../pm-plugins/utils/row-controls");
32
33
  var _types = require("../types");
33
34
  var _consts = require("./TableMenu/shared/consts");
34
35
  var isFocusingCalendar = function isFocusingCalendar(event) {
@@ -206,14 +207,27 @@ var handleMouseUp = exports.handleMouseUp = function handleMouseUp(view, mouseEv
206
207
  }
207
208
  return false;
208
209
  };
210
+ var resetDragHandleSelectedIntentOnMouseDown = function resetDragHandleSelectedIntentOnMouseDown(event, api) {
211
+ var _api$userIntent;
212
+ if ((0, _dom.isTableDragHandleButton)(event.target)) {
213
+ return;
214
+ }
215
+ if ((api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 || (_api$userIntent = _api$userIntent.sharedState.currentState()) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.currentUserIntent) === 'dragHandleSelected') {
216
+ var _api$core, _api$userIntent2;
217
+ api === null || api === void 0 || (_api$core = api.core) === null || _api$core === void 0 || _api$core.actions.execute(api === null || api === void 0 || (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 ? void 0 : _api$userIntent2.commands.setCurrentUserIntent('default'));
218
+ }
219
+ };
209
220
 
210
221
  // Ignore any `mousedown` `event` from control and numbered column buttons
211
222
  // PM end up changing selection during shift selection if not prevented
212
- var handleMouseDown = exports.handleMouseDown = function handleMouseDown(_, event) {
223
+ var handleMouseDown = exports.handleMouseDown = function handleMouseDown(_view, event, api) {
213
224
  var isControl = !!(event.target && event.target instanceof HTMLElement && ((0, _dom.isTableContainerOrWrapper)(event.target) || (0, _dom.isColumnControlsDecorations)(event.target) || (0, _dom.isRowControlsButton)(event.target) || (0, _dom.isDragCornerButton)(event.target)));
214
225
  if (isControl) {
215
226
  event.preventDefault();
216
227
  }
228
+ if ((0, _expValEquals.expValEquals)('platform_editor_table_menu_updates', 'isEnabled', true)) {
229
+ resetDragHandleSelectedIntentOnMouseDown(event, api);
230
+ }
217
231
  return isControl;
218
232
  };
219
233
  var handleMouseOut = exports.handleMouseOut = function handleMouseOut(view, mouseEvent) {
@@ -292,12 +306,26 @@ var handleMouseMoveDebounce = function handleMouseMoveDebounce(nodeViewPortalPro
292
306
  return false;
293
307
  }
294
308
  var element = event.target;
309
+ var isTableMenuUpdatesEnabled = (0, _expValEquals.expValEquals)('platform_editor_table_menu_updates', 'isEnabled', true);
310
+
311
+ // Spanned cells need mouse-position-based hover indexes; normal cells already report them.
312
+ if (isTableMenuUpdatesEnabled) {
313
+ // eslint-disable-next-line @atlaskit/editor/no-as-casting
314
+ var tableCell = (0, _utils.isElementInTableCell)(element);
315
+ if (tableCell && (tableCell.rowSpan > 1 || tableCell.colSpan > 1)) {
316
+ var dragDropState = (0, _pluginFactory.getPluginState)(view.state);
317
+ if (dragDropState && !dragDropState.isDragging) {
318
+ trackCellLocation(view, event);
319
+ }
320
+ }
321
+ }
295
322
  if ((0, _dom.isColumnControlsDecorations)(element) || (0, _dom.isDragColumnFloatingInsertDot)(element)) {
296
323
  var state = view.state,
297
324
  dispatch = view.dispatch;
298
325
  var _getPluginState6 = (0, _pluginFactory2.getPluginState)(state),
299
326
  insertColumnButtonIndex = _getPluginState6.insertColumnButtonIndex;
300
- var _getColumnOrRowIndex9 = (0, _dom.getColumnOrRowIndex)(element),
327
+ var indexSourceElement = isTableMenuUpdatesEnabled ? (0, _dom.getIndexAttributeSourceElement)(element) : element;
328
+ var _getColumnOrRowIndex9 = (0, _dom.getColumnOrRowIndex)(indexSourceElement),
301
329
  _getColumnOrRowIndex0 = (0, _slicedToArray2.default)(_getColumnOrRowIndex9, 2),
302
330
  startIndex = _getColumnOrRowIndex0[0],
303
331
  endIndex = _getColumnOrRowIndex0[1];
@@ -311,7 +339,8 @@ var handleMouseMoveDebounce = function handleMouseMoveDebounce(nodeViewPortalPro
311
339
  _dispatch4 = view.dispatch;
312
340
  var _getPluginState7 = (0, _pluginFactory2.getPluginState)(_state3),
313
341
  insertRowButtonIndex = _getPluginState7.insertRowButtonIndex;
314
- var _getColumnOrRowIndex1 = (0, _dom.getColumnOrRowIndex)(element),
342
+ var _indexSourceElement = isTableMenuUpdatesEnabled ? (0, _dom.getIndexAttributeSourceElement)(element) : element;
343
+ var _getColumnOrRowIndex1 = (0, _dom.getColumnOrRowIndex)(_indexSourceElement),
315
344
  _getColumnOrRowIndex10 = (0, _slicedToArray2.default)(_getColumnOrRowIndex1, 2),
316
345
  _startIndex3 = _getColumnOrRowIndex10[0],
317
346
  _endIndex2 = _getColumnOrRowIndex10[1];
@@ -329,8 +358,8 @@ var handleMouseMoveDebounce = function handleMouseMoveDebounce(nodeViewPortalPro
329
358
  resizeHandleColumnIndex = _getPluginState8.resizeHandleColumnIndex,
330
359
  resizeHandleRowIndex = _getPluginState8.resizeHandleRowIndex;
331
360
  var isKeyboardResize = (0, _pluginFactory2.getPluginState)(_state4).isKeyboardResize;
332
- var tableCell = (0, _utils.closestElement)(element, 'td, th');
333
- var cellStartPosition = view.posAtDOM(tableCell, 0);
361
+ var _tableCell = (0, _utils.closestElement)(element, 'td, th');
362
+ var cellStartPosition = view.posAtDOM(_tableCell, 0);
334
363
  var rect = (0, _utils3.findCellRectClosestToPos)(_state4.doc.resolve(cellStartPosition));
335
364
  if (rect) {
336
365
  var columnEndIndexTarget = _positionColumn === 'left' ? rect.left : rect.right;
@@ -459,7 +488,8 @@ var whenTableInFocus = exports.whenTableInFocus = function whenTableInFocus(even
459
488
  };
460
489
  };
461
490
  var trackCellLocation = function trackCellLocation(view, mouseEvent) {
462
- var _tableElement$dataset;
491
+ var _tableElement$dataset, _getRowIndexByMousePo;
492
+ var isTableMenuUpdatesEnabled = (0, _expValEquals.expValEquals)('platform_editor_table_menu_updates', 'isEnabled', true);
463
493
  var target = mouseEvent.target;
464
494
  // Ignored via go/ees005
465
495
  // eslint-disable-next-line @atlaskit/editor/no-as-casting
@@ -484,13 +514,36 @@ var trackCellLocation = function trackCellLocation(view, mouseEvent) {
484
514
  // eslint-disable-next-line @atlaskit/editor/no-as-casting
485
515
  var rowElement = (0, _utils.closestElement)(target, 'tr');
486
516
  var htmlRowIndex = rowElement && rowElement.rowIndex;
517
+ var rowIndex = maybeTableCell.rowSpan > 1 && mouseEvent instanceof MouseEvent && isTableMenuUpdatesEnabled ? (_getRowIndexByMousePo = (0, _rowControls.getRowIndexByMousePosition)(tableRef, mouseEvent, {
518
+ startIndex: htmlRowIndex,
519
+ endIndex: htmlRowIndex + maybeTableCell.rowSpan
520
+ })) !== null && _getRowIndexByMousePo !== void 0 ? _getRowIndexByMousePo : htmlRowIndex : htmlRowIndex;
487
521
  var tableMap = tableNode && _tableMap.TableMap.get(tableNode);
488
522
  var colIndex = htmlColIndex;
489
523
  if (tableMap) {
490
524
  var convertedColIndex = (0, _columnControls.convertHTMLCellIndexToColumnIndex)(htmlColIndex, htmlRowIndex, tableMap);
491
- colIndex = (0, _columnControls.getColumnIndexMappedToColumnIndexInFirstRow)(convertedColIndex, htmlRowIndex, tableMap);
525
+ if (isTableMenuUpdatesEnabled) {
526
+ // New behaviour: the column controls grid renders one track per visual column
527
+ // (`getColumnsWidthsWithMergedCells`), so the hover index must be the visual column
528
+ // index — not the first-row-cell index. Mapping to the first-row cell would collapse
529
+ // every column under a merged first-row cell to the same index, leaving the drag handle
530
+ // stuck when hovering cells in other rows. For a colspanned cell, refine the converted
531
+ // (left-most) visual column to the exact visual column under the pointer.
532
+ colIndex = convertedColIndex;
533
+ if (maybeTableCell.colSpan > 1 && mouseEvent instanceof MouseEvent) {
534
+ var _getColumnIndexByMous;
535
+ colIndex = (_getColumnIndexByMous = (0, _columnControls.getColumnIndexByMousePosition)(maybeTableCell, mouseEvent, {
536
+ startIndex: convertedColIndex,
537
+ endIndex: convertedColIndex + maybeTableCell.colSpan
538
+ })) !== null && _getColumnIndexByMous !== void 0 ? _getColumnIndexByMous : convertedColIndex;
539
+ }
540
+ } else {
541
+ // Old behaviour: the legacy grid renders one track per first-row cell, so snap to the
542
+ // first-row column index (a merged first-row cell collapses to a single column track).
543
+ colIndex = (0, _columnControls.getColumnIndexMappedToColumnIndexInFirstRow)(convertedColIndex, htmlRowIndex, tableMap);
544
+ }
492
545
  }
493
- (0, _commands.hoverCell)(htmlRowIndex, colIndex)(view.state, view.dispatch);
546
+ (0, _commands.hoverCell)(rowIndex, colIndex)(view.state, view.dispatch);
494
547
  };
495
548
  var withCellTracking = exports.withCellTracking = function withCellTracking(eventHandler) {
496
549
  return function (view, mouseEvent) {
@@ -1,12 +1,19 @@
1
1
  import { pluginKey } from '../plugin-key';
2
+ import { isFullRowOrColumnSelected } from '../utils/selection';
2
3
  const applyMenuUserIntent = (tr, api, nextActiveTableMenu) => {
3
- var _api$userIntent;
4
- const isRowOrColumnMenuOpen = nextActiveTableMenu.type === 'row' || nextActiveTableMenu.type === 'column';
5
- api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.commands.setCurrentUserIntent(isRowOrColumnMenuOpen ? 'tableDragMenuPopupOpen' : 'default')({
4
+ var _api$userIntent2;
5
+ if (nextActiveTableMenu.type === 'row' || nextActiveTableMenu.type === 'column') {
6
+ var _api$userIntent;
7
+ api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.commands.setCurrentUserIntent('tableDragMenuPopupOpen')({
8
+ tr
9
+ });
10
+ return;
11
+ }
12
+ api === null || api === void 0 ? void 0 : (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 ? void 0 : _api$userIntent2.commands.setCurrentUserIntent(isFullRowOrColumnSelected(tr.selection) ? 'dragHandleSelected' : 'default')({
6
13
  tr
7
14
  });
8
15
  };
9
- export const closeActiveTableMenu = api => ({
16
+ export const closeActiveTableMenu = (api, options) => ({
10
17
  tr
11
18
  }) => {
12
19
  const nextActiveTableMenu = {
@@ -18,7 +25,9 @@ export const closeActiveTableMenu = api => ({
18
25
  activeTableMenu: nextActiveTableMenu
19
26
  }
20
27
  });
21
- applyMenuUserIntent(tr, api, nextActiveTableMenu);
28
+ if (!(options !== null && options !== void 0 && options.skipUserIntent)) {
29
+ applyMenuUserIntent(tr, api, nextActiveTableMenu);
30
+ }
22
31
  if (!tr.docChanged) {
23
32
  tr.setMeta('addToHistory', false);
24
33
  }
@@ -98,7 +98,9 @@ const destroyFn = (editorView, editorAnalyticsAPI, isTableScalingEnabled, isTabl
98
98
  tr
99
99
  }) => {
100
100
  var _api$userIntent;
101
- closeActiveTableMenu(api)({
101
+ closeActiveTableMenu(api, {
102
+ skipUserIntent: true
103
+ })({
102
104
  tr
103
105
  });
104
106
  api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.commands.setCurrentUserIntent('dragging')({
@@ -346,7 +346,7 @@ export const createPlugin = (dispatchAnalyticsEvent, dispatch, portalProviderAPI
346
346
  handleDOMEvents: {
347
347
  focus: handleFocus,
348
348
  blur: handleBlur,
349
- mousedown: withCellTracking(handleMouseDown),
349
+ mousedown: withCellTracking((view, event) => handleMouseDown(view, event, pluginInjectionApi)),
350
350
  mouseleave: handleMouseLeave,
351
351
  mousemove: whenTableInFocus(handleMouseMove(nodeViewPortalProviderAPI), pluginInjectionApi),
352
352
  mouseenter: handleMouseEnter,
@@ -34,6 +34,69 @@ export const getColumnsWidths = view => {
34
34
  }
35
35
  return widths;
36
36
  };
37
+
38
+ /**
39
+ * Splits a merged cell's rendered width by `colwidth` ratios, falling back to an even split when
40
+ * usable ratios are unavailable.
41
+ */
42
+ export const getProportionalColumnWidths = (totalWidth, columnCount, ratios) => {
43
+ const evenSplit = () => new Array(columnCount).fill(totalWidth / columnCount);
44
+ if (!ratios || ratios.length !== columnCount) {
45
+ return evenSplit();
46
+ }
47
+ const total = ratios.reduce((sum, ratio) => sum + (ratio > 0 ? ratio : 0), 0);
48
+ if (total <= 0) {
49
+ return evenSplit();
50
+ }
51
+ return ratios.map(ratio => totalWidth * ((ratio > 0 ? ratio : 0) / total));
52
+ };
53
+
54
+ /**
55
+ * Like `getColumnsWidths`, but fills every visual column under a first-row `colspan` so column
56
+ * controls can render one grid track per column.
57
+ */
58
+ export const getColumnsWidthsWithMergedCells = view => {
59
+ const {
60
+ selection
61
+ } = view.state;
62
+ const table = findTable(selection);
63
+ if (!table) {
64
+ return [];
65
+ }
66
+ const map = TableMap.get(table.node);
67
+ const domAtPos = view.domAtPos.bind(view);
68
+ const widths = Array.from({
69
+ length: map.width
70
+ });
71
+ for (let i = 0; i < map.width; i++) {
72
+ var _node$attrs$colspan;
73
+ if (map.isCellMergedTopLeft(0, i)) {
74
+ continue;
75
+ }
76
+
77
+ // Ignored via go/ees005
78
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
79
+ const node = table.node.nodeAt(map.map[i]);
80
+ const pos = map.map[i] + table.start;
81
+ // Ignored via go/ees005
82
+ // eslint-disable-next-line @atlaskit/editor/no-as-casting
83
+ const cellRef = findDomRefAtPos(pos, domAtPos);
84
+ const rect = cellRef.getBoundingClientRect();
85
+ const measuredWidth = (rect ? rect.width : cellRef.offsetWidth) + 1;
86
+ const colspan = (_node$attrs$colspan = node.attrs.colspan) !== null && _node$attrs$colspan !== void 0 ? _node$attrs$colspan : 1;
87
+ if (colspan <= 1) {
88
+ widths[i] = measuredWidth;
89
+ continue;
90
+ }
91
+ const colwidth = Array.isArray(node.attrs.colwidth) ? node.attrs.colwidth : undefined;
92
+ const perColumnWidths = getProportionalColumnWidths(measuredWidth, colspan, colwidth);
93
+ for (let span = 0; span < colspan && i + span < map.width; span++) {
94
+ widths[i + span] = perColumnWidths[span];
95
+ }
96
+ i += colspan - 1;
97
+ }
98
+ return widths;
99
+ };
37
100
  export const getColumnDeleteButtonParams = (columnsWidths, selection) => {
38
101
  const rect = getSelectionRect(selection);
39
102
  if (!rect) {
@@ -108,7 +171,9 @@ const getRelativeDomCellWidths = ({
108
171
  const totalCalculatedCellWidth = cellColWidths.reduce((acc, cellColWidth) => acc + cellColWidth, 0);
109
172
  return cellColWidths.map(cellColWidth => width * (cellColWidth / totalCalculatedCellWidth));
110
173
  };
111
- export const colWidthsForRow = tr => {
174
+ export const colWidthsForRow = (tr, {
175
+ useColwidthRatios = false
176
+ } = {}) => {
112
177
  // get the colspans
113
178
  const rowColSpans = maphElem(tr, cell => Number(cell.getAttribute('colspan') || 1 /* default to span of 1 */));
114
179
 
@@ -132,7 +197,7 @@ export const colWidthsForRow = tr => {
132
197
  // reverse engineer cell widths from table widths
133
198
  const domBasedCellWidths = [];
134
199
  cellInfos.map(cell => {
135
- domBasedCellWidths.push(...getRelativeDomCellWidths(cell));
200
+ domBasedCellWidths.push(...(useColwidthRatios ? getRelativeDomCellWidths(cell) : new Array(cell.colspan).fill(cell.width / cell.colspan)));
136
201
  });
137
202
  if (cellInfos.reduce((acc, cell) => acc + cell.width, 0) !== 0) {
138
203
  const newWidths = mapTableColwidthsToRow(rowColSpans, domBasedCellWidths);
@@ -146,6 +211,53 @@ export const colWidthsForRow = tr => {
146
211
  const pctWidths = rowColSpans.map(cellColSpan => cellColSpan / visualColCount * 100);
147
212
  return pctWidths.map(pct => `${pct}%`).join(' ');
148
213
  };
214
+
215
+ /**
216
+ * Returns the visual column index that the mouse pointer is over within a column-spanned
217
+ * cell, by walking the per-column boundaries of the spanned cell and finding the column whose
218
+ * horizontal range contains `mouseEvent.clientX`.
219
+ *
220
+ * `<td>.cellIndex` for a cell with `colspan > 1` only resolves to the first column the cell
221
+ * occupies, so hovering anywhere inside a colspanned first-row cell pins the column drag handle
222
+ * to that first column. This resolver disambiguates which visual column the pointer is actually
223
+ * over so the handle/menu can target a single column.
224
+ *
225
+ * The search is restricted to columns in `[startIndex, endIndex)` (the cell's own span). The
226
+ * spanned cell's bounding rect is read once and split into per-column boundaries using the cell's
227
+ * `data-colwidth` ratios (falling back to an even split for unresized columns) — this keeps the
228
+ * work bounded by the colspan size and avoids measuring the whole table.
229
+ *
230
+ * Returns `undefined` when the mouse is outside the spanned range (so callers can fall back to
231
+ * the converted HTML column index).
232
+ */
233
+ export const getColumnIndexByMousePosition = (cellElement, mouseEvent, columnIndexRange) => {
234
+ var _cellElement$dataset$;
235
+ const {
236
+ startIndex,
237
+ endIndex
238
+ } = columnIndexRange;
239
+ const columnCount = endIndex - startIndex;
240
+ if (columnCount <= 0) {
241
+ return undefined;
242
+ }
243
+ const cellRect = cellElement.getBoundingClientRect();
244
+ if (mouseEvent.clientX < cellRect.left || mouseEvent.clientX >= cellRect.right) {
245
+ return undefined;
246
+ }
247
+
248
+ // Read DOM ratios here to avoid resolving the ProseMirror node on this mouse-move path.
249
+ const ratios = (_cellElement$dataset$ = cellElement.dataset.colwidth) === null || _cellElement$dataset$ === void 0 ? void 0 : _cellElement$dataset$.split(',').map(Number).filter(value => !Number.isNaN(value));
250
+ const perColumnWidths = getProportionalColumnWidths(cellRect.width, columnCount, ratios);
251
+ const offsetFromLeft = mouseEvent.clientX - cellRect.left;
252
+ let cumulativeWidth = 0;
253
+ for (let column = 0; column < columnCount; column++) {
254
+ cumulativeWidth += perColumnWidths[column];
255
+ if (offsetFromLeft < cumulativeWidth) {
256
+ return startIndex + column;
257
+ }
258
+ }
259
+ return endIndex - 1;
260
+ };
149
261
  export const convertHTMLCellIndexToColumnIndex = (htmlColIndex, htmlRowIndex, tableMap) => {
150
262
  // Same numbers (positions) in tableMap.map array mean that there are merged cells.
151
263
  // Cells can be merged across columns. So we need to check if the cell on the left and current cell have the same value.
@@ -6,6 +6,25 @@ export const isCell = node => {
6
6
  export const isCornerButton = node => containsClassName(node, ClassName.CONTROLS_CORNER_BUTTON);
7
7
  export const isInsertRowButton = node => containsClassName(node, ClassName.CONTROLS_INSERT_ROW) || closestElement(node, `.${ClassName.CONTROLS_INSERT_ROW}`) || containsClassName(node, ClassName.CONTROLS_BUTTON_OVERLAY) && closestElement(node, `.${ClassName.ROW_CONTROLS}`);
8
8
  export const getColumnOrRowIndex = target => [parseInt(target.getAttribute('data-start-index') || '-1', 10), parseInt(target.getAttribute('data-end-index') || '-1', 10)];
9
+
10
+ /**
11
+ * Returns the element that carries the `data-start-index` / `data-end-index` attributes for a
12
+ * column/row control.
13
+ *
14
+ * The floating insert dot is a child of a wrapper that holds the index attributes, so when the
15
+ * pointer is directly over the dot the attributes are not on the event target. This walks up to
16
+ * the nearest ancestor that has `data-start-index`, falling back to the original element so the
17
+ * behaviour is unchanged when the target already carries the attributes.
18
+ */
19
+ export const getIndexAttributeSourceElement = target => {
20
+ if (target.hasAttribute('data-start-index')) {
21
+ return target;
22
+ }
23
+ // Ignored via go/ees005
24
+ // eslint-disable-next-line @atlaskit/editor/no-as-casting
25
+ const closestWithIndex = target.closest('[data-start-index]');
26
+ return closestWithIndex !== null && closestWithIndex !== void 0 ? closestWithIndex : target;
27
+ };
9
28
  export const isColumnControlsDecorations = node => containsClassName(node, ClassName.COLUMN_CONTROLS_DECORATIONS);
10
29
  export const isRowControlsButton = node => containsClassName(node, ClassName.ROW_CONTROLS_BUTTON) || containsClassName(node, ClassName.NUMBERED_COLUMN_BUTTON);
11
30
  export const isResizeHandleDecoration = node => containsClassName(node, ClassName.RESIZE_HANDLE_DECORATION);
@@ -16,6 +35,7 @@ export const isTableContainerOrWrapper = node => containsClassName(node, ClassNa
16
35
  export const isDragRowFloatingInsertDot = node => containsClassName(node, ClassName.DRAG_ROW_FLOATING_INSERT_DOT_WRAPPER);
17
36
  export const isDragColumnFloatingInsertDot = node => containsClassName(node, ClassName.DRAG_COLUMN_FLOATING_INSERT_DOT_WRAPPER);
18
37
  export const isDragCornerButton = node => containsClassName(node, ClassName.DRAG_CORNER_BUTTON) || containsClassName(node, ClassName.DRAG_CORNER_BUTTON_INNER);
38
+ export const isTableDragHandleButton = target => target instanceof HTMLElement && Boolean(target.closest(`.${ClassName.DRAG_HANDLE_BUTTON_CONTAINER}`));
19
39
 
20
40
  /*
21
41
  * This function returns which side of a given element the mouse cursor is,
@@ -74,6 +74,48 @@ export const getRowsParams = rowsHeights => {
74
74
  }
75
75
  return rows;
76
76
  };
77
+
78
+ /**
79
+ * Returns the visual row index that the mouse pointer is over, by walking the row heights
80
+ * inside `tbody` and finding the row whose vertical range contains `mouseEvent.clientY`.
81
+ *
82
+ * When `rowIndexRange` is provided, the search is restricted to rows in that range (the
83
+ * `endIndex` is exclusive). This is the hot path used on `mousemove` when hovering over a
84
+ * row-spanned cell — restricting the range to `[startIndex, endIndex)` keeps the number
85
+ * of forced layout reads bounded by the row-span size, not the table size.
86
+ *
87
+ * Returns `undefined` when the mouse is above the search range or below it (so callers can
88
+ * fall back to the HTML row index).
89
+ */
90
+ export const getRowIndexByMousePosition = (tableRef, mouseEvent, rowIndexRange) => {
91
+ var _rowIndexRange$startI, _rowIndexRange$endInd;
92
+ const tableBody = tableRef.querySelector('tbody');
93
+ if (!tableBody) {
94
+ return undefined;
95
+ }
96
+ const rows = tableBody.children;
97
+ const startIndex = (_rowIndexRange$startI = rowIndexRange === null || rowIndexRange === void 0 ? void 0 : rowIndexRange.startIndex) !== null && _rowIndexRange$startI !== void 0 ? _rowIndexRange$startI : 0;
98
+ const endIndex = Math.min((_rowIndexRange$endInd = rowIndexRange === null || rowIndexRange === void 0 ? void 0 : rowIndexRange.endIndex) !== null && _rowIndexRange$endInd !== void 0 ? _rowIndexRange$endInd : rows.length, rows.length);
99
+ if (startIndex >= endIndex) {
100
+ return undefined;
101
+ }
102
+ const firstRowRect = rows[startIndex].getBoundingClientRect();
103
+ if (mouseEvent.clientY < firstRowRect.top) {
104
+ return undefined;
105
+ }
106
+ let rowBottom = firstRowRect.bottom;
107
+ if (mouseEvent.clientY < rowBottom) {
108
+ return startIndex;
109
+ }
110
+ for (let rowIndex = startIndex + 1; rowIndex < endIndex; rowIndex++) {
111
+ const rowRect = rows[rowIndex].getBoundingClientRect();
112
+ rowBottom = rowRect.bottom;
113
+ if (mouseEvent.clientY < rowBottom) {
114
+ return rowIndex;
115
+ }
116
+ }
117
+ return undefined;
118
+ };
77
119
  export const getRowClassNames = (index, selection, hoveredRows = [], isInDanger, isResizing) => {
78
120
  const classNames = [];
79
121
  if (isRowSelected(index)(selection) || hoveredRows.indexOf(index) > -1 && !isResizing) {
@@ -1,3 +1,6 @@
1
+ import { CellSelection } from '@atlaskit/editor-tables/cell-selection';
2
+ import { TableMap } from '@atlaskit/editor-tables/table-map';
3
+ import { findTableClosestToPos, getSelectionRect } from '@atlaskit/editor-tables/utils';
1
4
  export const getSelectedColumnIndexes = selectionRect => {
2
5
  const columnIndexes = [];
3
6
  for (let i = selectionRect.left; i < selectionRect.right; i++) {
@@ -11,4 +14,74 @@ export const getSelectedRowIndexes = selectionRect => {
11
14
  rowIndexes.push(i);
12
15
  }
13
16
  return rowIndexes;
17
+ };
18
+
19
+ /**
20
+ * Treats a column as fully selected when its first-row area is covered by a horizontal merge and
21
+ * the explicit `CellSelection` starts below that merged cell. `CellSelection.isColSelection()`
22
+ * returns `false` in this case because the selection does not include the merged first-row cell.
23
+ */
24
+ export const isColumnSelectionWithMergedFirstRow = selection => {
25
+ const rect = getSelectionRect(selection);
26
+ if (!rect) {
27
+ return false;
28
+ }
29
+ const table = findTableClosestToPos(selection.$anchorCell);
30
+ if (!table) {
31
+ return false;
32
+ }
33
+ const map = TableMap.get(table.node);
34
+ if (rect.bottom !== map.height || rect.top === 0) {
35
+ return false;
36
+ }
37
+ for (let col = rect.left; col < rect.right; col++) {
38
+ const topCellRect = map.findCell(map.map[col]);
39
+ const isHorizontallyMerged = topCellRect.right - topCellRect.left > 1;
40
+ const bridgesGapAboveSelection = topCellRect.bottom >= rect.top;
41
+ if (!isHorizontallyMerged || !bridgesGapAboveSelection) {
42
+ return false;
43
+ }
44
+ }
45
+ return true;
46
+ };
47
+
48
+ /**
49
+ * Treats a row as fully selected when its first-column area is covered by a vertical merge and the
50
+ * explicit `CellSelection` starts to the right of that merged cell. `CellSelection.isRowSelection()`
51
+ * returns `false` in this case because the selection does not include the merged first-column cell.
52
+ */
53
+ export const isRowSelectionWithMergedFirstColumn = selection => {
54
+ const rect = getSelectionRect(selection);
55
+ if (!rect) {
56
+ return false;
57
+ }
58
+ const table = findTableClosestToPos(selection.$anchorCell);
59
+ if (!table) {
60
+ return false;
61
+ }
62
+ const map = TableMap.get(table.node);
63
+ if (rect.right !== map.width || rect.left === 0) {
64
+ return false;
65
+ }
66
+ for (let row = rect.top; row < rect.bottom; row++) {
67
+ const leftCellRect = map.findCell(map.map[row * map.width]);
68
+ const isVerticallyMerged = leftCellRect.bottom - leftCellRect.top > 1;
69
+ const bridgesGapLeftOfSelection = leftCellRect.right >= rect.left;
70
+ if (!isVerticallyMerged || !bridgesGapLeftOfSelection) {
71
+ return false;
72
+ }
73
+ }
74
+ return true;
75
+ };
76
+
77
+ /**
78
+ * Returns `true` when the selection covers one or more complete rows or columns (the kind of
79
+ * selection produced by clicking a row/column drag handle), including the merged-cell variants
80
+ * where `CellSelection.isRowSelection()` / `isColSelection()` return `false`.
81
+ */
82
+ export const isFullRowOrColumnSelected = selection => {
83
+ if (!(selection instanceof CellSelection)) {
84
+ return false;
85
+ }
86
+ return selection.isRowSelection() || selection.isColSelection() || isRowSelectionWithMergedFirstColumn(selection) || isColumnSelectionWithMergedFirstRow(selection);
14
87
  };
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @atlaskit/design-system/no-html-button */
2
2
 
3
- import React, { useEffect, useMemo, useRef, useState } from 'react';
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import classnames from 'classnames';
5
5
  import ReactDOM from 'react-dom';
6
6
  import { injectIntl } from 'react-intl';
@@ -11,6 +11,7 @@ import { findTable, TableMap } from '@atlaskit/editor-tables';
11
11
  import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
12
12
  import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
13
13
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
14
+ import { closeActiveTableMenu } from '../../pm-plugins/commands/active-table-menu';
14
15
  import { getPluginState as getDnDPluginState } from '../../pm-plugins/drag-and-drop/plugin-factory';
15
16
  import { findDuplicatePosition, hasMergedCellsInSelection } from '../../pm-plugins/utils/merged-cells';
16
17
  import { TableCssClassName as ClassName } from '../../types';
@@ -18,6 +19,7 @@ import { dragTableInsertColumnButtonSize } from '../consts';
18
19
  import { DragPreview } from '../DragPreview';
19
20
  import { HandleIconComponent } from './HandleIconComponent';
20
21
  const DragHandleComponent = ({
22
+ api,
21
23
  isDragMenuTarget,
22
24
  tableLocalId,
23
25
  direction = 'row',
@@ -148,6 +150,36 @@ const DragHandleComponent = ({
148
150
  }, [tableLocalId, direction, indexes, isRow, editorView.state.selection, hasMergedCells]);
149
151
  const showDragMenuAnchorId = isRow ? 'drag-handle-button-row' : 'drag-handle-button-column';
150
152
  const browser = getBrowserInfo();
153
+
154
+ // Clicking the drag handle's clickable zone is a plain row/column selection (not the menu
155
+ // trigger), so the selection toolbar should appear. The floating menu defers closing for clicks
156
+ // inside the drag controls, so we close it here and reset the user intent to 'default' (in the
157
+ // same transaction) so the toolbar is shown.
158
+ const closeActiveDragMenuAndShowToolbar = useCallback(() => {
159
+ var _api$table, _api$table$sharedStat, _api$core;
160
+ if (!api) {
161
+ return;
162
+ }
163
+ const activeTableMenu = (_api$table = api.table) === null || _api$table === void 0 ? void 0 : (_api$table$sharedStat = _api$table.sharedState.currentState()) === null || _api$table$sharedStat === void 0 ? void 0 : _api$table$sharedStat.activeTableMenu;
164
+ const isActiveTableMenuOpen = (activeTableMenu === null || activeTableMenu === void 0 ? void 0 : activeTableMenu.type) === 'row' || (activeTableMenu === null || activeTableMenu === void 0 ? void 0 : activeTableMenu.type) === 'column';
165
+ if (!isActiveTableMenuOpen) {
166
+ return;
167
+ }
168
+ (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
169
+ tr
170
+ }) => {
171
+ var _api$userIntent;
172
+ closeActiveTableMenu(api, {
173
+ skipUserIntent: true
174
+ })({
175
+ tr
176
+ });
177
+ (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.commands.setCurrentUserIntent('default')({
178
+ tr
179
+ });
180
+ return tr;
181
+ });
182
+ }, [api]);
151
183
  return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
152
184
  type: "button"
153
185
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
@@ -175,7 +207,10 @@ const DragHandleComponent = ({
175
207
  // return focus to editor so copying table selections whilst still works, i cannot call e.preventDefault in a mousemove event as this stops dragstart events from firing
176
208
  // -> this is bad for a11y but is the current standard new copy/paste keyboard shortcuts should be introduced instead
177
209
  editorView.focus();
178
- if (isDragMenuOpen) {
210
+ if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
211
+ // New menu: close the open row/column menu (and show the selection toolbar).
212
+ closeActiveDragMenuAndShowToolbar();
213
+ } else if (isDragMenuOpen) {
179
214
  toggleDragMenu && toggleDragMenu('mouse', e);
180
215
  }
181
216
  },
@@ -216,12 +251,20 @@ const DragHandleComponent = ({
216
251
  // return focus to editor so copying table selections whilst still works, i cannot call e.preventDefault in a mousemove event as this stops dragstart events from firing
217
252
  // -> this is bad for a11y but is the current standard new copy/paste keyboard shortcuts should be introduced instead
218
253
  editorView.focus();
219
- toggleDragMenu && toggleDragMenu('mouse', e);
254
+ if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
255
+ toggleDragMenu && toggleDragMenu('mouse', e, indexes[0]);
256
+ } else {
257
+ toggleDragMenu && toggleDragMenu('mouse', e);
258
+ }
220
259
  },
221
260
  onClick: onClick,
222
261
  onKeyDown: e => {
223
262
  if (e.key === 'Enter' || e.key === ' ') {
224
- toggleDragMenu && toggleDragMenu('keyboard');
263
+ if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
264
+ toggleDragMenu && toggleDragMenu('keyboard', undefined, indexes[0]);
265
+ } else {
266
+ toggleDragMenu && toggleDragMenu('keyboard');
267
+ }
225
268
  }
226
269
  }
227
270
  }, appearance !== 'placeholder' ?
@@ -30,7 +30,9 @@ function getRowOptions(index) {
30
30
  }
31
31
  };
32
32
  }
33
- function getColumnOptions(index, tableContainer, hasNumberedColumns) {
33
+ function getColumnOptions(index, tableContainer, hasNumberedColumns,
34
+ // Distance between the target cell top and table top, used when anchoring to a lower-row cell.
35
+ verticalOffsetCorrection = 0) {
34
36
  const options = {
35
37
  alignX: 'end',
36
38
  alignY: 'top',
@@ -41,16 +43,32 @@ function getColumnOptions(index, tableContainer, hasNumberedColumns) {
41
43
  // we should always set the InsertButton on the start,
42
44
  // considering the offset from the first column
43
45
  onPositionCalculated(position) {
46
+ // Move the popup upward whether the offset parent provides `top` or `bottom`.
47
+ let verticalCorrection;
48
+ if (verticalOffsetCorrection) {
49
+ if (position.top !== undefined) {
50
+ verticalCorrection = {
51
+ top: position.top - verticalOffsetCorrection
52
+ };
53
+ } else if (position.bottom !== undefined) {
54
+ verticalCorrection = {
55
+ bottom: position.bottom + verticalOffsetCorrection
56
+ };
57
+ }
58
+ }
44
59
  const {
45
60
  left
46
61
  } = position;
47
62
  if (!left) {
48
- // If not left, lest skip expensive next calculations.
49
- return position;
63
+ return {
64
+ ...position,
65
+ ...verticalCorrection
66
+ };
50
67
  }
51
68
  if (index === 0) {
52
69
  return {
53
70
  ...position,
71
+ ...verticalCorrection,
54
72
  left: hasNumberedColumns ? HORIZONTAL_ALIGN_NUMBERED_COLUMN_BUTTON : HORIZONTAL_ALIGN_COLUMN_BUTTON
55
73
  };
56
74
  }
@@ -59,6 +77,7 @@ function getColumnOptions(index, tableContainer, hasNumberedColumns) {
59
77
  const rect = tableContainer ? tableContainer.getBoundingClientRect() : null;
60
78
  return {
61
79
  ...position,
80
+ ...verticalCorrection,
62
81
  left: rect && left > rect.width ? rect.width : left
63
82
  };
64
83
  }
@@ -75,10 +94,10 @@ function getColumnOptions(index, tableContainer, hasNumberedColumns) {
75
94
  }
76
95
  return options;
77
96
  }
78
- function getPopupOptions(direction, index, hasNumberedColumns, tableContainer) {
97
+ function getPopupOptions(direction, index, hasNumberedColumns, tableContainer, verticalOffsetCorrection = 0) {
79
98
  switch (direction) {
80
99
  case 'column':
81
- return getColumnOptions(index, tableContainer, hasNumberedColumns);
100
+ return getColumnOptions(index, tableContainer, hasNumberedColumns, verticalOffsetCorrection);
82
101
  case 'row':
83
102
  return getRowOptions(index);
84
103
  default: