@atlaskit/editor-plugin-table 24.2.1 → 24.2.3

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 (48) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cjs/pm-plugins/utils/column-controls.js +119 -2
  3. package/dist/cjs/pm-plugins/utils/dom.js +20 -1
  4. package/dist/cjs/pm-plugins/utils/row-controls.js +43 -1
  5. package/dist/cjs/pm-plugins/utils/selection.js +61 -1
  6. package/dist/cjs/ui/DragHandle/index.js +10 -2
  7. package/dist/cjs/ui/FloatingInsertButton/getPopupOptions.js +19 -5
  8. package/dist/cjs/ui/FloatingInsertButton/index.js +37 -3
  9. package/dist/cjs/ui/TableFloatingColumnControls/ColumnControls/index.js +25 -15
  10. package/dist/cjs/ui/TableFloatingColumnControls/index.js +1 -1
  11. package/dist/cjs/ui/TableFloatingControls/RowControls/DragControls.js +28 -12
  12. package/dist/cjs/ui/common-styles.js +6 -1
  13. package/dist/cjs/ui/event-handlers.js +47 -7
  14. package/dist/cjs/ui/rounded-table-styles.js +1 -1
  15. package/dist/es2019/pm-plugins/utils/column-controls.js +114 -2
  16. package/dist/es2019/pm-plugins/utils/dom.js +19 -0
  17. package/dist/es2019/pm-plugins/utils/row-controls.js +42 -0
  18. package/dist/es2019/pm-plugins/utils/selection.js +60 -0
  19. package/dist/es2019/ui/DragHandle/index.js +10 -2
  20. package/dist/es2019/ui/FloatingInsertButton/getPopupOptions.js +24 -5
  21. package/dist/es2019/ui/FloatingInsertButton/index.js +35 -3
  22. package/dist/es2019/ui/TableFloatingColumnControls/ColumnControls/index.js +26 -16
  23. package/dist/es2019/ui/TableFloatingColumnControls/index.js +2 -2
  24. package/dist/es2019/ui/TableFloatingControls/RowControls/DragControls.js +29 -12
  25. package/dist/es2019/ui/common-styles.js +28 -2
  26. package/dist/es2019/ui/event-handlers.js +47 -7
  27. package/dist/es2019/ui/rounded-table-styles.js +1 -1
  28. package/dist/esm/pm-plugins/utils/column-controls.js +118 -1
  29. package/dist/esm/pm-plugins/utils/dom.js +19 -0
  30. package/dist/esm/pm-plugins/utils/row-controls.js +42 -0
  31. package/dist/esm/pm-plugins/utils/selection.js +60 -0
  32. package/dist/esm/ui/DragHandle/index.js +10 -2
  33. package/dist/esm/ui/FloatingInsertButton/getPopupOptions.js +19 -5
  34. package/dist/esm/ui/FloatingInsertButton/index.js +37 -3
  35. package/dist/esm/ui/TableFloatingColumnControls/ColumnControls/index.js +27 -17
  36. package/dist/esm/ui/TableFloatingColumnControls/index.js +2 -2
  37. package/dist/esm/ui/TableFloatingControls/RowControls/DragControls.js +30 -14
  38. package/dist/esm/ui/common-styles.js +7 -2
  39. package/dist/esm/ui/event-handlers.js +49 -9
  40. package/dist/esm/ui/rounded-table-styles.js +1 -1
  41. package/dist/types/pm-plugins/utils/column-controls.d.ts +35 -1
  42. package/dist/types/pm-plugins/utils/dom.d.ts +10 -0
  43. package/dist/types/pm-plugins/utils/row-controls.d.ts +16 -0
  44. package/dist/types/pm-plugins/utils/selection.d.ts +13 -0
  45. package/dist/types/ui/DragHandle/index.d.ts +1 -1
  46. package/dist/types/ui/FloatingInsertButton/getPopupOptions.d.ts +1 -1
  47. package/dist/types/ui/FloatingInsertButton/index.d.ts +1 -0
  48. package/package.json +3 -3
