autumnnote 1.0.7 → 1.0.8
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/README.md +3 -3
- package/dist/autumnnote.css +40 -4
- package/dist/autumnnote.es.js +513 -107
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +513 -107
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/js/editing/Style.js +195 -44
- package/src/js/editing/Typing.js +2 -2
- package/src/js/module/Editor.js +14 -3
- package/src/js/module/EmojiDialog.js +11 -0
- package/src/js/module/IconDialog.js +11 -0
- package/src/js/module/ImageDialog.js +9 -5
- package/src/js/module/Placeholder.js +6 -1
- package/src/js/module/TableTooltip.js +393 -86
- package/src/styles/autumnnote.scss +50 -5
|
@@ -64,6 +64,43 @@ function getCellAfterVisualCol(row, visualIdx) {
|
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Build a 2D grid map of the table, accounting for both rowspan and colspan.
|
|
69
|
+
*
|
|
70
|
+
* gridMap[r][c] = the DOM cell occupying visual grid position (r, c).
|
|
71
|
+
* cellPos = WeakMap: cell → { r, c, rs, cs } (top-left grid origin + span).
|
|
72
|
+
*
|
|
73
|
+
* Uses HTMLTableElement.rows which is scoped to the table itself and never
|
|
74
|
+
* includes rows from nested tables.
|
|
75
|
+
*
|
|
76
|
+
* @param {HTMLTableElement} table
|
|
77
|
+
* @returns {{ gridMap: Object, cellPos: WeakMap }}
|
|
78
|
+
*/
|
|
79
|
+
function buildGridMap(table) {
|
|
80
|
+
const rows = Array.from(table.rows);
|
|
81
|
+
const gridMap = {};
|
|
82
|
+
const cellPos = new WeakMap();
|
|
83
|
+
rows.forEach((row, r) => {
|
|
84
|
+
if (!gridMap[r]) gridMap[r] = {};
|
|
85
|
+
let c = 0;
|
|
86
|
+
for (const cell of row.cells) {
|
|
87
|
+
// Skip positions already occupied by a rowspan from a previous row
|
|
88
|
+
while (gridMap[r][c]) c++;
|
|
89
|
+
const rs = cell.rowSpan || 1;
|
|
90
|
+
const cs = cell.colSpan || 1;
|
|
91
|
+
cellPos.set(cell, { r, c, rs, cs });
|
|
92
|
+
for (let dr = 0; dr < rs; dr++) {
|
|
93
|
+
if (!gridMap[r + dr]) gridMap[r + dr] = {};
|
|
94
|
+
for (let dc = 0; dc < cs; dc++) {
|
|
95
|
+
gridMap[r + dr][c + dc] = cell;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
c += cs;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return { gridMap, cellPos };
|
|
102
|
+
}
|
|
103
|
+
|
|
67
104
|
const ICONS = {
|
|
68
105
|
rowAbove: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="12" x2="21" y2="12"/><path d="M12 3v7"/><path d="M9 7l3-4 3 4"/></svg>`,
|
|
69
106
|
rowBelow: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="12" x2="21" y2="12"/><path d="M12 12v7"/><path d="M9 17l3 4 3-4"/></svg>`,
|
|
@@ -71,11 +108,13 @@ const ICONS = {
|
|
|
71
108
|
colLeft: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="12" y1="3" x2="12" y2="21"/><path d="M3 12h7"/><path d="M7 8l-4 4 4 4"/></svg>`,
|
|
72
109
|
colRight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="12" y1="3" x2="12" y2="21"/><path d="M12 12h9"/><path d="M17 8l4 4-4 4"/></svg>`,
|
|
73
110
|
deleteCol: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="12" y1="3" x2="12" y2="21"/><line x1="15" y1="6" x2="21" y2="12"/><line x1="21" y1="6" x2="15" y2="12"/></svg>`,
|
|
74
|
-
mergeCells:
|
|
111
|
+
mergeCells: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="8" height="10" rx="1"/><rect x="14" y="7" width="8" height="10" rx="1"/><path d="M10 12h4"/><path d="M12 10l2 2-2 2"/></svg>`,
|
|
112
|
+
unmergeCells: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="5" width="20" height="14" rx="1"/><line x1="12" y1="5" x2="12" y2="19" stroke-dasharray="2.5 2"/><line x1="2" y1="12" x2="22" y2="12" stroke-dasharray="2.5 2"/><path d="M9 9 L6 12 L9 15"/><path d="M15 9 L18 12 L15 15"/></svg>`,
|
|
75
113
|
colWidth: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="4" x2="7" y2="20"/><line x1="17" y1="4" x2="17" y2="20"/><line x1="7" y1="12" x2="17" y2="12"/><path d="M10 9l-3 3 3 3"/><path d="M14 9l3 3-3 3"/></svg>`,
|
|
76
114
|
rowHeight: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" y1="7" x2="20" y2="7"/><line x1="4" y1="17" x2="20" y2="17"/><line x1="12" y1="7" x2="12" y2="17"/><path d="M9 10l3-3 3 3"/><path d="M9 14l3 3 3-3"/></svg>`,
|
|
77
115
|
tableBorder: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6" stroke-width="1"/><line x1="3" y1="13" x2="21" y2="13" stroke-width="2"/><line x1="3" y1="20" x2="21" y2="20" stroke-width="3"/></svg>`,
|
|
78
116
|
deleteTable: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/><line x1="16" y1="16" x2="22" y2="22" stroke="#ef4444"/><line x1="22" y1="16" x2="16" y2="22" stroke="#ef4444"/></svg>`,
|
|
117
|
+
selectCells: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4 L4 20 L9 15 L12 21 L14 20 L11 14 L17 14 Z" fill="currentColor" opacity="0.15"/><path d="M4 4 L4 20 L9 15 L12 21 L14 20 L11 14 L17 14 Z"/></svg>`,
|
|
79
118
|
};
|
|
80
119
|
|
|
81
120
|
export class TableTooltip {
|
|
@@ -92,6 +131,13 @@ export class TableTooltip {
|
|
|
92
131
|
this._sizeApply = null;
|
|
93
132
|
this._sizeTitleEl = null;
|
|
94
133
|
this._sizeInputEl = null;
|
|
134
|
+
// Cell selection
|
|
135
|
+
this._selectMode = false;
|
|
136
|
+
this._selectedCells = [];
|
|
137
|
+
this._selectStart = null;
|
|
138
|
+
this._selectDragging = false;
|
|
139
|
+
this._selectBtn = null;
|
|
140
|
+
this._editable = null;
|
|
95
141
|
}
|
|
96
142
|
|
|
97
143
|
initialize() {
|
|
@@ -102,6 +148,36 @@ export class TableTooltip {
|
|
|
102
148
|
document.body.appendChild(this._sizePopover);
|
|
103
149
|
|
|
104
150
|
const editable = this.context.layoutInfo.editable;
|
|
151
|
+
this._editable = editable;
|
|
152
|
+
|
|
153
|
+
// ── Cell selection drag ──────────────────────────────────────────────────
|
|
154
|
+
const onSelMousedown = (e) => {
|
|
155
|
+
if (!this._selectMode) return;
|
|
156
|
+
const cell = e.target.closest('td, th');
|
|
157
|
+
if (!cell || !editable.contains(cell)) return;
|
|
158
|
+
// Don't interfere with col/row resize (inline cursor is set by resize logic)
|
|
159
|
+
if (cell.style.cursor === 'col-resize' || cell.style.cursor === 'row-resize') return;
|
|
160
|
+
e.preventDefault(); // suppress text-cursor placement while selecting cells
|
|
161
|
+
this._activeTable = cell.closest('table');
|
|
162
|
+
this._selectStart = cell;
|
|
163
|
+
this._selectDragging = true;
|
|
164
|
+
this._setSelection([cell]);
|
|
165
|
+
};
|
|
166
|
+
const onSelMousemove = (e) => {
|
|
167
|
+
if (!this._selectMode || !this._selectDragging || !this._selectStart) return;
|
|
168
|
+
const cell = e.target.closest('td, th');
|
|
169
|
+
if (!cell || !editable.contains(cell)) return;
|
|
170
|
+
if (cell.closest('table') !== this._activeTable) return;
|
|
171
|
+
this._setSelection(this._getRectCells(this._selectStart, cell));
|
|
172
|
+
};
|
|
173
|
+
const onSelMouseup = () => { this._selectDragging = false; };
|
|
174
|
+
|
|
175
|
+
this._disposers.push(
|
|
176
|
+
on(editable, 'mousedown', onSelMousedown),
|
|
177
|
+
on(editable, 'mousemove', onSelMousemove),
|
|
178
|
+
on(document, 'mouseup', onSelMouseup),
|
|
179
|
+
);
|
|
180
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
105
181
|
|
|
106
182
|
this._disposers.push(
|
|
107
183
|
on(editable, 'mouseover', (e) => {
|
|
@@ -114,6 +190,7 @@ export class TableTooltip {
|
|
|
114
190
|
}
|
|
115
191
|
}, { passive: true }),
|
|
116
192
|
on(editable, 'mouseout', (e) => {
|
|
193
|
+
if (this._selectMode) return; // keep tooltip alive during cell selection
|
|
117
194
|
const to = e.relatedTarget;
|
|
118
195
|
if (!to || (
|
|
119
196
|
!editable.contains(to) &&
|
|
@@ -124,6 +201,7 @@ export class TableTooltip {
|
|
|
124
201
|
}
|
|
125
202
|
}, { passive: true }),
|
|
126
203
|
on(document, 'click', (e) => {
|
|
204
|
+
if (this._selectMode && this._activeTable && this._activeTable.contains(e.target)) return;
|
|
127
205
|
if (this._activeTable &&
|
|
128
206
|
!this._activeTable.contains(e.target) &&
|
|
129
207
|
!this._el.contains(e.target) &&
|
|
@@ -210,11 +288,16 @@ export class TableTooltip {
|
|
|
210
288
|
if (_edge === 'col') {
|
|
211
289
|
_startW = _nearCell.offsetWidth;
|
|
212
290
|
_colIdx = getVisualColIndex(_nearCell);
|
|
213
|
-
// Cache column cells once so onDocMove never runs querySelectorAll per frame
|
|
291
|
+
// Cache column cells once so onDocMove never runs querySelectorAll per frame.
|
|
292
|
+
// F-1: skip merged cells (colSpan > 1) — setting width on a merged cell
|
|
293
|
+
// distributes it equally across all spanned columns instead of resizing
|
|
294
|
+
// only the target column. Rows that have an individual cell at this
|
|
295
|
+
// visual column index are resized independently as expected.
|
|
214
296
|
_colCells = _colIdx >= 0
|
|
215
297
|
? Array.from(_table.querySelectorAll('tr'))
|
|
216
298
|
.map(r => getCellAtVisualCol(r, _colIdx))
|
|
217
299
|
.filter(Boolean)
|
|
300
|
+
.filter(c => (c.colSpan || 1) === 1)
|
|
218
301
|
: [];
|
|
219
302
|
document.body.style.cursor = 'col-resize';
|
|
220
303
|
} else {
|
|
@@ -307,6 +390,12 @@ export class TableTooltip {
|
|
|
307
390
|
|
|
308
391
|
el.appendChild(this._sep());
|
|
309
392
|
|
|
393
|
+
// Select-cells toggle
|
|
394
|
+
this._selectBtn = this._makeBtn(ICONS.selectCells, 'Select Cells', () => this._toggleSelectMode());
|
|
395
|
+
el.appendChild(this._selectBtn);
|
|
396
|
+
|
|
397
|
+
el.appendChild(this._sep());
|
|
398
|
+
|
|
310
399
|
// Row operations
|
|
311
400
|
el.appendChild(this._makeBtn(ICONS.rowAbove, 'Add Row Above', () => this._addRow('above')));
|
|
312
401
|
el.appendChild(this._makeBtn(ICONS.rowBelow, 'Add Row Below', () => this._addRow('below')));
|
|
@@ -322,7 +411,8 @@ export class TableTooltip {
|
|
|
322
411
|
el.appendChild(this._sep());
|
|
323
412
|
|
|
324
413
|
// Merge cells
|
|
325
|
-
el.appendChild(this._makeBtn(ICONS.mergeCells,
|
|
414
|
+
el.appendChild(this._makeBtn(ICONS.mergeCells, 'Merge Cells', () => this._mergeCells()));
|
|
415
|
+
el.appendChild(this._makeBtn(ICONS.unmergeCells, 'Unmerge Cells', () => this._unmergeCells()));
|
|
326
416
|
|
|
327
417
|
el.appendChild(this._sep());
|
|
328
418
|
|
|
@@ -342,6 +432,7 @@ export class TableTooltip {
|
|
|
342
432
|
this._disposers.push(
|
|
343
433
|
on(el, 'mouseenter', () => this._clearTimers()),
|
|
344
434
|
on(el, 'mouseleave', () => {
|
|
435
|
+
if (this._selectMode) return; // keep tooltip alive during cell selection
|
|
345
436
|
if (this._sizePopover && this._sizePopover.style.display !== 'none') return;
|
|
346
437
|
this._scheduleHide();
|
|
347
438
|
}),
|
|
@@ -412,6 +503,13 @@ export class TableTooltip {
|
|
|
412
503
|
this._el.style.display = 'none';
|
|
413
504
|
this._activeTable = null;
|
|
414
505
|
this._activeCell = null;
|
|
506
|
+
// Reset select mode
|
|
507
|
+
if (this._selectMode) {
|
|
508
|
+
this._selectMode = false;
|
|
509
|
+
if (this._selectBtn) this._selectBtn.classList.remove('an-link-tooltip-btn--active');
|
|
510
|
+
if (this._editable) this._editable.classList.remove('an-table-select-mode');
|
|
511
|
+
}
|
|
512
|
+
this._clearSelection();
|
|
415
513
|
this._clearTimers();
|
|
416
514
|
this._hideSizePopover();
|
|
417
515
|
}
|
|
@@ -461,18 +559,129 @@ export class TableTooltip {
|
|
|
461
559
|
|| (this._activeTable && this._activeTable.querySelector('td, th'));
|
|
462
560
|
}
|
|
463
561
|
|
|
562
|
+
// ---------------------------------------------------------------------------
|
|
563
|
+
// Cell selection helpers
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
|
|
566
|
+
_toggleSelectMode() {
|
|
567
|
+
this._selectMode = !this._selectMode;
|
|
568
|
+
if (this._selectBtn) {
|
|
569
|
+
this._selectBtn.classList.toggle('an-link-tooltip-btn--active', this._selectMode);
|
|
570
|
+
}
|
|
571
|
+
if (this._editable) {
|
|
572
|
+
this._editable.classList.toggle('an-table-select-mode', this._selectMode);
|
|
573
|
+
}
|
|
574
|
+
if (!this._selectMode) {
|
|
575
|
+
this._clearSelection();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_clearSelection() {
|
|
580
|
+
this._selectedCells.forEach((c) => c.classList.remove('an-cell-selected'));
|
|
581
|
+
this._selectedCells = [];
|
|
582
|
+
this._selectStart = null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
_setSelection(cells) {
|
|
586
|
+
// Remove highlight from cells no longer in selection
|
|
587
|
+
this._selectedCells.forEach((c) => {
|
|
588
|
+
if (!cells.includes(c)) c.classList.remove('an-cell-selected');
|
|
589
|
+
});
|
|
590
|
+
this._selectedCells = cells;
|
|
591
|
+
cells.forEach((c) => c.classList.add('an-cell-selected'));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Returns all cells in the rectangular area between startCell and endCell,
|
|
596
|
+
* correctly handling rowspan/colspan by using the grid map.
|
|
597
|
+
* The rect is expanded iteratively until it is stable — this ensures any
|
|
598
|
+
* merged cell that starts outside the initial rect but spans into it is
|
|
599
|
+
* fully included.
|
|
600
|
+
*/
|
|
601
|
+
_getRectCells(startCell, endCell) {
|
|
602
|
+
if (!startCell) return [];
|
|
603
|
+
if (!endCell || startCell === endCell) return [startCell];
|
|
604
|
+
const table = startCell.closest('table');
|
|
605
|
+
if (!table || !table.contains(endCell)) return [startCell];
|
|
606
|
+
|
|
607
|
+
const { gridMap, cellPos } = buildGridMap(table);
|
|
608
|
+
const sp = cellPos.get(startCell);
|
|
609
|
+
const ep = cellPos.get(endCell);
|
|
610
|
+
if (!sp || !ep) return [startCell];
|
|
611
|
+
|
|
612
|
+
let minR = Math.min(sp.r, ep.r);
|
|
613
|
+
let maxR = Math.max(sp.r + sp.rs - 1, ep.r + ep.rs - 1);
|
|
614
|
+
let minC = Math.min(sp.c, ep.c);
|
|
615
|
+
let maxC = Math.max(sp.c + sp.cs - 1, ep.c + ep.cs - 1);
|
|
616
|
+
|
|
617
|
+
// Iteratively expand until stable — handles spans that exceed the current rect
|
|
618
|
+
let changed = true;
|
|
619
|
+
while (changed) {
|
|
620
|
+
changed = false;
|
|
621
|
+
for (let r = minR; r <= maxR; r++) {
|
|
622
|
+
const rowMap = gridMap[r];
|
|
623
|
+
if (!rowMap) continue;
|
|
624
|
+
for (let c = minC; c <= maxC; c++) {
|
|
625
|
+
const cell = rowMap[c];
|
|
626
|
+
if (!cell) continue;
|
|
627
|
+
const pos = cellPos.get(cell);
|
|
628
|
+
if (!pos) continue;
|
|
629
|
+
if (pos.r < minR) { minR = pos.r; changed = true; }
|
|
630
|
+
if (pos.r + pos.rs - 1 > maxR) { maxR = pos.r + pos.rs - 1; changed = true; }
|
|
631
|
+
if (pos.c < minC) { minC = pos.c; changed = true; }
|
|
632
|
+
if (pos.c + pos.cs - 1 > maxC) { maxC = pos.c + pos.cs - 1; changed = true; }
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Collect unique cells in document order
|
|
638
|
+
const seen = new Set();
|
|
639
|
+
const result = [];
|
|
640
|
+
for (let r = minR; r <= maxR; r++) {
|
|
641
|
+
const rowMap = gridMap[r];
|
|
642
|
+
if (!rowMap) continue;
|
|
643
|
+
for (let c = minC; c <= maxC; c++) {
|
|
644
|
+
const cell = rowMap[c];
|
|
645
|
+
if (cell && !seen.has(cell)) {
|
|
646
|
+
seen.add(cell);
|
|
647
|
+
result.push(cell);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return result.length > 0 ? result : [startCell];
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Returns the active cell set: user-selected cells when available,
|
|
656
|
+
* otherwise the single active/cursor cell.
|
|
657
|
+
* @returns {HTMLTableCellElement[]}
|
|
658
|
+
*/
|
|
659
|
+
_getSelectedCells() {
|
|
660
|
+
return this._selectedCells.length > 0
|
|
661
|
+
? this._selectedCells
|
|
662
|
+
: [this._getCell()].filter(Boolean);
|
|
663
|
+
}
|
|
664
|
+
|
|
464
665
|
// ---------------------------------------------------------------------------
|
|
465
666
|
// Table operations
|
|
466
667
|
// ---------------------------------------------------------------------------
|
|
467
668
|
|
|
468
669
|
_addRow(position) {
|
|
469
|
-
const
|
|
470
|
-
if (!
|
|
471
|
-
const
|
|
472
|
-
if (!
|
|
473
|
-
const
|
|
670
|
+
const cells = this._getSelectedCells();
|
|
671
|
+
if (!cells.length) return;
|
|
672
|
+
const table = cells[0].closest('table');
|
|
673
|
+
if (!table) return;
|
|
674
|
+
const allRows = Array.from(table.querySelectorAll('tr'));
|
|
675
|
+
const selectedRows = [...new Set(cells.map((c) => c.closest('tr')).filter(Boolean))];
|
|
676
|
+
// Reference row: topmost for 'above', bottommost for 'below'
|
|
677
|
+
const refRow = selectedRows.reduce((best, r) => {
|
|
678
|
+
const bi = allRows.indexOf(best);
|
|
679
|
+
const ri = allRows.indexOf(r);
|
|
680
|
+
return position === 'above' ? (ri < bi ? r : best) : (ri > bi ? r : best);
|
|
681
|
+
});
|
|
682
|
+
const colCount = Array.from(refRow.cells).reduce((sum, c) => sum + (c.colSpan || 1), 0);
|
|
474
683
|
const newRow = document.createElement('tr');
|
|
475
|
-
const refCells = Array.from(
|
|
684
|
+
const refCells = Array.from(refRow.cells);
|
|
476
685
|
for (let i = 0; i < colCount; i++) {
|
|
477
686
|
const td = createElement('td', {}, ['\u00a0']);
|
|
478
687
|
const ref = refCells[i];
|
|
@@ -480,25 +689,24 @@ export class TableTooltip {
|
|
|
480
689
|
if (ref && ref.style.minWidth) td.style.minWidth = ref.style.minWidth;
|
|
481
690
|
newRow.appendChild(td);
|
|
482
691
|
}
|
|
483
|
-
if (position === 'above')
|
|
484
|
-
else
|
|
485
|
-
// Defer positioning until the browser has painted the new layout
|
|
692
|
+
if (position === 'above') refRow.parentElement?.insertBefore(newRow, refRow);
|
|
693
|
+
else refRow.insertAdjacentElement('afterend', newRow);
|
|
486
694
|
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
487
695
|
this.context.invoke('editor.afterCommand');
|
|
488
696
|
}
|
|
489
697
|
|
|
490
698
|
_addColumn(position) {
|
|
491
|
-
const
|
|
492
|
-
if (!
|
|
493
|
-
const table =
|
|
699
|
+
const cells = this._getSelectedCells();
|
|
700
|
+
if (!cells.length) return;
|
|
701
|
+
const table = cells[0].closest('table');
|
|
494
702
|
if (!table) return;
|
|
495
|
-
const
|
|
703
|
+
const colIndices = cells.map((c) => getVisualColIndex(c));
|
|
704
|
+
const targetColIdx = position === 'left' ? Math.min(...colIndices) : Math.max(...colIndices);
|
|
496
705
|
const rows = Array.from(table.querySelectorAll('tr'));
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const isHeaders = rows.map(r => r.closest('thead') !== null);
|
|
706
|
+
const refs = rows.map((r) => position === 'left'
|
|
707
|
+
? getCellAtVisualCol(r, targetColIdx)
|
|
708
|
+
: getCellAfterVisualCol(r, targetColIdx));
|
|
709
|
+
const isHeaders = rows.map((r) => r.closest('thead') !== null);
|
|
502
710
|
rows.forEach((r, i) => {
|
|
503
711
|
r.insertBefore(createElement(isHeaders[i] ? 'th' : 'td', {}, ['\u00a0']), refs[i]);
|
|
504
712
|
});
|
|
@@ -507,34 +715,45 @@ export class TableTooltip {
|
|
|
507
715
|
}
|
|
508
716
|
|
|
509
717
|
_deleteRow() {
|
|
510
|
-
const
|
|
511
|
-
if (!
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
if (!row || !table) return;
|
|
515
|
-
// Guard: do not delete the last body row (thead rows are not counted)
|
|
718
|
+
const cells = this._getSelectedCells();
|
|
719
|
+
if (!cells.length) return;
|
|
720
|
+
const table = cells[0].closest('table');
|
|
721
|
+
if (!table) return;
|
|
516
722
|
const tbody = table.querySelector('tbody');
|
|
517
|
-
const
|
|
518
|
-
|
|
723
|
+
const totalBodyRows = tbody
|
|
724
|
+
? tbody.querySelectorAll('tr').length
|
|
725
|
+
: table.querySelectorAll('tr').length;
|
|
726
|
+
const selectedRows = [...new Set(cells.map((c) => c.closest('tr')).filter(Boolean))];
|
|
727
|
+
const bodyRowsToDelete = selectedRows.filter((r) => r.closest('tbody'));
|
|
728
|
+
// Guard: keep at least one body row
|
|
729
|
+
if (bodyRowsToDelete.length >= totalBodyRows) return;
|
|
519
730
|
this._activeCell = null;
|
|
520
|
-
|
|
731
|
+
this._clearSelection();
|
|
732
|
+
selectedRows.forEach((r) => r.parentElement?.removeChild(r));
|
|
521
733
|
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
522
734
|
this.context.invoke('editor.afterCommand');
|
|
523
735
|
}
|
|
524
736
|
|
|
525
737
|
_deleteColumn() {
|
|
526
|
-
const
|
|
527
|
-
if (!
|
|
528
|
-
const table =
|
|
738
|
+
const cells = this._getSelectedCells();
|
|
739
|
+
if (!cells.length) return;
|
|
740
|
+
const table = cells[0].closest('table');
|
|
529
741
|
if (!table) return;
|
|
530
|
-
const
|
|
531
|
-
if (
|
|
532
|
-
const
|
|
742
|
+
const tableRows = Array.from(table.querySelectorAll('tr'));
|
|
743
|
+
if (tableRows[0] && tableRows[0].cells.length <= 1) return; // guard: keep ≥1 col
|
|
744
|
+
const colIndices = [...new Set(cells.map((c) => getVisualColIndex(c)))];
|
|
745
|
+
if (colIndices.length >= (tableRows[0]?.cells.length ?? 1)) return;
|
|
746
|
+
// Collect all cell references upfront before any removal (avoids stale visual indices)
|
|
747
|
+
const cellsToDelete = [];
|
|
748
|
+
colIndices.forEach((colIdx) => {
|
|
749
|
+
tableRows.forEach((r) => {
|
|
750
|
+
const c = getCellAtVisualCol(r, colIdx);
|
|
751
|
+
if (c) cellsToDelete.push(c);
|
|
752
|
+
});
|
|
753
|
+
});
|
|
533
754
|
this._activeCell = null;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
|
|
537
|
-
cells.forEach((c, i) => { if (c) rows[i].removeChild(c); });
|
|
755
|
+
this._clearSelection();
|
|
756
|
+
cellsToDelete.forEach((c) => c.parentElement?.removeChild(c));
|
|
538
757
|
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
539
758
|
this.context.invoke('editor.afterCommand');
|
|
540
759
|
}
|
|
@@ -542,41 +761,57 @@ export class TableTooltip {
|
|
|
542
761
|
_mergeCells() {
|
|
543
762
|
const cell = this._getCell();
|
|
544
763
|
if (!cell) return;
|
|
545
|
-
const sel = window.getSelection();
|
|
546
|
-
if (!sel || sel.rangeCount === 0) return;
|
|
547
|
-
const range = sel.getRangeAt(0);
|
|
548
764
|
const table = cell.closest('table');
|
|
549
765
|
if (!table) return;
|
|
550
766
|
|
|
551
|
-
//
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (rows.length === 1) {
|
|
561
|
-
// Horizontal merge within a single row
|
|
562
|
-
const row = rows[0];
|
|
563
|
-
const rowSelected = Array.from(row.cells).filter((c) => selected.includes(c));
|
|
564
|
-
if (rowSelected.length < 2) return;
|
|
565
|
-
const first = rowSelected[0];
|
|
566
|
-
first.colSpan = rowSelected.reduce((sum, c) => sum + (c.colSpan || 1), 0);
|
|
567
|
-
first.innerHTML = rowSelected.map((c) => c.innerHTML).join('');
|
|
568
|
-
rowSelected.slice(1).forEach((c) => row.removeChild(c));
|
|
569
|
-
} else {
|
|
570
|
-
// Vertical merge across rows — merge into first selected cell (rowspan)
|
|
571
|
-
const visualCols = [...new Set(selected.map((c) => getVisualColIndex(c)))];
|
|
572
|
-
if (visualCols.length !== 1) return; // only support single-column vertical merge
|
|
573
|
-
const first = selected[0];
|
|
574
|
-
first.rowSpan = selected.reduce((sum, c) => sum + (c.rowSpan || 1), 0);
|
|
575
|
-
first.innerHTML = selected.map((c) => c.innerHTML).join('');
|
|
576
|
-
selected.slice(1).forEach((c) => {
|
|
577
|
-
if (c.closest('tr')) c.closest('tr').removeChild(c);
|
|
767
|
+
// Prefer user panel-selected cells; fall back to text-selection range
|
|
768
|
+
let selected = this._getSelectedCells().filter((c) => table.contains(c));
|
|
769
|
+
if (selected.length < 2) {
|
|
770
|
+
const sel = window.getSelection();
|
|
771
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
772
|
+
const range = sel.getRangeAt(0);
|
|
773
|
+
const allCells = Array.from(table.querySelectorAll('td, th'));
|
|
774
|
+
selected = allCells.filter((c) => {
|
|
775
|
+
try { return range.intersectsNode(c); } catch { return false; }
|
|
578
776
|
});
|
|
777
|
+
if (selected.length < 2) return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const { gridMap, cellPos } = buildGridMap(table);
|
|
781
|
+
|
|
782
|
+
// Bounding rect that encompasses every selected cell's full span
|
|
783
|
+
let minR = Infinity, maxR = -Infinity, minC = Infinity, maxC = -Infinity;
|
|
784
|
+
selected.forEach((c) => {
|
|
785
|
+
const pos = cellPos.get(c);
|
|
786
|
+
if (!pos) return;
|
|
787
|
+
if (pos.r < minR) minR = pos.r;
|
|
788
|
+
if (pos.r + pos.rs - 1 > maxR) maxR = pos.r + pos.rs - 1;
|
|
789
|
+
if (pos.c < minC) minC = pos.c;
|
|
790
|
+
if (pos.c + pos.cs - 1 > maxC) maxC = pos.c + pos.cs - 1;
|
|
791
|
+
});
|
|
792
|
+
if (minR === Infinity) return;
|
|
793
|
+
|
|
794
|
+
// Collect every unique cell in the bounding rect
|
|
795
|
+
const seen = new Set();
|
|
796
|
+
const rectCells = [];
|
|
797
|
+
for (let r = minR; r <= maxR; r++) {
|
|
798
|
+
const rowMap = gridMap[r];
|
|
799
|
+
if (!rowMap) continue;
|
|
800
|
+
for (let c = minC; c <= maxC; c++) {
|
|
801
|
+
const tc = rowMap[c];
|
|
802
|
+
if (tc && !seen.has(tc)) { seen.add(tc); rectCells.push(tc); }
|
|
803
|
+
}
|
|
579
804
|
}
|
|
805
|
+
if (rectCells.length < 2) return;
|
|
806
|
+
|
|
807
|
+
const first = rectCells[0];
|
|
808
|
+
first.colSpan = maxC - minC + 1;
|
|
809
|
+
first.rowSpan = maxR - minR + 1;
|
|
810
|
+
first.style.verticalAlign = 'middle';
|
|
811
|
+
first.innerHTML = rectCells.map((c) => c.innerHTML).join('');
|
|
812
|
+
rectCells.slice(1).forEach((c) => c.parentElement?.removeChild(c));
|
|
813
|
+
|
|
814
|
+
this._clearSelection();
|
|
580
815
|
this.context.invoke('editor.afterCommand');
|
|
581
816
|
}
|
|
582
817
|
|
|
@@ -588,6 +823,73 @@ export class TableTooltip {
|
|
|
588
823
|
this.context.invoke('editor.afterCommand');
|
|
589
824
|
}
|
|
590
825
|
|
|
826
|
+
_unmergeCells() {
|
|
827
|
+
const cells = this._getSelectedCells();
|
|
828
|
+
if (!cells.length) return;
|
|
829
|
+
const table = cells[0].closest('table');
|
|
830
|
+
if (!table) return;
|
|
831
|
+
const mergedCells = cells.filter(
|
|
832
|
+
(c) => table.contains(c) && ((c.colSpan || 1) > 1 || (c.rowSpan || 1) > 1)
|
|
833
|
+
);
|
|
834
|
+
if (!mergedCells.length) return;
|
|
835
|
+
mergedCells.forEach((cell) => {
|
|
836
|
+
if (table.contains(cell)) this._unmergeOne(cell, table);
|
|
837
|
+
});
|
|
838
|
+
this._clearSelection();
|
|
839
|
+
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
840
|
+
this.context.invoke('editor.afterCommand');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Split a single merged cell (colspan/rowspan > 1) back into individual cells.
|
|
845
|
+
* New cells are empty ( ); the original cell retains its content.
|
|
846
|
+
* @param {HTMLTableCellElement} cell
|
|
847
|
+
* @param {HTMLTableElement} table
|
|
848
|
+
*/
|
|
849
|
+
_unmergeOne(cell, table) {
|
|
850
|
+
const cs = cell.colSpan || 1;
|
|
851
|
+
const rs = cell.rowSpan || 1;
|
|
852
|
+
if (cs === 1 && rs === 1) return;
|
|
853
|
+
|
|
854
|
+
// Build grid map from current DOM state (before any mutation)
|
|
855
|
+
const { cellPos } = buildGridMap(table);
|
|
856
|
+
const pos = cellPos.get(cell);
|
|
857
|
+
if (!pos) return;
|
|
858
|
+
|
|
859
|
+
const { r, c } = pos;
|
|
860
|
+
const tableRows = Array.from(table.rows);
|
|
861
|
+
const tag = cell.tagName.toLowerCase(); // preserve td / th
|
|
862
|
+
|
|
863
|
+
// Reset the original cell
|
|
864
|
+
cell.rowSpan = 1;
|
|
865
|
+
cell.colSpan = 1;
|
|
866
|
+
cell.style.verticalAlign = '';
|
|
867
|
+
|
|
868
|
+
// Same row: insert (cs - 1) sibling cells after the original cell
|
|
869
|
+
if (cs > 1) {
|
|
870
|
+
const insertRef = cell.nextElementSibling;
|
|
871
|
+
for (let dc = 1; dc < cs; dc++) {
|
|
872
|
+
tableRows[r].insertBefore(createElement(tag, {}, ['\u00a0']), insertRef);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Rows below: insert cs cells at the correct visual column position (for rowspan)
|
|
877
|
+
for (let dr = 1; dr < rs; dr++) {
|
|
878
|
+
const targetRow = tableRows[r + dr];
|
|
879
|
+
if (!targetRow) continue;
|
|
880
|
+
// Find the first cell in this row whose visual-col origin is beyond c
|
|
881
|
+
// (using cellPos built before mutations — valid for these untouched rows)
|
|
882
|
+
let ref = null;
|
|
883
|
+
for (const tc of targetRow.cells) {
|
|
884
|
+
const tp = cellPos.get(tc);
|
|
885
|
+
if (tp && tp.c > c) { ref = tc; break; }
|
|
886
|
+
}
|
|
887
|
+
for (let dc = 0; dc < cs; dc++) {
|
|
888
|
+
targetRow.insertBefore(createElement(tag, {}, ['\u00a0']), ref);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
591
893
|
// ---------------------------------------------------------------------------
|
|
592
894
|
// Size popover (column width / row height)
|
|
593
895
|
// ---------------------------------------------------------------------------
|
|
@@ -653,7 +955,6 @@ export class TableTooltip {
|
|
|
653
955
|
if (type === 'border') {
|
|
654
956
|
const table = cell.closest('table');
|
|
655
957
|
if (!table) return;
|
|
656
|
-
// Read current inline border-width, fall back to computed value, then default 1px
|
|
657
958
|
const firstCell = table.querySelector('td, th');
|
|
658
959
|
const currentPx = firstCell
|
|
659
960
|
? (parseInt(firstCell.style.borderWidth, 10) ||
|
|
@@ -664,19 +965,20 @@ export class TableTooltip {
|
|
|
664
965
|
this._sizeInputEl.max = '10';
|
|
665
966
|
this._sizeInputEl.value = currentPx;
|
|
666
967
|
this._sizeApply = (val) => {
|
|
667
|
-
// Apply border-width to every cell; with border-collapse:collapse this
|
|
668
|
-
// controls all grid lines including the outer table border.
|
|
669
|
-
// Setting only borderWidth preserves the existing border-color from CSS.
|
|
670
968
|
const cells = Array.from(table.querySelectorAll('td, th'));
|
|
671
969
|
if (val === 0) {
|
|
672
|
-
cells.forEach(c => { c.style.borderWidth = '0'; c.style.borderStyle = 'none'; });
|
|
970
|
+
cells.forEach((c) => { c.style.borderWidth = '0'; c.style.borderStyle = 'none'; });
|
|
673
971
|
} else {
|
|
674
|
-
cells.forEach(c => { c.style.borderWidth = `${val}px`; c.style.borderStyle = 'solid'; });
|
|
972
|
+
cells.forEach((c) => { c.style.borderWidth = `${val}px`; c.style.borderStyle = 'solid'; });
|
|
675
973
|
}
|
|
676
974
|
this.context.invoke('editor.afterCommand');
|
|
677
975
|
};
|
|
678
976
|
} else {
|
|
679
977
|
const isCol = type === 'col';
|
|
978
|
+
const activeCells = this._getSelectedCells().filter((c) => {
|
|
979
|
+
const t = c.closest('table');
|
|
980
|
+
return t && t === cell.closest('table');
|
|
981
|
+
});
|
|
680
982
|
this._sizeTitleEl.textContent = isCol ? 'Column Width (px)' : 'Row Height (px)';
|
|
681
983
|
this._sizeInputEl.min = '1';
|
|
682
984
|
this._sizeInputEl.max = '2000';
|
|
@@ -684,20 +986,25 @@ export class TableTooltip {
|
|
|
684
986
|
? (cell.offsetWidth || 120)
|
|
685
987
|
: (cell.closest('tr') ? (cell.closest('tr').offsetHeight || 40) : 40);
|
|
686
988
|
this._sizeApply = (val) => {
|
|
989
|
+
const table = cell.closest('table');
|
|
990
|
+
if (!table) return;
|
|
687
991
|
if (isCol) {
|
|
688
|
-
|
|
689
|
-
const
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
992
|
+
// Apply to all selected columns (or just the current column)
|
|
993
|
+
const colIndices = [...new Set(activeCells.map((c) => getVisualColIndex(c)))];
|
|
994
|
+
const tableRows = Array.from(table.querySelectorAll('tr'));
|
|
995
|
+
colIndices.forEach((colIdx) => {
|
|
996
|
+
tableRows.forEach((r) => {
|
|
997
|
+
const c = getCellAtVisualCol(r, colIdx);
|
|
998
|
+
// F-1: skip merged cells — same reason as drag-resize _colCells.
|
|
999
|
+
if (c && (c.colSpan || 1) === 1) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
|
|
1000
|
+
});
|
|
695
1001
|
});
|
|
696
1002
|
} else {
|
|
697
|
-
|
|
698
|
-
|
|
1003
|
+
// Apply to all selected rows (or just the current row)
|
|
1004
|
+
const selectedRows = [...new Set(activeCells.map((c) => c.closest('tr')).filter(Boolean))];
|
|
1005
|
+
selectedRows.forEach((row) => {
|
|
699
1006
|
for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
|
|
700
|
-
}
|
|
1007
|
+
});
|
|
701
1008
|
}
|
|
702
1009
|
this.context.invoke('editor.afterCommand');
|
|
703
1010
|
};
|