autumnnote 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +874 -0
  3. package/dist/autumnnote.css +1 -0
  4. package/dist/autumnnote.es.js +5888 -0
  5. package/dist/autumnnote.es.js.map +1 -0
  6. package/dist/autumnnote.umd.js +74 -0
  7. package/dist/autumnnote.umd.js.map +1 -0
  8. package/package.json +55 -0
  9. package/src/js/Context.js +497 -0
  10. package/src/js/core/dom.js +315 -0
  11. package/src/js/core/env.js +25 -0
  12. package/src/js/core/func.js +153 -0
  13. package/src/js/core/key.js +66 -0
  14. package/src/js/core/lists.js +121 -0
  15. package/src/js/core/markdown.js +294 -0
  16. package/src/js/core/range.js +194 -0
  17. package/src/js/core/sanitise.js +78 -0
  18. package/src/js/editing/History.js +205 -0
  19. package/src/js/editing/Style.js +329 -0
  20. package/src/js/editing/Table.js +59 -0
  21. package/src/js/editing/Typing.js +142 -0
  22. package/src/js/index.js +126 -0
  23. package/src/js/module/Buttons.js +300 -0
  24. package/src/js/module/Clipboard.js +460 -0
  25. package/src/js/module/CodeTooltip.js +428 -0
  26. package/src/js/module/Codeview.js +122 -0
  27. package/src/js/module/ContextMenu.js +470 -0
  28. package/src/js/module/Editor.js +528 -0
  29. package/src/js/module/EmojiDialog.js +726 -0
  30. package/src/js/module/FindReplace.js +440 -0
  31. package/src/js/module/Fullscreen.js +80 -0
  32. package/src/js/module/IconDialog.js +620 -0
  33. package/src/js/module/ImageDialog.js +208 -0
  34. package/src/js/module/ImageResizer.js +216 -0
  35. package/src/js/module/ImageTooltip.js +286 -0
  36. package/src/js/module/LinkDialog.js +204 -0
  37. package/src/js/module/LinkTooltip.js +242 -0
  38. package/src/js/module/Placeholder.js +44 -0
  39. package/src/js/module/ShortcutsDialog.js +141 -0
  40. package/src/js/module/Statusbar.js +238 -0
  41. package/src/js/module/TableTooltip.js +568 -0
  42. package/src/js/module/Toolbar.js +562 -0
  43. package/src/js/module/VideoDialog.js +263 -0
  44. package/src/js/module/VideoResizer.js +227 -0
  45. package/src/js/module/VideoTooltip.js +252 -0
  46. package/src/js/renderer.js +107 -0
  47. package/src/js/settings.js +134 -0
  48. package/src/styles/_variables.scss +48 -0
  49. package/src/styles/autumnnote.scss +1740 -0
  50. package/types/index.d.ts +324 -0