@@ -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);
@@ -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,5 @@
1
+ import { TableMap } from '@atlaskit/editor-tables/table-map';
2
+ import { findTableClosestToPos, getSelectionRect } from '@atlaskit/editor-tables/utils';
1
3
  export const getSelectedColumnIndexes = selectionRect => {
2
4
  const columnIndexes = [];
3
5
  for (let i = selectionRect.left; i < selectionRect.right; i++) {
@@ -11,4 +13,62 @@ export const getSelectedRowIndexes = selectionRect => {
11
13
  rowIndexes.push(i);
12
14
  }
13
15
  return rowIndexes;
16
+ };
17
+
18
+ /**
19
+ * Treats a column as fully selected when its first-row area is covered by a horizontal merge and
20
+ * the explicit `CellSelection` starts below that merged cell. `CellSelection.isColSelection()`
21
+ * returns `false` in this case because the selection does not include the merged first-row cell.
22
+ */
23
+ export const isColumnSelectionWithMergedFirstRow = selection => {
24
+ const rect = getSelectionRect(selection);
25
+ if (!rect) {
26
+ return false;
27
+ }
28
+ const table = findTableClosestToPos(selection.$anchorCell);
29
+ if (!table) {
30
+ return false;
31
+ }
32
+ const map = TableMap.get(table.node);
33
+ if (rect.bottom !== map.height || rect.top === 0) {
34
+ return false;
35
+ }
36
+ for (let col = rect.left; col < rect.right; col++) {
37
+ const topCellRect = map.findCell(map.map[col]);
38
+ const isHorizontallyMerged = topCellRect.right - topCellRect.left > 1;
39
+ const bridgesGapAboveSelection = topCellRect.bottom >= rect.top;
40
+ if (!isHorizontallyMerged || !bridgesGapAboveSelection) {
41
+ return false;
42
+ }
43
+ }
44
+ return true;
45
+ };
46
+
47
+ /**
48
+ * Treats a row as fully selected when its first-column area is covered by a vertical merge and the
49
+ * explicit `CellSelection` starts to the right of that merged cell. `CellSelection.isRowSelection()`
50
+ * returns `false` in this case because the selection does not include the merged first-column cell.
51
+ */
52
+ export const isRowSelectionWithMergedFirstColumn = selection => {
53
+ const rect = getSelectionRect(selection);
54
+ if (!rect) {
55
+ return false;
56
+ }
57
+ const table = findTableClosestToPos(selection.$anchorCell);
58
+ if (!table) {
59
+ return false;
60
+ }
61
+ const map = TableMap.get(table.node);
62
+ if (rect.right !== map.width || rect.left === 0) {
63
+ return false;
64
+ }
65
+ for (let row = rect.top; row < rect.bottom; row++) {
66
+ const leftCellRect = map.findCell(map.map[row * map.width]);
67
+ const isVerticallyMerged = leftCellRect.bottom - leftCellRect.top > 1;
68
+ const bridgesGapLeftOfSelection = leftCellRect.right >= rect.left;
69
+ if (!isVerticallyMerged || !bridgesGapLeftOfSelection) {
70
+ return false;
71
+ }
72
+ }
73
+ return true;
14
74
  };
@@ -216,12 +216,20 @@ const DragHandleComponent = ({
216
216
  // 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
217
  // -> this is bad for a11y but is the current standard new copy/paste keyboard shortcuts should be introduced instead
218
218
  editorView.focus();
219
- toggleDragMenu && toggleDragMenu('mouse', e);
219
+ if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
220
+ toggleDragMenu && toggleDragMenu('mouse', e, indexes[0]);
221
+ } else {
222
+ toggleDragMenu && toggleDragMenu('mouse', e);
223
+ }
220
224
  },
221
225
  onClick: onClick,
222
226
  onKeyDown: e => {
223
227
  if (e.key === 'Enter' || e.key === ' ') {
224
- toggleDragMenu && toggleDragMenu('keyboard');
228
+ if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
229
+ toggleDragMenu && toggleDragMenu('keyboard', undefined, indexes[0]);
230
+ } else {
231
+ toggleDragMenu && toggleDragMenu('keyboard');
232
+ }
225
233
  }
226
234
  }
