@jackuait/blok 0.10.1 → 0.10.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.
- package/dist/blok.mjs +2 -2
- package/dist/chunks/{blok-u_68bnlk.mjs → blok-3wc3aInM.mjs} +483 -466
- package/dist/chunks/{constants-VDhCUk4c.mjs → constants-Bp622jic.mjs} +109 -103
- package/dist/chunks/{tools-1ZFajlGN.mjs → tools-BC1jRfoS.mjs} +697 -646
- package/dist/full.mjs +3 -3
- package/dist/react.mjs +2 -2
- package/dist/tools.mjs +2 -2
- package/package.json +1 -1
- package/src/components/modules/toolbar/index.ts +114 -10
- package/src/components/modules/toolbar/plus-button.ts +37 -0
- package/src/components/modules/toolbar/settings-toggler.ts +6 -0
- package/src/components/modules/uiControllers/controllers/keyboard.ts +56 -22
- package/src/components/selection/cursor.ts +12 -2
- package/src/components/ui/toolbox.ts +31 -4
- package/src/components/utils/popover/popover-position.ts +8 -3
- package/src/tools/code/index.ts +4 -0
- package/src/tools/table/index.ts +10 -19
- package/src/tools/table/table-cell-selection.ts +126 -7
- package/src/tools/table/table-core.ts +59 -5
- package/src/tools/table/table-model.ts +8 -0
- package/src/tools/table/table-row-col-controls.ts +40 -18
|
@@ -86,6 +86,8 @@ interface CellSelectionOptions {
|
|
|
86
86
|
isMergedCell?: (row: number, col: number) => boolean;
|
|
87
87
|
/** Called when user requests to split a merged cell. */
|
|
88
88
|
onSplitCell?: (row: number, col: number) => void;
|
|
89
|
+
/** Returns the colspan and rowspan of the cell at (row, col). Used to expand the selection rect to full merged-cell spans. */
|
|
90
|
+
getCellSpan?: (row: number, col: number) => { colspan: number; rowspan: number };
|
|
89
91
|
i18n: I18n;
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -118,7 +120,9 @@ export class TableCellSelection {
|
|
|
118
120
|
private onMergeCells: ((range: SelectionRange) => void) | undefined;
|
|
119
121
|
private isMergedCell: ((row: number, col: number) => boolean) | undefined;
|
|
120
122
|
private onSplitCell: ((row: number, col: number) => void) | undefined;
|
|
123
|
+
private getCellSpan: ((row: number, col: number) => { colspan: number; rowspan: number }) | undefined;
|
|
121
124
|
private lastPaintedRange: SelectionRange | null = null;
|
|
125
|
+
private preExpansionWasSingleCell = false;
|
|
122
126
|
|
|
123
127
|
private boundPointerDown: (e: PointerEvent) => void;
|
|
124
128
|
private boundPointerMove: (e: PointerEvent) => void;
|
|
@@ -148,6 +152,7 @@ export class TableCellSelection {
|
|
|
148
152
|
this.onMergeCells = options.onMergeCells;
|
|
149
153
|
this.isMergedCell = options.isMergedCell;
|
|
150
154
|
this.onSplitCell = options.onSplitCell;
|
|
155
|
+
this.getCellSpan = options.getCellSpan;
|
|
151
156
|
this.i18n = options.i18n;
|
|
152
157
|
this.grid.style.position = 'relative';
|
|
153
158
|
|
|
@@ -466,6 +471,14 @@ export class TableCellSelection {
|
|
|
466
471
|
return;
|
|
467
472
|
}
|
|
468
473
|
|
|
474
|
+
// For single-cell selections, if the user has a non-collapsed native text
|
|
475
|
+
// selection within the cell's contenteditable (i.e., they selected specific
|
|
476
|
+
// text characters), defer to the browser's native copy so their text
|
|
477
|
+
// selection is copied rather than the whole cell block structure.
|
|
478
|
+
if (this.selectedCells.length <= 1 && this.hasNativeTextSelection()) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
469
482
|
e.preventDefault();
|
|
470
483
|
this.onCopy?.([...this.selectedCells], e.clipboardData);
|
|
471
484
|
}
|
|
@@ -475,18 +488,37 @@ export class TableCellSelection {
|
|
|
475
488
|
return;
|
|
476
489
|
}
|
|
477
490
|
|
|
491
|
+
// For single-cell selections, if the user has a non-collapsed native text
|
|
492
|
+
// selection within the cell's contenteditable, defer to the browser's native
|
|
493
|
+
// cut so their text selection is cut rather than clearing the entire cell.
|
|
494
|
+
if (this.selectedCells.length <= 1 && this.hasNativeTextSelection()) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
478
498
|
e.preventDefault();
|
|
479
499
|
this.onCut?.([...this.selectedCells], e.clipboardData);
|
|
480
500
|
this.onClearContent?.([...this.selectedCells]);
|
|
481
501
|
this.clearSelection();
|
|
482
502
|
}
|
|
483
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Returns true if the browser has a non-collapsed text selection (i.e. the
|
|
506
|
+
* user has selected one or more characters inside a contenteditable), as
|
|
507
|
+
* opposed to a mere caret position or no selection at all.
|
|
508
|
+
*/
|
|
509
|
+
private hasNativeTextSelection(): boolean {
|
|
510
|
+
const selection = window.getSelection();
|
|
511
|
+
|
|
512
|
+
return selection !== null && !selection.isCollapsed;
|
|
513
|
+
}
|
|
514
|
+
|
|
484
515
|
private clearSelection(): void {
|
|
485
516
|
const hadSelection = this.hasSelection;
|
|
486
517
|
|
|
487
518
|
this.restoreModifiedCells();
|
|
488
519
|
this.hasSelection = false;
|
|
489
520
|
this.lastPaintedRange = null;
|
|
521
|
+
this.preExpansionWasSingleCell = false;
|
|
490
522
|
|
|
491
523
|
if (hadSelection) {
|
|
492
524
|
this.onSelectionActiveChange?.(false);
|
|
@@ -540,6 +572,50 @@ export class TableCellSelection {
|
|
|
540
572
|
document.addEventListener('pointerdown', this.boundClearSelection);
|
|
541
573
|
}
|
|
542
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Expand a selection rect to fully include the spans of any merged cells
|
|
577
|
+
* whose origins fall within the rect. Iterates until the rect is stable,
|
|
578
|
+
* since pulling in a new merged cell may expose further cells that extend
|
|
579
|
+
* beyond the current boundary.
|
|
580
|
+
*/
|
|
581
|
+
private expandRectToMergedSpans(rect: SelectionRange): SelectionRange {
|
|
582
|
+
if (!this.getCellSpan) {
|
|
583
|
+
return rect;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return this.expandRectStep(rect);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private expandRectStep(rect: SelectionRange): SelectionRange {
|
|
590
|
+
const getCellSpan = this.getCellSpan;
|
|
591
|
+
|
|
592
|
+
if (!getCellSpan) {
|
|
593
|
+
return rect;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const rows = Array.from({ length: rect.maxRow - rect.minRow + 1 }, (_, i) => rect.minRow + i);
|
|
597
|
+
const cols = Array.from({ length: rect.maxCol - rect.minCol + 1 }, (_, i) => rect.minCol + i);
|
|
598
|
+
|
|
599
|
+
const expanded = rows.reduce<SelectionRange>((acc, r) => {
|
|
600
|
+
return cols.reduce<SelectionRange>((inner, c) => {
|
|
601
|
+
const { colspan, rowspan } = getCellSpan(r, c);
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
minRow: inner.minRow,
|
|
605
|
+
maxRow: Math.max(inner.maxRow, r + rowspan - 1),
|
|
606
|
+
minCol: inner.minCol,
|
|
607
|
+
maxCol: Math.max(inner.maxCol, c + colspan - 1),
|
|
608
|
+
};
|
|
609
|
+
}, acc);
|
|
610
|
+
}, rect);
|
|
611
|
+
|
|
612
|
+
const changed =
|
|
613
|
+
expanded.maxRow !== rect.maxRow ||
|
|
614
|
+
expanded.maxCol !== rect.maxCol;
|
|
615
|
+
|
|
616
|
+
return changed ? this.expandRectStep(expanded) : expanded;
|
|
617
|
+
}
|
|
618
|
+
|
|
543
619
|
private paintSelection(): void {
|
|
544
620
|
if (!this.anchorCell || !this.extentCell) {
|
|
545
621
|
return;
|
|
@@ -557,12 +633,15 @@ export class TableCellSelection {
|
|
|
557
633
|
const minCol = Math.min(this.anchorCell.col, this.extentCell.col);
|
|
558
634
|
const maxCol = Math.max(this.anchorCell.col, this.extentCell.col);
|
|
559
635
|
|
|
560
|
-
this.
|
|
636
|
+
this.preExpansionWasSingleCell = minRow === maxRow && minCol === maxCol;
|
|
637
|
+
this.lastPaintedRange = this.expandRectToMergedSpans({ minRow, maxRow, minCol, maxCol });
|
|
638
|
+
|
|
639
|
+
const { minRow: expandedMinRow, maxRow: expandedMaxRow, minCol: expandedMinCol, maxCol: expandedMaxCol } = this.lastPaintedRange;
|
|
561
640
|
|
|
562
641
|
const rows = this.grid.querySelectorAll(`[${ROW_ATTR}]`);
|
|
563
642
|
|
|
564
643
|
// Mark selected cells
|
|
565
|
-
this.selectedCells = this.collectCellsInRange(rows,
|
|
644
|
+
this.selectedCells = this.collectCellsInRange(rows, expandedMinRow, expandedMaxRow, expandedMinCol, expandedMaxCol);
|
|
566
645
|
this.selectedCells.forEach(cell => {
|
|
567
646
|
cell.setAttribute(SELECTED_ATTR, '');
|
|
568
647
|
});
|
|
@@ -570,8 +649,8 @@ export class TableCellSelection {
|
|
|
570
649
|
// Calculate overlay position from bounding rects of corner cells.
|
|
571
650
|
// Try coordinate-based lookup first (works with merged cells),
|
|
572
651
|
// then fall back to index-based lookup for backwards compatibility.
|
|
573
|
-
const firstCell = this.findCellByCoordOrIndex(rows,
|
|
574
|
-
const lastCell = this.findCellByCoordOrIndex(rows,
|
|
652
|
+
const firstCell = this.findCellByCoordOrIndex(rows, expandedMinRow, expandedMinCol);
|
|
653
|
+
const lastCell = this.findCellByCoordOrIndex(rows, expandedMaxRow, expandedMaxCol);
|
|
575
654
|
|
|
576
655
|
if (!firstCell || !lastCell) {
|
|
577
656
|
return;
|
|
@@ -803,7 +882,7 @@ export class TableCellSelection {
|
|
|
803
882
|
if (this.lastPaintedRange && this.onMergeCells) {
|
|
804
883
|
const range = this.lastPaintedRange;
|
|
805
884
|
const isMultiCell = range.minRow !== range.maxRow || range.minCol !== range.maxCol;
|
|
806
|
-
const canMerge = isMultiCell && this.canMergeCells?.(range);
|
|
885
|
+
const canMerge = isMultiCell && !this.preExpansionWasSingleCell && this.canMergeCells?.(range);
|
|
807
886
|
|
|
808
887
|
if (canMerge) {
|
|
809
888
|
mergeItems.push({
|
|
@@ -821,8 +900,9 @@ export class TableCellSelection {
|
|
|
821
900
|
if (this.lastPaintedRange && this.onSplitCell) {
|
|
822
901
|
const range = this.lastPaintedRange;
|
|
823
902
|
const isSingleCell = range.minRow === range.maxRow && range.minCol === range.maxCol;
|
|
903
|
+
const isSingleOriginExpanded = this.preExpansionWasSingleCell && this.isMergedCell?.(range.minRow, range.minCol);
|
|
824
904
|
|
|
825
|
-
if (isSingleCell && this.isMergedCell?.(range.minRow, range.minCol)) {
|
|
905
|
+
if ((isSingleCell || isSingleOriginExpanded) && this.isMergedCell?.(range.minRow, range.minCol)) {
|
|
826
906
|
mergeItems.push({
|
|
827
907
|
icon: IconSplitCell,
|
|
828
908
|
title: this.i18n.t('tools.table.splitCell'),
|
|
@@ -936,6 +1016,23 @@ export class TableCellSelection {
|
|
|
936
1016
|
return null;
|
|
937
1017
|
}
|
|
938
1018
|
|
|
1019
|
+
// Prefer logical coordinate attributes stamped by reindexCoordinates() —
|
|
1020
|
+
// these are correct even when rows have fewer physical <td> elements than
|
|
1021
|
+
// logical columns (e.g. after a colspan/rowspan merge).
|
|
1022
|
+
const cellRowAttr = cell.getAttribute(CELL_ROW_ATTR);
|
|
1023
|
+
const cellColAttr = cell.getAttribute(CELL_COL_ATTR);
|
|
1024
|
+
|
|
1025
|
+
if (cellRowAttr !== null && cellColAttr !== null) {
|
|
1026
|
+
const rowIndex = parseInt(cellRowAttr, 10);
|
|
1027
|
+
const colIndex = parseInt(cellColAttr, 10);
|
|
1028
|
+
|
|
1029
|
+
if (!isNaN(rowIndex) && !isNaN(colIndex)) {
|
|
1030
|
+
return { row: rowIndex, col: colIndex };
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Fallback: physical DOM index — only used for grids without coordinate
|
|
1035
|
+
// attributes (e.g. legacy non-table grid elements).
|
|
939
1036
|
const rows = Array.from(this.grid.querySelectorAll(`[${ROW_ATTR}]`));
|
|
940
1037
|
const rowIndex = rows.indexOf(row);
|
|
941
1038
|
|
|
@@ -1025,6 +1122,12 @@ export class TableCellSelection {
|
|
|
1025
1122
|
/**
|
|
1026
1123
|
* Find a cell by coordinate attributes first, falling back to index-based
|
|
1027
1124
|
* lookup when coordinate attributes are not present.
|
|
1125
|
+
*
|
|
1126
|
+
* When both primary lookups fail (e.g. `col` points to a covered logical
|
|
1127
|
+
* column that has no physical <td> of its own), scan all cells in the row
|
|
1128
|
+
* and return the one whose colspan range covers `col`. This handles the
|
|
1129
|
+
* case where `expandRectToMergedSpans` has expanded the selection corner to
|
|
1130
|
+
* a column that is spanned by an origin cell at a lower column index.
|
|
1028
1131
|
*/
|
|
1029
1132
|
private findCellByCoordOrIndex(
|
|
1030
1133
|
rows: NodeListOf<Element>,
|
|
@@ -1039,7 +1142,23 @@ export class TableCellSelection {
|
|
|
1039
1142
|
return coordCell;
|
|
1040
1143
|
}
|
|
1041
1144
|
|
|
1042
|
-
|
|
1145
|
+
const indexCell = rows[row]?.querySelectorAll(`[${CELL_ATTR}]`)[col] as HTMLElement | undefined;
|
|
1146
|
+
|
|
1147
|
+
if (indexCell) {
|
|
1148
|
+
return indexCell;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Neither coord-based nor index-based lookup found a cell. Walk all
|
|
1152
|
+
// physical cells in the row to find one whose logical column range covers
|
|
1153
|
+
// the requested column (origin cellCol <= col <= cellCol + colSpan - 1).
|
|
1154
|
+
const rowCells = Array.from(rows[row]?.querySelectorAll<HTMLElement>(`[${CELL_ATTR}]`) ?? []);
|
|
1155
|
+
|
|
1156
|
+
return rowCells.find(cell => {
|
|
1157
|
+
const cellCol = Number(cell.getAttribute(CELL_COL_ATTR));
|
|
1158
|
+
const cellColSpan = (cell as HTMLTableCellElement).colSpan || 1;
|
|
1159
|
+
|
|
1160
|
+
return cellCol <= col && cellCol + cellColSpan - 1 >= col;
|
|
1161
|
+
});
|
|
1043
1162
|
}
|
|
1044
1163
|
|
|
1045
1164
|
/**
|
|
@@ -413,17 +413,71 @@ export class TableGrid {
|
|
|
413
413
|
/**
|
|
414
414
|
* Reindex coordinate attributes on all cells after structural changes.
|
|
415
415
|
* Sets data-blok-table-cell-row and data-blok-table-cell-col to match
|
|
416
|
-
* each cell's
|
|
416
|
+
* each cell's model (logical) position, accounting for colspan and rowspan.
|
|
417
|
+
*
|
|
418
|
+
* Uses sparse table reconstruction: tracks columns blocked by rowspan cells
|
|
419
|
+
* from previous rows so that each DOM cell gets the correct model column index
|
|
420
|
+
* rather than its physical DOM index.
|
|
417
421
|
*/
|
|
418
422
|
public reindexCoordinates(table: HTMLElement): void {
|
|
419
|
-
const rows = table.querySelectorAll(`[${ROW_ATTR}]`);
|
|
423
|
+
const rows = Array.from(table.querySelectorAll(`[${ROW_ATTR}]`));
|
|
424
|
+
|
|
425
|
+
// Map from rowIndex -> Set of columnIndices occupied by rowspan cells from earlier rows
|
|
426
|
+
const occupiedCols: Map<number, Set<number>> = new Map();
|
|
420
427
|
|
|
421
428
|
rows.forEach((row, r) => {
|
|
422
|
-
const cells = row.querySelectorAll(`[${CELL_ATTR}]`);
|
|
429
|
+
const cells = Array.from(row.querySelectorAll(`[${CELL_ATTR}]`));
|
|
430
|
+
const blockedCols = occupiedCols.get(r) ?? new Set<number>();
|
|
431
|
+
|
|
432
|
+
cells.reduce((modelCol, cell) => {
|
|
433
|
+
const tdCell = cell as HTMLTableCellElement;
|
|
434
|
+
|
|
435
|
+
// Skip columns that are occupied by rowspan cells from previous rows
|
|
436
|
+
const skipBlocked = (c: number): number => (blockedCols.has(c) ? skipBlocked(c + 1) : c);
|
|
437
|
+
const col = skipBlocked(modelCol);
|
|
423
438
|
|
|
424
|
-
cells.forEach((cell, c) => {
|
|
425
439
|
cell.setAttribute(CELL_ROW_ATTR, String(r));
|
|
426
|
-
cell.setAttribute(CELL_COL_ATTR, String(
|
|
440
|
+
cell.setAttribute(CELL_COL_ATTR, String(col));
|
|
441
|
+
|
|
442
|
+
const colSpan = tdCell.colSpan || 1;
|
|
443
|
+
const rowSpan = tdCell.rowSpan || 1;
|
|
444
|
+
|
|
445
|
+
// If this cell has rowspan > 1, mark those columns as blocked in subsequent rows
|
|
446
|
+
if (rowSpan > 1) {
|
|
447
|
+
this.blockRowspanCols(occupiedCols, r, col, rowSpan, colSpan);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Advance by colspan
|
|
451
|
+
return col + colSpan;
|
|
452
|
+
}, 0);
|
|
453
|
+
|
|
454
|
+
occupiedCols.delete(r);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Register blocked columns in occupiedCols for a cell with rowspan > 1.
|
|
460
|
+
* All columns in [startCol, startCol + colSpan) are blocked for rows
|
|
461
|
+
* [startRow + 1, startRow + rowSpan).
|
|
462
|
+
*/
|
|
463
|
+
private blockRowspanCols(
|
|
464
|
+
occupiedCols: Map<number, Set<number>>,
|
|
465
|
+
startRow: number,
|
|
466
|
+
startCol: number,
|
|
467
|
+
rowSpan: number,
|
|
468
|
+
colSpan: number
|
|
469
|
+
): void {
|
|
470
|
+
Array.from({ length: rowSpan - 1 }, (_, i) => i + 1).forEach((dr) => {
|
|
471
|
+
const futureRow = startRow + dr;
|
|
472
|
+
|
|
473
|
+
if (!occupiedCols.has(futureRow)) {
|
|
474
|
+
occupiedCols.set(futureRow, new Set());
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const blocked = occupiedCols.get(futureRow) as Set<number>;
|
|
478
|
+
|
|
479
|
+
Array.from({ length: colSpan }, (_, dc) => dc).forEach((dc) => {
|
|
480
|
+
blocked.add(startCol + dc);
|
|
427
481
|
});
|
|
428
482
|
});
|
|
429
483
|
}
|
|
@@ -181,6 +181,10 @@ export class TableModel {
|
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
if (this.isSpannedCell(row, col)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
184
188
|
// Enforce invariant 5: no block in more than one cell
|
|
185
189
|
const existing = this.blockCellMap.get(blockId);
|
|
186
190
|
|
|
@@ -221,6 +225,10 @@ export class TableModel {
|
|
|
221
225
|
return;
|
|
222
226
|
}
|
|
223
227
|
|
|
228
|
+
if (this.isSpannedCell(row, col)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
224
232
|
// Remove old entries from map
|
|
225
233
|
for (const oldId of this.contentGrid[row][col].blocks) {
|
|
226
234
|
this.blockCellMap.delete(oldId);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { I18n } from '../../../types/api';
|
|
2
2
|
import { twMerge } from '../../components/utils/tw';
|
|
3
3
|
|
|
4
|
-
import { BORDER_WIDTH, CELL_ATTR, ROW_ATTR } from './table-core';
|
|
4
|
+
import { BORDER_WIDTH, CELL_ATTR, CELL_COL_ATTR, CELL_ROW_ATTR, ROW_ATTR } from './table-core';
|
|
5
5
|
import { collapseGrip, createGripDotsSvg, expandGrip, GRIP_HOVER_SIZE, setGripPillSize } from './table-grip-visuals';
|
|
6
6
|
import { getCumulativeColEdges, TableRowColDrag } from './table-row-col-drag';
|
|
7
7
|
import { createGripPopover } from './table-row-col-popover';
|
|
@@ -419,11 +419,39 @@ export class TableRowColControls {
|
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
const rowEl = rows[i] as HTMLElement;
|
|
422
|
-
const centerY = rowEl.offsetTop + rowEl.offsetHeight / 2;
|
|
423
|
-
const style = grip.style;
|
|
424
422
|
|
|
425
|
-
|
|
426
|
-
|
|
423
|
+
// Find the cell with the maximum rowSpan in this row to correctly
|
|
424
|
+
// center the grip over merged cells that span multiple rows.
|
|
425
|
+
const cellsInRow = this.grid.querySelectorAll<HTMLElement>(`[${CELL_ROW_ATTR}="${i}"]`);
|
|
426
|
+
const originCell = Array.from(cellsInRow).reduce<HTMLTableCellElement | null>((best, cell) => {
|
|
427
|
+
const tdCell = cell as HTMLTableCellElement;
|
|
428
|
+
const bestSpan = best !== null ? best.rowSpan || 1 : 0;
|
|
429
|
+
|
|
430
|
+
return (tdCell.rowSpan || 1) > bestSpan ? tdCell : best;
|
|
431
|
+
}, null);
|
|
432
|
+
const maxRowSpan = originCell !== null ? (originCell.rowSpan || 1) : 1;
|
|
433
|
+
|
|
434
|
+
if (maxRowSpan > 1 && originCell !== null) {
|
|
435
|
+
// Use getBoundingClientRect() on the origin cell to get its actual rendered
|
|
436
|
+
// height — summing tr.offsetHeight is inaccurate when the merged cell's
|
|
437
|
+
// content forces the browser to redistribute height across rows (each
|
|
438
|
+
// individual tr.offsetHeight stays at its minimum rather than reflecting
|
|
439
|
+
// the full visual contribution of the merged content).
|
|
440
|
+
const container = this.overlay ?? this.grid;
|
|
441
|
+
const containerRect = container.getBoundingClientRect();
|
|
442
|
+
const cellRect = originCell.getBoundingClientRect();
|
|
443
|
+
const centerY = cellRect.top - containerRect.top + cellRect.height / 2;
|
|
444
|
+
const style = grip.style;
|
|
445
|
+
|
|
446
|
+
style.left = `${-BORDER_WIDTH / 2}px`;
|
|
447
|
+
style.top = `${centerY}px`;
|
|
448
|
+
} else {
|
|
449
|
+
const centerY = rowEl.offsetTop + rowEl.offsetHeight / 2;
|
|
450
|
+
const style = grip.style;
|
|
451
|
+
|
|
452
|
+
style.left = `${-BORDER_WIDTH / 2}px`;
|
|
453
|
+
style.top = `${centerY}px`;
|
|
454
|
+
}
|
|
427
455
|
});
|
|
428
456
|
}
|
|
429
457
|
|
|
@@ -482,27 +510,21 @@ export class TableRowColControls {
|
|
|
482
510
|
}
|
|
483
511
|
|
|
484
512
|
private getCellPosition(cell: HTMLElement): { row: number; col: number } | null {
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
if (!row) {
|
|
488
|
-
return null;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const rows = Array.from(this.grid.querySelectorAll(`[${ROW_ATTR}]`));
|
|
492
|
-
const rowIndex = rows.indexOf(row);
|
|
513
|
+
const rowAttr = cell.getAttribute(CELL_ROW_ATTR);
|
|
514
|
+
const colAttr = cell.getAttribute(CELL_COL_ATTR);
|
|
493
515
|
|
|
494
|
-
if (
|
|
516
|
+
if (rowAttr === null || colAttr === null) {
|
|
495
517
|
return null;
|
|
496
518
|
}
|
|
497
519
|
|
|
498
|
-
const
|
|
499
|
-
const
|
|
520
|
+
const row = parseInt(rowAttr, 10);
|
|
521
|
+
const col = parseInt(colAttr, 10);
|
|
500
522
|
|
|
501
|
-
if (
|
|
523
|
+
if (isNaN(row) || isNaN(col)) {
|
|
502
524
|
return null;
|
|
503
525
|
}
|
|
504
526
|
|
|
505
|
-
return { row
|
|
527
|
+
return { row, col };
|
|
506
528
|
}
|
|
507
529
|
|
|
508
530
|
/**
|