autumnnote 1.0.6 → 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 +168 -4
- package/dist/autumnnote.es.js +886 -115
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +886 -115
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +3 -2
- package/src/js/Context.js +6 -0
- package/src/js/core/sanitise.js +20 -2
- package/src/js/editing/Style.js +288 -24
- package/src/js/editing/Typing.js +2 -2
- package/src/js/module/Buttons.js +5 -4
- package/src/js/module/Clipboard.js +8 -0
- package/src/js/module/CodeTooltip.js +1 -0
- package/src/js/module/ContextMenu.js +146 -8
- package/src/js/module/Editor.js +73 -1
- package/src/js/module/EmojiDialog.js +11 -0
- package/src/js/module/IconDialog.js +11 -0
- package/src/js/module/ImageCropOverlay.js +2 -2
- package/src/js/module/ImageDialog.js +22 -3
- package/src/js/module/ImageResizer.js +2 -0
- package/src/js/module/ImageTooltip.js +9 -0
- package/src/js/module/LinkTooltip.js +4 -0
- package/src/js/module/Placeholder.js +7 -1
- package/src/js/module/TableTooltip.js +399 -86
- package/src/js/module/Toolbar.js +21 -3
- package/src/js/module/VideoDialog.js +5 -5
- package/src/js/module/VideoResizer.js +11 -0
- package/src/js/module/VideoTooltip.js +1 -0
- package/src/js/renderer.js +3 -0
- package/src/styles/autumnnote.scss +194 -12
|
@@ -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,9 +148,40 @@ 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) => {
|
|
184
|
+
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
108
185
|
const table = e.target.closest('table');
|
|
109
186
|
if (table && editable.contains(table)) {
|
|
110
187
|
const cell = e.target.closest('td, th');
|
|
@@ -113,6 +190,7 @@ export class TableTooltip {
|
|
|
113
190
|
}
|
|
114
191
|
}, { passive: true }),
|
|
115
192
|
on(editable, 'mouseout', (e) => {
|
|
193
|
+
if (this._selectMode) return; // keep tooltip alive during cell selection
|
|
116
194
|
const to = e.relatedTarget;
|
|
117
195
|
if (!to || (
|
|
118
196
|
!editable.contains(to) &&
|
|
@@ -123,6 +201,7 @@ export class TableTooltip {
|
|
|
123
201
|
}
|
|
124
202
|
}, { passive: true }),
|
|
125
203
|
on(document, 'click', (e) => {
|
|
204
|
+
if (this._selectMode && this._activeTable && this._activeTable.contains(e.target)) return;
|
|
126
205
|
if (this._activeTable &&
|
|
127
206
|
!this._activeTable.contains(e.target) &&
|
|
128
207
|
!this._el.contains(e.target) &&
|
|
@@ -169,6 +248,10 @@ export class TableTooltip {
|
|
|
169
248
|
|
|
170
249
|
const onEditorMove = (e) => {
|
|
171
250
|
if (_resizing) return;
|
|
251
|
+
if (this.context.layoutInfo.container.classList.contains('an-disabled')) {
|
|
252
|
+
clearHover();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
172
255
|
// Throttle to one check per animation frame — getBoundingClientRect forces reflow
|
|
173
256
|
if (_rafEditorMove !== null) return;
|
|
174
257
|
const target = e.target;
|
|
@@ -195,6 +278,7 @@ export class TableTooltip {
|
|
|
195
278
|
};
|
|
196
279
|
|
|
197
280
|
const onEditorDown = (e) => {
|
|
281
|
+
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
198
282
|
if (!_nearCell || !_nearEdge) return;
|
|
199
283
|
_resizing = true;
|
|
200
284
|
_edge = _nearEdge;
|
|
@@ -204,11 +288,16 @@ export class TableTooltip {
|
|
|
204
288
|
if (_edge === 'col') {
|
|
205
289
|
_startW = _nearCell.offsetWidth;
|
|
206
290
|
_colIdx = getVisualColIndex(_nearCell);
|
|
207
|
-
// 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.
|
|
208
296
|
_colCells = _colIdx >= 0
|
|
209
297
|
? Array.from(_table.querySelectorAll('tr'))
|
|
210
298
|
.map(r => getCellAtVisualCol(r, _colIdx))
|
|
211
299
|
.filter(Boolean)
|
|
300
|
+
.filter(c => (c.colSpan || 1) === 1)
|
|
212
301
|
: [];
|
|
213
302
|
document.body.style.cursor = 'col-resize';
|
|
214
303
|
} else {
|
|
@@ -301,6 +390,12 @@ export class TableTooltip {
|
|
|
301
390
|
|
|
302
391
|
el.appendChild(this._sep());
|
|
303
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
|
+
|
|
304
399
|
// Row operations
|
|
305
400
|
el.appendChild(this._makeBtn(ICONS.rowAbove, 'Add Row Above', () => this._addRow('above')));
|
|
306
401
|
el.appendChild(this._makeBtn(ICONS.rowBelow, 'Add Row Below', () => this._addRow('below')));
|
|
@@ -316,7 +411,8 @@ export class TableTooltip {
|
|
|
316
411
|
el.appendChild(this._sep());
|
|
317
412
|
|
|
318
413
|
// Merge cells
|
|
319
|
-
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()));
|
|
320
416
|
|
|
321
417
|
el.appendChild(this._sep());
|
|
322
418
|
|
|
@@ -336,6 +432,7 @@ export class TableTooltip {
|
|
|
336
432
|
this._disposers.push(
|
|
337
433
|
on(el, 'mouseenter', () => this._clearTimers()),
|
|
338
434
|
on(el, 'mouseleave', () => {
|
|
435
|
+
if (this._selectMode) return; // keep tooltip alive during cell selection
|
|
339
436
|
if (this._sizePopover && this._sizePopover.style.display !== 'none') return;
|
|
340
437
|
this._scheduleHide();
|
|
341
438
|
}),
|
|
@@ -406,6 +503,13 @@ export class TableTooltip {
|
|
|
406
503
|
this._el.style.display = 'none';
|
|
407
504
|
this._activeTable = null;
|
|
408
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();
|
|
409
513
|
this._clearTimers();
|
|
410
514
|
this._hideSizePopover();
|
|
411
515
|
}
|
|
@@ -455,18 +559,129 @@ export class TableTooltip {
|
|
|
455
559
|
|| (this._activeTable && this._activeTable.querySelector('td, th'));
|
|
456
560
|
}
|
|
457
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
|
+
|
|
458
665
|
// ---------------------------------------------------------------------------
|
|
459
666
|
// Table operations
|
|
460
667
|
// ---------------------------------------------------------------------------
|
|
461
668
|
|
|
462
669
|
_addRow(position) {
|
|
463
|
-
const
|
|
464
|
-
if (!
|
|
465
|
-
const
|
|
466
|
-
if (!
|
|
467
|
-
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);
|
|
468
683
|
const newRow = document.createElement('tr');
|
|
469
|
-
const refCells = Array.from(
|
|
684
|
+
const refCells = Array.from(refRow.cells);
|
|
470
685
|
for (let i = 0; i < colCount; i++) {
|
|
471
686
|
const td = createElement('td', {}, ['\u00a0']);
|
|
472
687
|
const ref = refCells[i];
|
|
@@ -474,25 +689,24 @@ export class TableTooltip {
|
|
|
474
689
|
if (ref && ref.style.minWidth) td.style.minWidth = ref.style.minWidth;
|
|
475
690
|
newRow.appendChild(td);
|
|
476
691
|
}
|
|
477
|
-
if (position === 'above')
|
|
478
|
-
else
|
|
479
|
-
// 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);
|
|
480
694
|
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
481
695
|
this.context.invoke('editor.afterCommand');
|
|
482
696
|
}
|
|
483
697
|
|
|
484
698
|
_addColumn(position) {
|
|
485
|
-
const
|
|
486
|
-
if (!
|
|
487
|
-
const table =
|
|
699
|
+
const cells = this._getSelectedCells();
|
|
700
|
+
if (!cells.length) return;
|
|
701
|
+
const table = cells[0].closest('table');
|
|
488
702
|
if (!table) return;
|
|
489
|
-
const
|
|
703
|
+
const colIndices = cells.map((c) => getVisualColIndex(c));
|
|
704
|
+
const targetColIdx = position === 'left' ? Math.min(...colIndices) : Math.max(...colIndices);
|
|
490
705
|
const rows = Array.from(table.querySelectorAll('tr'));
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
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);
|
|
496
710
|
rows.forEach((r, i) => {
|
|
497
711
|
r.insertBefore(createElement(isHeaders[i] ? 'th' : 'td', {}, ['\u00a0']), refs[i]);
|
|
498
712
|
});
|
|
@@ -501,34 +715,45 @@ export class TableTooltip {
|
|
|
501
715
|
}
|
|
502
716
|
|
|
503
717
|
_deleteRow() {
|
|
504
|
-
const
|
|
505
|
-
if (!
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
if (!row || !table) return;
|
|
509
|
-
// 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;
|
|
510
722
|
const tbody = table.querySelector('tbody');
|
|
511
|
-
const
|
|
512
|
-
|
|
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;
|
|
513
730
|
this._activeCell = null;
|
|
514
|
-
|
|
731
|
+
this._clearSelection();
|
|
732
|
+
selectedRows.forEach((r) => r.parentElement?.removeChild(r));
|
|
515
733
|
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
516
734
|
this.context.invoke('editor.afterCommand');
|
|
517
735
|
}
|
|
518
736
|
|
|
519
737
|
_deleteColumn() {
|
|
520
|
-
const
|
|
521
|
-
if (!
|
|
522
|
-
const table =
|
|
738
|
+
const cells = this._getSelectedCells();
|
|
739
|
+
if (!cells.length) return;
|
|
740
|
+
const table = cells[0].closest('table');
|
|
523
741
|
if (!table) return;
|
|
524
|
-
const
|
|
525
|
-
if (
|
|
526
|
-
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
|
+
});
|
|
527
754
|
this._activeCell = null;
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
|
|
531
|
-
cells.forEach((c, i) => { if (c) rows[i].removeChild(c); });
|
|
755
|
+
this._clearSelection();
|
|
756
|
+
cellsToDelete.forEach((c) => c.parentElement?.removeChild(c));
|
|
532
757
|
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
533
758
|
this.context.invoke('editor.afterCommand');
|
|
534
759
|
}
|
|
@@ -536,41 +761,57 @@ export class TableTooltip {
|
|
|
536
761
|
_mergeCells() {
|
|
537
762
|
const cell = this._getCell();
|
|
538
763
|
if (!cell) return;
|
|
539
|
-
const sel = window.getSelection();
|
|
540
|
-
if (!sel || sel.rangeCount === 0) return;
|
|
541
|
-
const range = sel.getRangeAt(0);
|
|
542
764
|
const table = cell.closest('table');
|
|
543
765
|
if (!table) return;
|
|
544
766
|
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if (rows.length === 1) {
|
|
555
|
-
// Horizontal merge within a single row
|
|
556
|
-
const row = rows[0];
|
|
557
|
-
const rowSelected = Array.from(row.cells).filter((c) => selected.includes(c));
|
|
558
|
-
if (rowSelected.length < 2) return;
|
|
559
|
-
const first = rowSelected[0];
|
|
560
|
-
first.colSpan = rowSelected.reduce((sum, c) => sum + (c.colSpan || 1), 0);
|
|
561
|
-
first.innerHTML = rowSelected.map((c) => c.innerHTML).join('');
|
|
562
|
-
rowSelected.slice(1).forEach((c) => row.removeChild(c));
|
|
563
|
-
} else {
|
|
564
|
-
// Vertical merge across rows — merge into first selected cell (rowspan)
|
|
565
|
-
const visualCols = [...new Set(selected.map((c) => getVisualColIndex(c)))];
|
|
566
|
-
if (visualCols.length !== 1) return; // only support single-column vertical merge
|
|
567
|
-
const first = selected[0];
|
|
568
|
-
first.rowSpan = selected.reduce((sum, c) => sum + (c.rowSpan || 1), 0);
|
|
569
|
-
first.innerHTML = selected.map((c) => c.innerHTML).join('');
|
|
570
|
-
selected.slice(1).forEach((c) => {
|
|
571
|
-
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; }
|
|
572
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
|
+
}
|
|
573
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();
|
|
574
815
|
this.context.invoke('editor.afterCommand');
|
|
575
816
|
}
|
|
576
817
|
|
|
@@ -582,6 +823,73 @@ export class TableTooltip {
|
|
|
582
823
|
this.context.invoke('editor.afterCommand');
|
|
583
824
|
}
|
|
584
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
|
+
|
|
585
893
|
// ---------------------------------------------------------------------------
|
|
586
894
|
// Size popover (column width / row height)
|
|
587
895
|
// ---------------------------------------------------------------------------
|
|
@@ -647,7 +955,6 @@ export class TableTooltip {
|
|
|
647
955
|
if (type === 'border') {
|
|
648
956
|
const table = cell.closest('table');
|
|
649
957
|
if (!table) return;
|
|
650
|
-
// Read current inline border-width, fall back to computed value, then default 1px
|
|
651
958
|
const firstCell = table.querySelector('td, th');
|
|
652
959
|
const currentPx = firstCell
|
|
653
960
|
? (parseInt(firstCell.style.borderWidth, 10) ||
|
|
@@ -658,19 +965,20 @@ export class TableTooltip {
|
|
|
658
965
|
this._sizeInputEl.max = '10';
|
|
659
966
|
this._sizeInputEl.value = currentPx;
|
|
660
967
|
this._sizeApply = (val) => {
|
|
661
|
-
// Apply border-width to every cell; with border-collapse:collapse this
|
|
662
|
-
// controls all grid lines including the outer table border.
|
|
663
|
-
// Setting only borderWidth preserves the existing border-color from CSS.
|
|
664
968
|
const cells = Array.from(table.querySelectorAll('td, th'));
|
|
665
969
|
if (val === 0) {
|
|
666
|
-
cells.forEach(c => { c.style.borderWidth = '0'; c.style.borderStyle = 'none'; });
|
|
970
|
+
cells.forEach((c) => { c.style.borderWidth = '0'; c.style.borderStyle = 'none'; });
|
|
667
971
|
} else {
|
|
668
|
-
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'; });
|
|
669
973
|
}
|
|
670
974
|
this.context.invoke('editor.afterCommand');
|
|
671
975
|
};
|
|
672
976
|
} else {
|
|
673
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
|
+
});
|
|
674
982
|
this._sizeTitleEl.textContent = isCol ? 'Column Width (px)' : 'Row Height (px)';
|
|
675
983
|
this._sizeInputEl.min = '1';
|
|
676
984
|
this._sizeInputEl.max = '2000';
|
|
@@ -678,20 +986,25 @@ export class TableTooltip {
|
|
|
678
986
|
? (cell.offsetWidth || 120)
|
|
679
987
|
: (cell.closest('tr') ? (cell.closest('tr').offsetHeight || 40) : 40);
|
|
680
988
|
this._sizeApply = (val) => {
|
|
989
|
+
const table = cell.closest('table');
|
|
990
|
+
if (!table) return;
|
|
681
991
|
if (isCol) {
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
+
});
|
|
689
1001
|
});
|
|
690
1002
|
} else {
|
|
691
|
-
|
|
692
|
-
|
|
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) => {
|
|
693
1006
|
for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
|
|
694
|
-
}
|
|
1007
|
+
});
|
|
695
1008
|
}
|
|
696
1009
|
this.context.invoke('editor.afterCommand');
|
|
697
1010
|
};
|