227
235
  }, 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:
@@ -13,6 +13,7 @@ import { findTable } from '@atlaskit/editor-tables/utils';
13
13
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
14
14
  import { insertColumnWithAnalytics, insertRowWithAnalytics } from '../../pm-plugins/commands/commands-with-analytics';
15
15
  import { checkIfNumberColumnEnabled } from '../../pm-plugins/utils/nodes';
16
+ import { isColumnSelectionWithMergedFirstRow, isRowSelectionWithMergedFirstColumn } from '../../pm-plugins/utils/selection';
16
17
  import { TableCssClassName as ClassName } from '../../types';
17
18
  import getPopupOptions from './getPopupOptions';
18
19
  import { DragAndDropInsertButton } from './InsertButton';
@@ -65,8 +66,14 @@ export class FloatingInsertButton extends React.Component {
65
66
  tr
66
67
  }
67
68
  } = editorView;
68
- if (tr.selection instanceof CellSelection && (tr.selection.isColSelection() || tr.selection.isRowSelection())) {
69
- return null;
69
+ if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
70
+ if (tr.selection instanceof CellSelection && (tr.selection.isColSelection() || tr.selection.isRowSelection() || isColumnSelectionWithMergedFirstRow(tr.selection) || isRowSelectionWithMergedFirstColumn(tr.selection))) {
71
+ return null;
72
+ }
73
+ } else {
74
+ if (tr.selection instanceof CellSelection && (tr.selection.isColSelection() || tr.selection.isRowSelection())) {
75
+ return null;
76
+ }
70
77
  }
71
78
  const tablePos = findTable(tr.selection);
72
79
  if (!tablePos) {
@@ -120,6 +127,15 @@ export class FloatingInsertButton extends React.Component {
120
127
  const index = type === 'column' ? insertColumnButtonIndex : insertRowButtonIndex;
121
128
  const hasNumberedColumns = checkIfNumberColumnEnabled(editorView.state.selection);
122
129
 
130
+ // If row 0 has a colspan, anchor to a lower row for the real column boundary (X),
131
+ // then move back up to the table top (Y):
132
+ // row 0: [ colspan=2 ]
133
+ // row 1: [ col 1 ][ col 2 ] ← anchor here for X
134
+ // ↑ button should render at row 0/table top
135
+ let verticalOffsetCorrection = 0;
136
+ if (type === 'column' && expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
137
+ verticalOffsetCorrection = Math.max(0, targetCellRef.getBoundingClientRect().top - tableRef.getBoundingClientRect().top);
138
+ }
123
139
  // Fixed the 'add column button' not visible issue when sticky header is enabled
124
140
  // By setting the Popup z-index higher than the sticky header z-index ( common-styles.ts tr.sticky)
125
141
  // Only when inserting a column, otherwise set to undefined
@@ -137,7 +153,7 @@ export class FloatingInsertButton extends React.Component {
137
153
  allowOutOfBounds: true
138
154
  // Ignored via go/ees005
139
155
  // eslint-disable-next-line react/jsx-props-no-spreading
140
- }, getPopupOptions(type, index, hasNumberedColumns, tableContainerWrapper), {
156
+ }, getPopupOptions(type, index, hasNumberedColumns, tableContainerWrapper, verticalOffsetCorrection), {
141
157
  zIndex: zIndex
142
158
  }), /*#__PURE__*/React.createElement(DragAndDropInsertButton, {
143
159
  type: type,
@@ -147,6 +163,18 @@ export class FloatingInsertButton extends React.Component {
147
163
  isChromelessEditor: isChromelessEditor
148
164
  }));
149
165
  }