@@ -0,0 +1,568 @@
1
+ // TableTooltip.js - Hover tooltip for tables inside the editor
2
+ // Shows a horizontal action bar above (or below) the hovered table,
3
+ // similar in appearance and interaction to ImageTooltip / VideoTooltip.
4
+ import { createElement, on } from '../core/dom.js';
5
+
6
+ const SHOW_DELAY = 120;
7
+ const HIDE_DELAY = 200;
8
+
9
+ const ICONS = {
10
+ 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>`,
11
+ 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>`,
12
+ deleteRow: `<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"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="21" y1="15" x2="15" y2="21"/></svg>`,
13
+ 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>`,
14
+ 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>`,
15
+ 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>`,
16
+ 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>`,
17
+ 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>`,
18
+ 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>`,
19
+ 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>`,
20
+ };
21
+
22
+ export class TableTooltip {
23
+ /** @param {import('../Context.js').Context} context */
24
+ constructor(context) {
25
+ this.context = context;
26
+ this._el = null;
27
+ this._activeTable = null;
28
+ this._activeCell = null; // last hovered td/th
29
+ this._showTimer = null;
30
+ this._hideTimer = null;
31
+ this._disposers = [];
32
+ this._sizePopover = null;
33
+ this._sizeApply = null;
34
+ this._sizeTitleEl = null;
35
+ this._sizeInputEl = null;
36
+ }
37
+
38
+ initialize() {
39
+ this._el = this._buildTooltip();
40
+ document.body.appendChild(this._el);
41
+
42
+ this._sizePopover = this._buildSizePopover();
43
+ document.body.appendChild(this._sizePopover);
44
+
45
+ const editable = this.context.layoutInfo.editable;
46
+
47
+ this._disposers.push(
48
+ on(editable, 'mouseover', (e) => {
49
+ const table = e.target.closest('table');
50
+ if (table && editable.contains(table)) {
51
+ const cell = e.target.closest('td, th');
52
+ if (cell) this._activeCell = cell;
53
+ this._scheduleShow(table);
54
+ }
55
+ }),
56
+ on(editable, 'mouseout', (e) => {
57
+ const to = e.relatedTarget;
58
+ if (!to || (
59
+ !editable.contains(to) &&
60
+ !this._el.contains(to) &&
61
+ !(this._sizePopover && this._sizePopover.contains(to))
62
+ )) {
63
+ this._scheduleHide();
64
+ }
65
+ }),
66
+ on(document, 'click', (e) => {
67
+ if (this._activeTable &&
68
+ !this._activeTable.contains(e.target) &&
69
+ !this._el.contains(e.target) &&
70
+ !(this._sizePopover && this._sizePopover.contains(e.target))) {
71
+ this._hide();
72
+ }
73
+ }),
74
+ );
75
+
76
+ this._initResize();
77
+ return this;
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Column & row drag-resize
82
+ // ---------------------------------------------------------------------------
83
+
84
+ _initResize() {
85
+ const editable = this.context.layoutInfo.editable;
86
+ const HIT = 6; // px proximity threshold
87
+
88
+ // Shared hover state
89
+ let _nearCell = null;
90
+ let _nearEdge = null; // 'col' | 'row' | null
91
+
92
+ // Active drag state
93
+ let _resizing = false;
94
+ let _edge = null; // 'col' | 'row'
95
+ let _startX = 0;
96
+ let _startY = 0;
97
+ let _startW = 0;
98
+ let _startH = 0;
99
+ let _colIdx = -1;
100
+ let _row = null;
101
+ let _table = null;
102
+
103
+ const clearHover = () => {
104
+ if (_nearCell) { _nearCell.style.cursor = ''; _nearCell = null; }
105
+ _nearEdge = null;
106
+ };
107
+
108
+ const onEditorMove = (e) => {
109
+ if (_resizing) return;
110
+ const cell = e.target.closest('td, th');
111
+ if (!cell || !editable.contains(cell)) { clearHover(); return; }
112
+ const rect = cell.getBoundingClientRect();
113
+ const onRight = Math.abs(e.clientX - rect.right) < HIT;
114
+ const onBottom = Math.abs(e.clientY - rect.bottom) < HIT;
115
+ if (onRight && onBottom) {
116
+ // Corner — prefer col-resize
117
+ cell.style.cursor = 'col-resize';
118
+ _nearCell = cell; _nearEdge = 'col';
119
+ } else if (onRight) {
120
+ cell.style.cursor = 'col-resize';
121
+ _nearCell = cell; _nearEdge = 'col';
122
+ } else if (onBottom) {
123
+ cell.style.cursor = 'row-resize';
124
+ _nearCell = cell; _nearEdge = 'row';
125
+ } else {
126
+ clearHover();
127
+ }
128
+ };
129
+
130
+ const onEditorDown = (e) => {
131
+ if (!_nearCell || !_nearEdge) return;
132
+ _resizing = true;
133
+ _edge = _nearEdge;
134
+ _startX = e.clientX;
135
+ _startY = e.clientY;
136
+ _table = _nearCell.closest('table');
137
+ if (_edge === 'col') {
138
+ _startW = _nearCell.offsetWidth;
139
+ _colIdx = Array.from(_nearCell.closest('tr').cells).indexOf(_nearCell);
140
+ document.body.style.cursor = 'col-resize';
141
+ } else {
142
+ _row = _nearCell.closest('tr');
143
+ _startH = _row ? _row.offsetHeight : 40;
144
+ document.body.style.cursor = 'row-resize';
145
+ }
146
+ document.body.style.userSelect = 'none';
147
+ e.preventDefault();
148
+ e.stopPropagation();
149
+ };
150
+
151
+ const onDocMove = (e) => {
152
+ if (!_resizing) return;
153
+ if (_edge === 'col') {
154
+ const newW = Math.max(30, _startW + (e.clientX - _startX));
155
+ if (_table && _colIdx >= 0) {
156
+ Array.from(_table.querySelectorAll('tr')).forEach((r) => {
157
+ const c = r.cells[_colIdx];
158
+ if (c) { c.style.width = `${newW}px`; c.style.minWidth = `${newW}px`; }
159
+ });
160
+ }
161
+ } else {
162
+ const newH = Math.max(20, _startH + (e.clientY - _startY));
163
+ if (_row) {
164
+ Array.from(_row.cells).forEach((c) => {
165
+ c.style.height = `${newH}px`;
166
+ c.style.minHeight = `${newH}px`;
167
+ });
168
+ }
169
+ }
170
+ };
171
+
172
+ const onDocUp = () => {
173
+ if (!_resizing) return;
174
+ _resizing = false;
175
+ document.body.style.userSelect = '';
176
+ document.body.style.cursor = '';
177
+ _edge = null;
178
+ _table = null;
179
+ _row = null;
180
+ _colIdx = -1;
181
+ this.context.invoke('editor.afterCommand');
182
+ };
183
+
184
+ this._disposers.push(
185
+ on(editable, 'mousemove', onEditorMove),
186
+ on(editable, 'mousedown', onEditorDown),
187
+ on(document, 'mousemove', onDocMove),
188
+ on(document, 'mouseup', onDocUp),
189
+ );
190
+ }
191
+
192
+ destroy() {
193
+ this._clearTimers();
194
+ this._disposers.forEach((d) => d());
195
+ this._disposers = [];
196
+ if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
197
+ this._el = null;
198
+ if (this._sizePopover && this._sizePopover.parentNode) {
199
+ this._sizePopover.parentNode.removeChild(this._sizePopover);
200
+ }
201
+ this._sizePopover = null;
202
+ }
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // Build tooltip bar
206
+ // ---------------------------------------------------------------------------
207
+
208
+ _buildTooltip() {
209
+ const el = createElement('div', {
210
+ class: 'an-link-tooltip an-table-tooltip',
211
+ role: 'toolbar',
212
+ 'aria-label': 'Table actions',
213
+ });
214
+ el.style.display = 'none';
215
+
216
+ // Label
217
+ this._label = createElement('span', { class: 'an-link-tooltip-url' });
218
+ this._label.textContent = 'Table';
219
+ el.appendChild(this._label);
220
+
221
+ el.appendChild(this._sep());
222
+
223
+ // Row operations
224
+ el.appendChild(this._makeBtn(ICONS.rowAbove, 'Add Row Above', () => this._addRow('above')));
225
+ el.appendChild(this._makeBtn(ICONS.rowBelow, 'Add Row Below', () => this._addRow('below')));
226
+ el.appendChild(this._makeBtn(ICONS.deleteRow, 'Delete Row', () => this._deleteRow()));
227
+
228
+ el.appendChild(this._sep());
229
+
230
+ // Column operations
231
+ el.appendChild(this._makeBtn(ICONS.colLeft, 'Add Column Left', () => this._addColumn('left')));
232
+ el.appendChild(this._makeBtn(ICONS.colRight, 'Add Column Right', () => this._addColumn('right')));
233
+ el.appendChild(this._makeBtn(ICONS.deleteCol, 'Delete Column', () => this._deleteColumn()));
234
+
235
+ el.appendChild(this._sep());
236
+
237
+ // Merge cells
238
+ el.appendChild(this._makeBtn(ICONS.mergeCells, 'Merge Cells', () => this._mergeCells()));
239
+
240
+ el.appendChild(this._sep());
241
+
242
+ // Resize
243
+ el.appendChild(this._makeBtn(ICONS.colWidth, 'Column Width', () => this._openSizePopover('col')));
244
+ el.appendChild(this._makeBtn(ICONS.rowHeight, 'Row Height', () => this._openSizePopover('row')));
245
+
246
+ el.appendChild(this._sep());
247
+
248
+ // Delete table (danger)
249
+ el.appendChild(this._makeBtn(ICONS.deleteTable, 'Delete Table', () => this._deleteTable(), true));
250
+
251
+ // Keep tooltip alive while hovering.
252
+ // Don't schedule hide on mouseleave when the size popover is open —
253
+ // the user is moving the mouse toward it.
254
+ this._disposers.push(
255
+ on(el, 'mouseenter', () => this._clearTimers()),
256
+ on(el, 'mouseleave', () => {
257
+ if (this._sizePopover && this._sizePopover.style.display !== 'none') return;
258
+ this._scheduleHide();
259
+ }),
260
+ );
261
+
262
+ return el;
263
+ }
264
+
265
+ _sep() {
266
+ return createElement('div', { class: 'an-link-tooltip-sep' });
267
+ }
268
+
269
+ /**
270
+ * @param {string} icon
271
+ * @param {string} title
272
+ * @param {Function} handler
273
+ * @param {boolean} [isDanger]
274
+ */
275
+ _makeBtn(icon, title, handler, isDanger = false) {
276
+ const btn = createElement('button', {
277
+ type: 'button',
278
+ class: isDanger
279
+ ? 'an-link-tooltip-btn an-link-tooltip-btn--danger'
280
+ : 'an-link-tooltip-btn',
281
+ title,
282
+ });
283
+ btn.innerHTML = icon;
284
+ this._disposers.push(on(btn, 'click', (e) => {
285
+ e.preventDefault();
286
+ e.stopPropagation();
287
+ handler();
288
+ }));
289
+ return btn;
290
+ }
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // Show / Hide
294
+ // ---------------------------------------------------------------------------
295
+
296
+ _scheduleShow(table) {
297
+ if (this._activeTable === table && this._el.style.display !== 'none') return;
298
+ clearTimeout(this._hideTimer);
299
+ this._hideTimer = null;
300
+ clearTimeout(this._showTimer);
301
+ this._showTimer = setTimeout(() => {
302
+ this._activeTable = table;
303
+ this._show();
304
+ }, SHOW_DELAY);
305
+ }
306
+
307
+ _scheduleHide() {
308
+ clearTimeout(this._showTimer);
309
+ this._showTimer = null;
310
+ if (this._hideTimer) return;
311
+ this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
312
+ }
313
+
314
+ _show() {
315
+ if (!this._activeTable) return;
316
+ this._el.style.display = 'flex';
317
+ this._positionNear(this._activeTable);
318
+ }
319
+
320
+ _hide() {
321
+ this._el.style.display = 'none';
322
+ this._activeTable = null;
323
+ this._activeCell = null;
324
+ this._clearTimers();
325
+ this._hideSizePopover();
326
+ }
327
+
328
+ _clearTimers() {
329
+ clearTimeout(this._showTimer);
330
+ clearTimeout(this._hideTimer);
331
+ this._showTimer = null;
332
+ this._hideTimer = null;
333
+ }
334
+
335
+ _positionNear(table) {
336
+ if (!table) return;
337
+ const rect = table.getBoundingClientRect();
338
+ const tipW = this._el.offsetWidth || 400;
339
+ const tipH = this._el.offsetHeight || 30;
340
+ const margin = 6;
341
+
342
+ // Center horizontally over the table; prefer above it
343
+ let left = rect.left + (rect.width - tipW) / 2;
344
+ let top = rect.top - tipH - margin;
345
+
346
+ if (top < margin) top = rect.bottom + margin;
347
+ if (left + tipW > window.innerWidth - margin) left = window.innerWidth - tipW - margin;
348
+ if (left < margin) left = margin;
349
+
350
+ this._el.style.left = `${left}px`;
351
+ this._el.style.top = `${top}px`;
352
+ }
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // Helper: get active cell (fallback to first td/th in table)
356
+ // ---------------------------------------------------------------------------
357
+
358
+ _getCell() {
359
+ return this._activeCell
360
+ || (this._activeTable && this._activeTable.querySelector('td, th'));
361
+ }
362
+
363
+ // ---------------------------------------------------------------------------
364
+ // Table operations
365
+ // ---------------------------------------------------------------------------
366
+
367
+ _addRow(position) {
368
+ const cell = this._getCell();
369
+ if (!cell) return;
370
+ const row = cell.closest('tr');
371
+ if (!row) return;
372
+ const colCount = Array.from(row.cells).reduce((sum, c) => sum + (c.colSpan || 1), 0);
373
+ const newRow = document.createElement('tr');
374
+ const refCells = Array.from(row.cells);
375
+ for (let i = 0; i < colCount; i++) {
376
+ const td = createElement('td', {}, ['\u00a0']);
377
+ const ref = refCells[i];
378
+ if (ref && ref.style.width) td.style.width = ref.style.width;
379
+ if (ref && ref.style.minWidth) td.style.minWidth = ref.style.minWidth;
380
+ newRow.appendChild(td);
381
+ }
382
+ if (position === 'above') row.parentElement?.insertBefore(newRow, row);
383
+ else row.insertAdjacentElement('afterend', newRow);
384
+ this._positionNear(this._activeTable);
385
+ this.context.invoke('editor.afterCommand');
386
+ }
387
+
388
+ _addColumn(position) {
389
+ const cell = this._getCell();
390
+ if (!cell) return;
391
+ const row = cell.closest('tr');
392
+ const table = cell.closest('table');
393
+ if (!row || !table) return;
394
+ const colIndex = Array.from(row.cells).indexOf(cell);
395
+ Array.from(table.querySelectorAll('tr')).forEach((r) => {
396
+ const cells = Array.from(r.cells);
397
+ const isHeader = r.closest('thead') !== null;
398
+ const newCell = createElement(isHeader ? 'th' : 'td', {}, ['\u00a0']);
399
+ const ref = position === 'left' ? cells[colIndex] : (cells[colIndex + 1] || null);
400
+ r.insertBefore(newCell, ref);
401
+ });
402
+ this._positionNear(this._activeTable);
403
+ this.context.invoke('editor.afterCommand');
404
+ }
405
+
406
+ _deleteRow() {
407
+ const cell = this._getCell();
408
+ if (!cell) return;
409
+ const row = cell.closest('tr');
410
+ const table = cell.closest('table');
411
+ if (!row || !table) return;
412
+ // Guard: do not delete the last body row (thead rows are not counted)
413
+ const tbody = table.querySelector('tbody');
414
+ const bodyRows = tbody ? tbody.querySelectorAll('tr').length : table.querySelectorAll('tr').length;
415
+ if (bodyRows <= 1 && row.closest('tbody')) return;
416
+ this._activeCell = null;
417
+ row.parentElement?.removeChild(row);
418
+ this._positionNear(this._activeTable);
419
+ this.context.invoke('editor.afterCommand');
420
+ }
421
+
422
+ _deleteColumn() {
423
+ const cell = this._getCell();
424
+ if (!cell) return;
425
+ const row = cell.closest('tr');
426
+ const table = cell.closest('table');
427
+ if (!row || !table) return;
428
+ if (row.cells.length <= 1) return;
429
+ const colIndex = Array.from(row.cells).indexOf(cell);
430
+ this._activeCell = null;
431
+ Array.from(table.querySelectorAll('tr')).forEach((r) => {
432
+ const c = r.cells[colIndex];
433
+ if (c) r.removeChild(c);
434
+ });
435
+ this._positionNear(this._activeTable);
436
+ this.context.invoke('editor.afterCommand');
437
+ }
438
+
439
+ _mergeCells() {
440
+ const cell = this._getCell();
441
+ if (!cell) return;
442
+ const row = cell.closest('tr');
443
+ if (!row) return;
444
+ const sel = window.getSelection();
445
+ if (!sel || sel.rangeCount === 0) return;
446
+ const range = sel.getRangeAt(0);
447
+ const selected = Array.from(row.cells).filter((c) => {
448
+ try { return range.intersectsNode(c); } catch { return false; }
449
+ });
450
+ if (selected.length < 2) return;
451
+ const first = selected[0];
452
+ first.colSpan = selected.reduce((sum, c) => sum + (c.colSpan || 1), 0);
453
+ first.innerHTML = selected.map((c) => c.innerHTML).join('');
454
+ selected.slice(1).forEach((c) => row.removeChild(c));
455
+ this.context.invoke('editor.afterCommand');
456
+ }
457
+
458
+ _deleteTable() {
459
+ const table = this._activeTable;
460
+ if (!table) return;
461
+ this._hide();
462
+ if (table.parentNode) table.parentNode.removeChild(table);
463
+ this.context.invoke('editor.afterCommand');
464
+ }
465
+
466
+ // ---------------------------------------------------------------------------
467
+ // Size popover (column width / row height)
468
+ // ---------------------------------------------------------------------------
469
+
470
+ _buildSizePopover() {
471
+ const popover = createElement('div', { class: 'an-size-popover' });
472
+ popover.style.display = 'none';
473
+
474
+ const titleEl = createElement('div', { class: 'an-size-popover-title' });
475
+ const body = createElement('div', { class: 'an-size-popover-body' });
476
+ const inputEl = createElement('input', {
477
+ type: 'number', class: 'an-size-input', min: '1', max: '2000', step: '1',
478
+ });
479
+ const unitEl = createElement('span', { class: 'an-size-unit' }, ['px']);
480
+ body.appendChild(inputEl);
481
+ body.appendChild(unitEl);
482
+
483
+ const actionsEl = createElement('div', { class: 'an-size-popover-actions' });
484
+ const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
485
+ cancelBtn.textContent = 'Cancel';
486
+ const applyBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
487
+ applyBtn.textContent = 'Apply';
488
+ actionsEl.appendChild(cancelBtn);
489
+ actionsEl.appendChild(applyBtn);
490
+
491
+ popover.appendChild(titleEl);
492
+ popover.appendChild(body);
493
+ popover.appendChild(actionsEl);
494
+
495
+ this._sizeTitleEl = titleEl;
496
+ this._sizeInputEl = inputEl;
497
+ this._sizeApply = null;
498
+
499
+ const d1 = on(applyBtn, 'click', () => {
500
+ const val = parseInt(this._sizeInputEl.value, 10);
501
+ if (val > 0 && typeof this._sizeApply === 'function') this._sizeApply(val);
502
+ this._hideSizePopover();
503
+ });
504
+ const d2 = on(cancelBtn, 'click', () => this._hideSizePopover());
505
+ const d3 = on(inputEl, 'keydown', (e) => {
506
+ if (e.key === 'Enter') { e.preventDefault(); applyBtn.click(); }
507
+ if (e.key === 'Escape') this._hideSizePopover();
508
+ });
509
+ const d4 = on(document, 'click', (e) => {
510
+ if (this._sizePopover &&
511
+ this._sizePopover.style.display !== 'none' &&
512
+ !this._sizePopover.contains(e.target) &&
513
+ !this._el.contains(e.target)) {
514
+ this._hideSizePopover();
515
+ }
516
+ });
517
+ // Keep the tooltip/popover alive while the mouse is over the popover.
518
+ const d5 = on(popover, 'mouseenter', () => this._clearTimers());
519
+ const d6 = on(popover, 'mouseleave', () => this._scheduleHide());
520
+ this._disposers.push(d1, d2, d3, d4, d5, d6);
521
+ return popover;
522
+ }
523
+
524
+ _openSizePopover(type) {
525
+ const cell = this._getCell();
526
+ if (!cell || !this._sizePopover) return;
527
+ const isCol = type === 'col';
528
+ this._sizeTitleEl.textContent = isCol ? 'Column Width (px)' : 'Row Height (px)';
529
+ this._sizeInputEl.value = isCol
530
+ ? (cell.offsetWidth || 120)
531
+ : (cell.closest('tr') ? (cell.closest('tr').offsetHeight || 40) : 40);
532
+
533
+ this._sizeApply = (val) => {
534
+ if (isCol) {
535
+ const table = cell.closest('table');
536
+ const colIndex = Array.from(cell.closest('tr').cells).indexOf(cell);
537
+ Array.from(table.querySelectorAll('tr')).forEach((r) => {
538
+ const c = r.cells[colIndex];
539
+ if (c) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
540
+ });
541
+ } else {
542
+ const row = cell.closest('tr');
543
+ if (row) Array.from(row.cells).forEach((c) => { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; });
544
+ }
545
+ this.context.invoke('editor.afterCommand');
546
+ };
547
+
548
+ this._sizePopover.style.display = 'block';
549
+ requestAnimationFrame(() => {
550
+ if (!this._sizePopover || !this._el) return;
551
+ const tipRect = this._el.getBoundingClientRect();
552
+ const pw = this._sizePopover.offsetWidth || 220;
553
+ const ph = this._sizePopover.offsetHeight || 110;
554
+ let left = tipRect.left;
555
+ let top = tipRect.bottom + 6;
556
+ if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
557
+ if (top + ph > window.innerHeight - 8) top = tipRect.top - ph - 6;
558
+ this._sizePopover.style.left = `${left}px`;
559
+ this._sizePopover.style.top = `${top}px`;
560
+ if (this._sizeInputEl) { this._sizeInputEl.focus(); this._sizeInputEl.select(); }
561
+ });
562
+ }
563
+
564
+ _hideSizePopover() {
565
+ if (this._sizePopover) this._sizePopover.style.display = 'none';
566
+ this._sizeApply = null;
567
+ }
568
+ }