166
+
167
+ // Finds a row where `columnIndex` has real left/right cell boundaries.
168
+ findRowWithUnmergedColumn(tableMap, columnIndex) {
169
+ for (let row = 0; row < tableMap.height; row++) {
170
+ const pos = tableMap.map[row * tableMap.width + columnIndex];
171
+ const rect = tableMap.findCell(pos);
172
+ if (rect.left === columnIndex && rect.right === columnIndex + 1) {
173
+ return row;
174
+ }
175
+ }
176
+ return null;
177
+ }
150
178
  getCellPosition(type, tableNode) {
151
179
  const {
152
180
  insertColumnButtonIndex,
@@ -163,6 +191,10 @@ export class FloatingInsertButton extends React.Component {
163
191
  if (columnIndex > tableMap.width - 1) {
164
192
  return null;
165
193
  }
194
+ if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
195
+ const rowWithRealColumn = this.findRowWithUnmergedColumn(tableMap, columnIndex);
196
+ return rowWithRealColumn === null ? null : tableMap.positionAt(rowWithRealColumn, columnIndex, tableNode);
197
+ }
166
198
  return tableMap.positionAt(0, columnIndex, tableNode);
167
199
  } else {
168
200
  // This condition is to make typescript happy.
@@ -8,15 +8,29 @@ import { akEditorTableNumberColumnWidth } from '@atlaskit/editor-shared-styles';
8
8
  import { CellSelection } from '@atlaskit/editor-tables';
9
9
  import { getSelectionRect } from '@atlaskit/editor-tables/utils';
10
10
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
11
- import { clearHoverSelection, closeActiveTableMenu, hoverCell, hoverColumns, selectColumn, selectColumns, toggleActiveTableMenu } from '../../../pm-plugins/commands';
11
+ import { clearHoverSelection, hoverCell, hoverColumns, selectColumn, selectColumns, toggleActiveTableMenu } from '../../../pm-plugins/commands';
12
12
  import { toggleDragMenuWithAnalytics } from '../../../pm-plugins/drag-and-drop/commands-with-analytics';
13
13
  import { getPluginState as getTablePluginState } from '../../../pm-plugins/plugin-factory';
14
14
  import { getRowsParams } from '../../../pm-plugins/utils/row-controls';
15
- import { getSelectedColumnIndexes } from '../../../pm-plugins/utils/selection';
15
+ import { getSelectedColumnIndexes, isColumnSelectionWithMergedFirstRow } from '../../../pm-plugins/utils/selection';
16
16
  import { TableCssClassName as ClassName } from '../../../types';
17
17
  import { DragHandle } from '../../DragHandle';
18
18
  const getSelectedColumns = selection => {
19
- if (selection instanceof CellSelection && selection.isColSelection()) {
19
+ if (!(selection instanceof CellSelection)) {
20
+ return [];
21
+ }
22
+ if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
23
+ // New behaviour: also treat a column selection that sits below a merged first-row cell as a
24
+ // full column selection.
25
+ if (!selection.isColSelection() && !isColumnSelectionWithMergedFirstRow(selection)) {
26
+ return [];
27
+ }
28
+ const rect = getSelectionRect(selection);
29
+ return rect ? getSelectedColumnIndexes(rect) : [];
30
+ }
31
+
32
+ // Old behaviour: only standard column selections are recognised.
33
+ if (selection.isColSelection()) {
20
34
  const rect = getSelectionRect(selection);
21
35
  if (!rect) {
22
36
  return [];
@@ -125,27 +139,23 @@ export const ColumnControls = ({
125
139
  clearHoverSelection()(state, dispatch);
126
140
  }
127
141
  }, [editorView, tableActive]);
128
- const toggleDragMenuHandler = useCallback((trigger, event) => {
142
+ const toggleDragMenuHandler = useCallback((trigger, event, handleIndex) => {
129
143
  var _api$analytics2;
144
+ if (event !== null && event !== void 0 && event.shiftKey) {
145
+ return;
146
+ }
130
147
  const {
131
148
  state,
132
149
  dispatch
133
150
  } = editorView;
134
- if (event !== null && event !== void 0 && event.shiftKey) {
135
- // Shift-click extends the selection rather than toggling the menu, but the
136
- // open drag menu would otherwise stay anchored to a stale column. Close it here
137
- // for the updated menu (legacy menu closes via outside-click on its dropdown).
138
- if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
139
- api === null || api === void 0 ? void 0 : api.core.actions.execute(closeActiveTableMenu(api));
140
- }
141
- return;
142
- }
143
151
  if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
144
- if (colIndex !== undefined && api) {
152
+ // Use the clicked handle index because `hoveredCell` can point at a merged cell's first column.
153
+ const targetColIndex = handleIndex !== null && handleIndex !== void 0 ? handleIndex : colIndex;
154
+ if (targetColIndex !== undefined && api) {
145
155
  const {
146
156
  activeTableMenu: currentActiveTableMenu
147
157
  } = getTablePluginState(state);
148
- const isSameActiveMenu = (currentActiveTableMenu === null || currentActiveTableMenu === void 0 ? void 0 : currentActiveTableMenu.type) === 'column' && currentActiveTableMenu.index === colIndex;
158
+ const isSameActiveMenu = (currentActiveTableMenu === null || currentActiveTableMenu === void 0 ? void 0 : currentActiveTableMenu.type) === 'column' && currentActiveTableMenu.index === targetColIndex;
149
159
  api.core.actions.execute(({
150
160
  tr
151
161
  }) => {
@@ -164,7 +174,7 @@ export const ColumnControls = ({
164
174
  }
165
175
  toggleActiveTableMenu({
166
176
  type: 'column',
167
- index: colIndex,
177
+ index: targetColIndex,
168
178
  openedBy: trigger
169
179
  }, currentActiveTableMenu, api)({
170
180
  tr
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
3
3
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
4
- import { getColumnsWidths } from '../../pm-plugins/utils/column-controls';
4
+ import { getColumnsWidths, getColumnsWidthsWithMergedCells } from '../../pm-plugins/utils/column-controls';
5
5
  import { containsHeaderColumn } from '../../pm-plugins/utils/nodes';
6
6
  import { getRowHeights } from '../../pm-plugins/utils/row-controls';
7
7
  import { isNativeStickySupported } from '../../pm-plugins/utils/sticky-header';
@@ -63,7 +63,7 @@ const TableFloatingColumnControls = ({
63
63
  if (!tableRef || !tableActive || isResizing) {
64
64
  return null;
65
65
  }
66
- const colWidths = getColumnsWidths(editorView);
66
+ const colWidths = expValEquals('platform_editor_table_menu_updates', 'isEnabled', true) ? getColumnsWidthsWithMergedCells(editorView) : getColumnsWidths(editorView);
67
67
  if (stickyTop) {
68
68
  var _containerRef$current;
69
69
  const columnControlTopOffsetFromParent = '-12px';
@@ -7,17 +7,31 @@ import { CellSelection } from '@atlaskit/editor-tables';
7
7
  import { getSelectionRect } from '@atlaskit/editor-tables/utils';
8
8
  import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
9
9
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
10
- import { clearHoverSelection, closeActiveTableMenu, toggleActiveTableMenu } from '../../../pm-plugins/commands';
10
+ import { clearHoverSelection, toggleActiveTableMenu } from '../../../pm-plugins/commands';
11
11
  import { toggleDragMenuWithAnalytics } from '../../../pm-plugins/drag-and-drop/commands-with-analytics';
12
12
  import { getPluginState as getTablePluginState } from '../../../pm-plugins/plugin-factory';
13
13
  import { getRowHeights, getRowsParams } from '../../../pm-plugins/utils/row-controls';
14
- import { getSelectedRowIndexes } from '../../../pm-plugins/utils/selection';
14
+ import { getSelectedRowIndexes, isRowSelectionWithMergedFirstColumn } from '../../../pm-plugins/utils/selection';
15
15
  import { TableCssClassName as ClassName } from '../../../types';
16
16
  import { dragRowControlsWidth, dropTargetExtendedWidth } from '../../consts';
17
17
  import { DragHandle } from '../../DragHandle';
18
18
  import RowDropTarget from '../RowDropTarget';
19
19
  const getSelectedRows = selection => {
20
- if (selection instanceof CellSelection && selection.isRowSelection()) {
20
+ if (!(selection instanceof CellSelection)) {
21
+ return [];
22
+ }
23
+ if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
24
+ // New behaviour: also treat a row selection that sits to the right of a merged first-column
25
+ // cell as a full row selection.
26
+ if (!selection.isRowSelection() && !isRowSelectionWithMergedFirstColumn(selection)) {
27
+ return [];
28
+ }
29
+ const rect = getSelectionRect(selection);
30
+ return rect ? getSelectedRowIndexes(rect) : [];
31
+ }
32
+
33
+ // Old behaviour: only standard row selections are recognised.
34
+ if (selection.isRowSelection()) {
21
35
  const rect = getSelectionRect(selection);
22
36
  if (!rect) {
23
37
  return [];
@@ -78,18 +92,14 @@ export const DragControls = ({
78
92
  }
79
93
  });
80
94
  }, [editorView]);
81
- const toggleDragMenuHandler = useCallback((trigger, event) => {
95
+ const toggleDragMenuHandler = useCallback((trigger, event, handleIndex) => {
82
96
  var _api$analytics2;
83
97
  if (event !== null && event !== void 0 && event.shiftKey) {
84
- // Shift-click extends the selection rather than toggling the menu, but the
85
- // open drag menu would otherwise stay anchored to a stale row. Close it here
86
- // for the updated menu (legacy menu closes via outside-click on its dropdown).
87
- if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
88
- api === null || api === void 0 ? void 0 : api.core.actions.execute(closeActiveTableMenu(api));
89
- }
90
98
  return;
91
99
  }
92
- const rowIndex = hoveredCell === null || hoveredCell === void 0 ? void 0 : hoveredCell.rowIndex;
100
+
101
+ // Use the clicked handle index because `hoveredCell` can point at a merged cell's first row.
102
+ const rowIndex = handleIndex !== null && handleIndex !== void 0 ? handleIndex : hoveredCell === null || hoveredCell === void 0 ? void 0 : hoveredCell.rowIndex;
93
103
  if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) {
94
104
  if (rowIndex !== undefined && api) {
95
105
  const {
@@ -230,10 +240,17 @@ export const DragControls = ({
230
240
  if (!tableActive) {
231
241
  return null;
232
242
  }
243
+ const selectedAppearance = isRowSelected && isEntireTableSelected ? isInDanger ? 'danger' : 'selected' : 'placeholder';
233
244
 
234
245
  // placeholder / selected need to always render at least one handle
235
246
  // so it can be focused via keyboard shortcuts
236
- handles.push(generateHandleByType('selected', isRowSelected && isEntireTableSelected ? isInDanger ? 'danger' : 'selected' : 'placeholder', `${selectedRowIndexes[0] + 1} / span ${selectedRowIndexes.length}`, selectedRowIndexes));
247
+ const selectedGridRow = expValEquals('platform_editor_table_menu_updates', 'isEnabled', true) ?
248
+ // New behaviour: always position the placeholder in the first row to avoid an invalid
249
+ // `NaN / span 0` grid placement (which makes the handle disappear) when no rows are selected.
250
+ selectedAppearance === 'placeholder' ? '1 / span 1' : `${selectedRowIndexes[0] + 1} / span ${selectedRowIndexes.length}` :
251
+ // Old behaviour.
252
+ `${selectedRowIndexes[0] + 1} / span ${selectedRowIndexes.length}`;
253
+ handles.push(generateHandleByType('selected', selectedAppearance, selectedGridRow, selectedRowIndexes));
237
254
  if (hoveredCell && isTableHovered && rowIndex !== undefined && !selectedRowIndexes.includes(rowIndex) && rowIndex < rowHeights.length) {
238
255
  handles.push(generateHandleByType('hover', 'default', `${rowIndex + 1} / span 1`, rowIndexes));
239
256
  }