autumnnote 2.1.0 → 2.3.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 (65) hide show
  1. package/README.md +74 -6
  2. package/dist/autumnnote.cjs +27 -22
  3. package/dist/autumnnote.es.js +474 -591
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.min.js +27 -22
  6. package/dist/autumnnote.umd.js +27 -22
  7. package/dist/autumnnote.umd.js.map +1 -1
  8. package/dist/icon-data-V0Xqv-wX.js +255 -0
  9. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  10. package/package.json +12 -3
  11. package/types/index.d.ts +8 -0
  12. package/src/js/Context.js +0 -854
  13. package/src/js/core/detectLang.js +0 -98
  14. package/src/js/core/dom.js +0 -372
  15. package/src/js/core/env.js +0 -38
  16. package/src/js/core/key.js +0 -66
  17. package/src/js/core/lists.js +0 -121
  18. package/src/js/core/markdown.js +0 -695
  19. package/src/js/core/range.js +0 -194
  20. package/src/js/core/sanitise.js +0 -304
  21. package/src/js/editing/History.js +0 -266
  22. package/src/js/editing/Style.js +0 -812
  23. package/src/js/editing/Table.js +0 -105
  24. package/src/js/editing/Typing.js +0 -397
  25. package/src/js/index.js +0 -193
  26. package/src/js/index.umd.js +0 -17
  27. package/src/js/module/AutoSaveRestore.js +0 -125
  28. package/src/js/module/BaseDialog.js +0 -133
  29. package/src/js/module/BaseMediaTooltip.js +0 -142
  30. package/src/js/module/BaseResizer.js +0 -322
  31. package/src/js/module/BubbleToolbar.js +0 -483
  32. package/src/js/module/Buttons.js +0 -399
  33. package/src/js/module/Clipboard.js +0 -579
  34. package/src/js/module/CodeTooltip.js +0 -493
  35. package/src/js/module/Codeview.js +0 -125
  36. package/src/js/module/ContextMenu.js +0 -621
  37. package/src/js/module/Editor.js +0 -747
  38. package/src/js/module/EmojiDialog.js +0 -254
  39. package/src/js/module/FindReplace.js +0 -512
  40. package/src/js/module/Fullscreen.js +0 -80
  41. package/src/js/module/IconDialog.js +0 -618
  42. package/src/js/module/ImageCropOverlay.js +0 -586
  43. package/src/js/module/ImageDialog.js +0 -193
  44. package/src/js/module/ImageResizer.js +0 -42
  45. package/src/js/module/ImageTooltip.js +0 -285
  46. package/src/js/module/LinkDialog.js +0 -145
  47. package/src/js/module/LinkTooltip.js +0 -250
  48. package/src/js/module/MarkdownShortcuts.js +0 -250
  49. package/src/js/module/Mention.js +0 -365
  50. package/src/js/module/Placeholder.js +0 -51
  51. package/src/js/module/ShortcutsDialog.js +0 -111
  52. package/src/js/module/SlashMenu.js +0 -376
  53. package/src/js/module/Statusbar.js +0 -246
  54. package/src/js/module/TableTooltip.js +0 -1392
  55. package/src/js/module/Toolbar.js +0 -855
  56. package/src/js/module/VideoDialog.js +0 -193
  57. package/src/js/module/VideoResizer.js +0 -66
  58. package/src/js/module/VideoTooltip.js +0 -248
  59. package/src/js/module/emoji-data.js +0 -496
  60. package/src/js/module/table-grid.js +0 -102
  61. package/src/js/module/table-icons.js +0 -33
  62. package/src/js/renderer.js +0 -120
  63. package/src/js/settings.js +0 -214
  64. package/src/styles/_variables.scss +0 -48
  65. package/src/styles/autumnnote.scss +0 -2877
@@ -1,1392 +0,0 @@
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
- import { ICONS } from './table-icons.js';
6
- import {
7
- getVisualColIndex,
8
- getCellAtVisualCol,
9
- getCellAfterVisualCol,
10
- buildGridMap,
11
- } from './table-grid.js';
12
-
13
- const SHOW_DELAY = 120;
14
- const HIDE_DELAY = 200;
15
-
16
- const SHADE_PRESETS = [
17
- '#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#efefef', '#ffffff',
18
- '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#9900ff', '#ff00ff',
19
- '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#d9d2e9', '#ead1dc',
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
- // Cell shade popover
37
- this._shadePopover = null;
38
- this._shadeTitleEl = null;
39
- this._shadeColorStrip = null;
40
- // Border color popover
41
- this._borderColorPopover = null;
42
- this._borderColorTitleEl = null;
43
- this._borderColorStrip = null;
44
- this._borderColorNoBtn = null;
45
- // Cell selection
46
- this._selectMode = false;
47
- this._selectedCells = [];
48
- this._selectStart = null;
49
- this._selectDragging = false;
50
- this._selectBtn = null;
51
- this._editable = null;
52
- }
53
-
54
- initialize() {
55
- this._el = this._buildTooltip();
56
- document.body.appendChild(this._el);
57
-
58
- this._sizePopover = this._buildSizePopover();
59
- document.body.appendChild(this._sizePopover);
60
-
61
- this._shadePopover = this._buildCellShadePopover();
62
- document.body.appendChild(this._shadePopover);
63
-
64
- this._borderColorPopover = this._buildBorderColorPopover();
65
- document.body.appendChild(this._borderColorPopover);
66
-
67
- const editable = this.context.layoutInfo.editable;
68
- this._editable = editable;
69
-
70
- // ── Cell selection drag ──────────────────────────────────────────────────
71
- const onSelMousedown = (e) => {
72
- if (!this._selectMode) return;
73
- const cell = e.target.closest('td, th');
74
- if (!cell || !editable.contains(cell)) return;
75
- // Don't interfere with col/row resize (inline cursor is set by resize logic)
76
- if (cell.style.cursor === 'col-resize' || cell.style.cursor === 'row-resize') return;
77
- e.preventDefault(); // suppress text-cursor placement while selecting cells
78
- this._activeTable = cell.closest('table');
79
- this._selectStart = cell;
80
- this._selectDragging = true;
81
- this._setSelection([cell]);
82
- };
83
- const onSelMousemove = (e) => {
84
- if (!this._selectMode || !this._selectDragging || !this._selectStart) return;
85
- const cell = e.target.closest('td, th');
86
- if (!cell || !editable.contains(cell)) return;
87
- if (cell.closest('table') !== this._activeTable) return;
88
- this._setSelection(this._getRectCells(this._selectStart, cell));
89
- };
90
- const onSelMouseup = () => { this._selectDragging = false; };
91
-
92
- this._disposers.push(
93
- on(editable, 'mousedown', onSelMousedown),
94
- on(editable, 'mousemove', onSelMousemove),
95
- on(document, 'mouseup', onSelMouseup),
96
- // ──────────────────────────────────────────────────────────────────────
97
- on(editable, 'mouseover', (e) => {
98
- if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
99
- const table = /** @type {Element} */ (e.target)?.closest('table');
100
- if (table && editable.contains(table)) {
101
- const cell = /** @type {Element} */ (e.target)?.closest('td, th');
102
- if (cell) {
103
- this._activeCell = cell;
104
- this._syncShadeStrip();
105
- }
106
- this._scheduleShow(table);
107
- }
108
- }, { passive: true }),
109
- on(editable, 'mouseout', (e) => {
110
- if (this._selectMode) return; // keep tooltip alive during cell selection
111
- const to = /** @type {Node|null} */ (/** @type {MouseEvent} */ (e).relatedTarget);
112
- if (!to || (
113
- !editable.contains(to) &&
114
- !this._el.contains(to) &&
115
- !this._sizePopover?.contains(to)
116
- )) {
117
- this._scheduleHide();
118
- }
119
- }, { passive: true }),
120
- on(document, 'click', (e) => {
121
- const et = /** @type {Node} */ (e.target);
122
- if (this._selectMode && this._activeTable?.contains(et)) return;
123
- if (this._activeTable &&
124
- !this._activeTable.contains(et) &&
125
- !this._el.contains(et) &&
126
- !this._sizePopover?.contains(et) &&
127
- !this._shadePopover?.contains(et) &&
128
- !this._borderColorPopover?.contains(et)) {
129
- this._hide();
130
- }
131
- }),
132
- // Sync shade strip whenever selection moves to a different cell
133
- on(document, 'selectionchange', () => this._syncShadeStrip()),
134
- // Hide when the page scrolls or resizes — the tooltip position becomes stale
135
- on(globalThis, 'scroll', () => this._hide(), { passive: true }),
136
- on(globalThis, 'resize', () => this._hide(), { passive: true }),
137
- );
138
-
139
- this._initResize();
140
- return this;
141
- }
142
-
143
- // ---------------------------------------------------------------------------
144
- // Column & row drag-resize
145
- // ---------------------------------------------------------------------------
146
-
147
- _initResize() {
148
- const editable = this.context.layoutInfo.editable;
149
- const HIT = 6; // px proximity threshold
150
-
151
- // Shared hover state
152
- let _nearCell = null;
153
- let _nearEdge = null; // 'col' | 'row' | null
154
-
155
- // Active drag state
156
- let _resizing = false;
157
- let _edge = null; // 'col' | 'row'
158
- let _startX = 0;
159
- let _startY = 0;
160
- let _startW = 0;
161
- let _startH = 0;
162
- let _colIdx = -1;
163
- let _colCells = null; // cells cached at drag-start — avoids querySelectorAll every frame
164
- let _row = null;
165
- let _table = null;
166
- let _rafDocMove = null; // pending rAF handle for resize drag
167
- let _rafEditorMove = null; // pending rAF handle for cursor detection
168
-
169
- const clearHover = () => {
170
- if (_nearCell) { _nearCell.style.cursor = ''; _nearCell = null; }
171
- _nearEdge = null;
172
- };
173
-
174
- const onEditorMove = (e) => {
175
- if (_resizing) return;
176
- if (this.context.layoutInfo.container.classList.contains('an-disabled')) {
177
- clearHover();
178
- return;
179
- }
180
- // Throttle to one check per animation frame — getBoundingClientRect forces reflow
181
- if (_rafEditorMove !== null) return;
182
- const target = e.target;
183
- const clientX = e.clientX;
184
- const clientY = e.clientY;
185
- _rafEditorMove = requestAnimationFrame(() => {
186
- _rafEditorMove = null;
187
- const cell = target.closest('td, th');
188
- if (!cell || !editable.contains(cell)) { clearHover(); return; }
189
- if (_nearCell && _nearCell !== cell) _nearCell.style.cursor = '';
190
- const rect = cell.getBoundingClientRect();
191
- const onRight = Math.abs(clientX - rect.right) < HIT;
192
- const onBottom = Math.abs(clientY - rect.bottom) < HIT;
193
- if (onRight) {
194
- cell.style.cursor = 'col-resize';
195
- _nearCell = cell; _nearEdge = 'col';
196
- } else if (onBottom) {
197
- cell.style.cursor = 'row-resize';
198
- _nearCell = cell; _nearEdge = 'row';
199
- } else {
200
- clearHover();
201
- }
202
- });
203
- };
204
-
205
- const onEditorDown = (e) => {
206
- if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
207
- if (!_nearCell || !_nearEdge) return;
208
- _resizing = true;
209
- _edge = _nearEdge;
210
- _startX = e.clientX;
211
- _startY = e.clientY;
212
- _table = _nearCell.closest('table');
213
- if (_edge === 'col') {
214
- // Right edge of a merged cell = last spanned column, not the start column
215
- _colIdx = getVisualColIndex(_nearCell) + (_nearCell.colSpan || 1) - 1;
216
- // Cache non-merged cells at that column — merged cells cannot be individually
217
- // resized per column (F-1: colSpan > 1 distributes width across all spanned cols)
218
- _colCells = _colIdx >= 0
219
- ? Array.from(_table.querySelectorAll('tr'))
220
- .map(r => getCellAtVisualCol(r, _colIdx))
221
- .filter(Boolean)
222
- .filter(c => (c.colSpan || 1) === 1)
223
- : [];
224
- // Use the actual column width from a non-merged cell; fall back to per-column estimate
225
- _startW = _colCells.length > 0
226
- ? _colCells[0].offsetWidth
227
- : Math.max(30, Math.round(_nearCell.offsetWidth / (_nearCell.colSpan || 1)));
228
- document.body.style.cursor = 'col-resize';
229
- } else {
230
- // Bottom edge of a merged cell = last spanned row, not the cell's own <tr>
231
- const tr = _nearCell.closest('tr');
232
- const rowStart = tr ? Array.from(_table.rows).indexOf(tr) : 0;
233
- const lastRowIdx = rowStart + (_nearCell.rowSpan || 1) - 1;
234
- _row = _table.rows[lastRowIdx] || tr;
235
- _startH = _row ? _row.offsetHeight : 40;
236
- document.body.style.cursor = 'row-resize';
237
- }
238
- document.body.style.userSelect = 'none';
239
- e.preventDefault();
240
- e.stopPropagation();
241
- };
242
-
243
- const onDocMove = (e) => {
244
- if (!_resizing) return;
245
- // Skip if a frame is already scheduled — avoids per-pixel style thrashing
246
- if (_rafDocMove !== null) return;
247
- const clientX = e.clientX;
248
- const clientY = e.clientY;
249
- _rafDocMove = requestAnimationFrame(() => {
250
- _rafDocMove = null;
251
- if (_edge === 'col') {
252
- const newW = Math.max(30, _startW + (clientX - _startX));
253
- for (const c of _colCells) {
254
- c.style.width = `${newW}px`;
255
- c.style.minWidth = `${newW}px`;
256
- }
257
- } else {
258
- const newH = Math.max(20, _startH + (clientY - _startY));
259
- if (_row) {
260
- for (const c of _row.cells) {
261
- c.style.height = `${newH}px`;
262
- c.style.minHeight = `${newH}px`;
263
- }
264
- }
265
- }
266
- });
267
- };
268
-
269
- const onDocUp = () => {
270
- if (!_resizing) return;
271
- // Cancel any in-flight rAF so stale writes don't land after mouseup
272
- if (_rafDocMove !== null) { cancelAnimationFrame(_rafDocMove); _rafDocMove = null; }
273
- _resizing = false;
274
- document.body.style.userSelect = '';
275
- document.body.style.cursor = '';
276
- _edge = null;
277
- _table = null;
278
- _row = null;
279
- _colIdx = -1;
280
- _colCells = null;
281
- this.context.invoke('editor.afterCommand');
282
- };
283
-
284
- this._disposers.push(
285
- on(editable, 'mousemove', onEditorMove),
286
- on(editable, 'mousedown', onEditorDown),
287
- on(document, 'mousemove', onDocMove),
288
- on(document, 'mouseup', onDocUp),
289
- );
290
- }
291
-
292
- destroy() {
293
- this._clearTimers();
294
- this._disposers.forEach((d) => d());
295
- this._disposers = [];
296
- if (this._el?.parentNode) this._el.remove();
297
- this._el = null;
298
- if (this._sizePopover?.parentNode) {
299
- this._sizePopover.remove();
300
- }
301
- this._sizePopover = null;
302
- if (this._shadePopover?.parentNode) {
303
- this._shadePopover.remove();
304
- }
305
- this._shadePopover = null;
306
- if (this._borderColorPopover?.parentNode) {
307
- this._borderColorPopover.remove();
308
- }
309
- this._borderColorPopover = null;
310
- }
311
-
312
- // ---------------------------------------------------------------------------
313
- // Build tooltip bar
314
- // ---------------------------------------------------------------------------
315
-
316
- _buildTooltip() {
317
- const L = this.context.locale.tooltips.table;
318
- const el = createElement('div', {
319
- class: 'an-link-tooltip an-table-tooltip',
320
- role: 'toolbar',
321
- 'aria-label': L.ariaLabel,
322
- });
323
- el.style.display = 'none';
324
-
325
- // Label
326
- this._label = createElement('span', { class: 'an-link-tooltip-url' });
327
- this._label.textContent = L.label;
328
- el.appendChild(this._label);
329
-
330
- el.appendChild(this._sep());
331
-
332
- // Select-cells toggle
333
- this._selectBtn = this._makeBtn(ICONS.selectCells, L.selectCells, () => this._toggleSelectMode());
334
- el.appendChild(this._selectBtn);
335
-
336
- el.appendChild(this._sep());
337
-
338
- // Row operations
339
- el.appendChild(this._makeBtn(ICONS.rowAbove, L.addRowAbove, () => this._addRow('above')));
340
- el.appendChild(this._makeBtn(ICONS.rowBelow, L.addRowBelow, () => this._addRow('below')));
341
- el.appendChild(this._makeBtn(ICONS.deleteRow, L.deleteRow, () => this._deleteRow()));
342
-
343
- el.appendChild(this._sep());
344
-
345
- // Column operations
346
- el.appendChild(this._makeBtn(ICONS.colLeft, L.addColumnLeft, () => this._addColumn('left')));
347
- el.appendChild(this._makeBtn(ICONS.colRight, L.addColumnRight, () => this._addColumn('right')));
348
- el.appendChild(this._makeBtn(ICONS.deleteCol, L.deleteColumn, () => this._deleteColumn()));
349
- el.appendChild(this._makeBtn(ICONS.sortAsc, L.sortAsc, () => this._sortColumn('asc')));
350
- el.appendChild(this._makeBtn(ICONS.sortDesc, L.sortDesc, () => this._sortColumn('desc')));
351
-
352
- el.appendChild(this._sep());
353
-
354
- // Merge cells
355
- el.appendChild(this._makeBtn(ICONS.mergeCells, L.mergeCells, () => this._mergeCells()));
356
- el.appendChild(this._makeBtn(ICONS.unmergeCells, L.unmergeCells, () => this._unmergeCells()));
357
-
358
- el.appendChild(this._sep());
359
-
360
- // Cell text alignment
361
- el.appendChild(this._makeBtn(ICONS.alignLeft, L.cellAlignLeft, () => this._applyCellAlign('left')));
362
- el.appendChild(this._makeBtn(ICONS.alignCenter, L.cellAlignCenter, () => this._applyCellAlign('center')));
363
- el.appendChild(this._makeBtn(ICONS.alignRight, L.cellAlignRight, () => this._applyCellAlign('right')));
364
- el.appendChild(this._makeBtn(ICONS.alignJustify, L.cellAlignJustify, () => this._applyCellAlign('justify')));
365
- el.appendChild(this._makeBtn(ICONS.headerRow, L.toggleHeaderRow, () => this._toggleHeaderRow()));
366
-
367
- el.appendChild(this._sep());
368
-
369
- // Cell background shading — uses color-strip variant like foreColor/hiliteColor buttons
370
- const shadeBtn = createElement('button', {
371
- type: 'button',
372
- class: 'an-link-tooltip-btn an-link-tooltip-btn--shade',
373
- title: L.cellBackground,
374
- });
375
- const shadeSvgWrap = createElement('span', { class: 'an-bubble-btn-svg' });
376
- shadeSvgWrap.innerHTML = ICONS.cellShade;
377
- const shadeStrip = createElement('span', { class: 'an-link-tooltip-color-strip' });
378
- shadeBtn.appendChild(shadeSvgWrap);
379
- shadeBtn.appendChild(shadeStrip);
380
- this._shadeColorStrip = shadeStrip;
381
- this._disposers.push(on(shadeBtn, 'click', (e) => {
382
- e.preventDefault();
383
- e.stopPropagation();
384
- this._openCellShadePopover();
385
- }));
386
- el.appendChild(shadeBtn);
387
-
388
- el.appendChild(this._sep());
389
-
390
- // Resize
391
- el.appendChild(this._makeBtn(ICONS.colWidth, L.columnWidth, () => this._openSizePopover('col')));
392
- el.appendChild(this._makeBtn(ICONS.rowHeight, L.rowHeight, () => this._openSizePopover('row')));
393
- el.appendChild(this._makeBtn(ICONS.tableBorder,L.tableBorderWidth, () => this._openSizePopover('border')));
394
- el.appendChild(this._makeBtn(ICONS.cellPadding, L.cellPadding, () => this._openSizePopover('cellPadding')));
395
-
396
- // Table border color button — color-strip variant like shade button
397
- const borderColorBtn = createElement('button', {
398
- type: 'button',
399
- class: 'an-link-tooltip-btn an-link-tooltip-btn--shade',
400
- title: L.tableBorderColor,
401
- });
402
- const borderColorSvgWrap = createElement('span', { class: 'an-bubble-btn-svg' });
403
- borderColorSvgWrap.innerHTML = ICONS.borderColor;
404
- const borderColorStrip = createElement('span', { class: 'an-link-tooltip-color-strip' });
405
- borderColorBtn.appendChild(borderColorSvgWrap);
406
- borderColorBtn.appendChild(borderColorStrip);
407
- this._borderColorStrip = borderColorStrip;
408
- this._disposers.push(on(borderColorBtn, 'click', (e) => {
409
- e.preventDefault();
410
- e.stopPropagation();
411
- this._openBorderColorPopover();
412
- }));
413
- el.appendChild(borderColorBtn);
414
-
415
- el.appendChild(this._sep());
416
-
417
- // Delete table (danger)
418
- el.appendChild(this._makeBtn(ICONS.deleteTable, L.deleteTable, () => this._deleteTable(), true));
419
-
420
- el.appendChild(this._sep());
421
- el.appendChild(this._makeBtn(ICONS.exportCSV, L.exportCSV, () => this._exportTableCSV()));
422
-
423
- // Keep tooltip alive while hovering.
424
- // Don't schedule hide on mouseleave when the size popover is open —
425
- // the user is moving the mouse toward it.
426
- this._disposers.push(
427
- on(el, 'mouseenter', () => this._clearTimers()),
428
- on(el, 'mouseleave', () => {
429
- if (this._selectMode) return; // keep tooltip alive during cell selection
430
- if (this._sizePopover && this._sizePopover.style.display !== 'none') return;
431
- if (this._shadePopover && this._shadePopover.style.display !== 'none') return;
432
- if (this._borderColorPopover && this._borderColorPopover.style.display !== 'none') return;
433
- this._scheduleHide();
434
- }),
435
- );
436
-
437
- return el;
438
- }
439
-
440
- _sep() {
441
- return createElement('div', { class: 'an-link-tooltip-sep' });
442
- }
443
-
444
- /**
445
- * @param {string} icon
446
- * @param {string} title
447
- * @param {Function} handler
448
- * @param {boolean} [isDanger]
449
- */
450
- _makeBtn(icon, title, handler, isDanger = false) {
451
- const btn = createElement('button', {
452
- type: 'button',
453
- class: isDanger
454
- ? 'an-link-tooltip-btn an-link-tooltip-btn--danger'
455
- : 'an-link-tooltip-btn',
456
- title,
457
- });
458
- btn.innerHTML = icon;
459
- this._disposers.push(on(btn, 'click', (e) => {
460
- e.preventDefault();
461
- e.stopPropagation();
462
- handler();
463
- }));
464
- return btn;
465
- }
466
-
467
- // ---------------------------------------------------------------------------
468
- // Show / Hide
469
- // ---------------------------------------------------------------------------
470
-
471
- _scheduleShow(table) {
472
- if (this._activeTable === table && this._el.style.display !== 'none') return;
473
- clearTimeout(this._hideTimer);
474
- this._hideTimer = null;
475
- clearTimeout(this._showTimer);
476
- this._showTimer = setTimeout(() => {
477
- this._activeTable = table;
478
- this._show();
479
- }, SHOW_DELAY);
480
- }
481
-
482
- _scheduleHide() {
483
- clearTimeout(this._showTimer);
484
- this._showTimer = null;
485
- if (this._hideTimer) return;
486
- this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
487
- }
488
-
489
- _show() {
490
- if (!this._activeTable) return;
491
- this._el.style.display = 'flex';
492
- this._syncShadeStrip();
493
- this._syncBorderColorStrip();
494
- // Defer: offsetWidth on a newly-visible element forces layout synchronously
495
- requestAnimationFrame(() => {
496
- if (this._activeTable) this._positionNear(this._activeTable);
497
- });
498
- }
499
-
500
- _syncShadeStrip() {
501
- if (!this._shadeColorStrip || !this._el || this._el.style.display === 'none') return;
502
- const cell = this._getCell();
503
- this._shadeColorStrip.style.background = cell?.style.backgroundColor || 'transparent';
504
- }
505
-
506
- _hide() {
507
- this._el.style.display = 'none';
508
- this._activeTable = null;
509
- this._activeCell = null;
510
- // Reset select mode
511
- if (this._selectMode) {
512
- this._selectMode = false;
513
- if (this._selectBtn) this._selectBtn.classList.remove('an-link-tooltip-btn--active');
514
- if (this._editable) this._editable.classList.remove('an-table-select-mode');
515
- }
516
- this._clearSelection();
517
- this._clearTimers();
518
- this._hideSizePopover();
519
- this._hideBorderColorPopover();
520
- }
521
-
522
- _clearTimers() {
523
- clearTimeout(this._showTimer);
524
- clearTimeout(this._hideTimer);
525
- this._showTimer = null;
526
- this._hideTimer = null;
527
- }
528
-
529
- _positionNear(table) {
530
- if (!table) return;
531
- const rect = table.getBoundingClientRect();
532
- const tipW = this._el.offsetWidth || 400;
533
- const tipH = this._el.offsetHeight || 30;
534
- const margin = 6;
535
-
536
- // Center horizontally over the table; prefer above it
537
- let left = rect.left + (rect.width - tipW) / 2;
538
- let top = rect.top - tipH - margin;
539
-
540
- if (top < margin) top = rect.bottom + margin;
541
- if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
542
- if (left < margin) left = margin;
543
-
544
- this._el.style.left = `${left}px`;
545
- this._el.style.top = `${top}px`;
546
- }
547
-
548
- // ---------------------------------------------------------------------------
549
- // Helper: get active cell (fallback to first td/th in table)
550
- // ---------------------------------------------------------------------------
551
-
552
- _getCell() {
553
- // Prefer the cell under the current text cursor (most intuitive for operations)
554
- const sel = globalThis.getSelection();
555
- if (sel?.rangeCount) {
556
- let container = sel.getRangeAt(0).commonAncestorContainer;
557
- if (container.nodeType === 3) container = container.parentElement;
558
- const cellFromSel = /** @type {Element} */ (container)?.closest('td, th');
559
- if (cellFromSel && this._activeTable?.contains(cellFromSel)) {
560
- return cellFromSel;
561
- }
562
- }
563
- return this._activeCell
564
- || this._activeTable?.querySelector('td, th');
565
- }
566
-
567
- // ---------------------------------------------------------------------------
568
- // Cell selection helpers
569
- // ---------------------------------------------------------------------------
570
-
571
- _toggleSelectMode() {
572
- this._selectMode = !this._selectMode;
573
- if (this._selectBtn) {
574
- this._selectBtn.classList.toggle('an-link-tooltip-btn--active', this._selectMode);
575
- }
576
- if (this._editable) {
577
- this._editable.classList.toggle('an-table-select-mode', this._selectMode);
578
- }
579
- if (!this._selectMode) {
580
- this._clearSelection();
581
- }
582
- }
583
-
584
- _clearSelection() {
585
- this._selectedCells.forEach((c) => c.classList.remove('an-cell-selected'));
586
- this._selectedCells = [];
587
- this._selectStart = null;
588
- }
589
-
590
- _setSelection(cells) {
591
- // Remove highlight from cells no longer in selection
592
- this._selectedCells.forEach((c) => {
593
- if (!cells.includes(c)) c.classList.remove('an-cell-selected');
594
- });
595
- this._selectedCells = cells;
596
- cells.forEach((c) => c.classList.add('an-cell-selected'));
597
- }
598
-
599
- /**
600
- * Returns all cells in the rectangular area between startCell and endCell,
601
- * correctly handling rowspan/colspan by using the grid map.
602
- * The rect is expanded iteratively until it is stable — this ensures any
603
- * merged cell that starts outside the initial rect but spans into it is
604
- * fully included.
605
- */
606
- _getRectCells(startCell, endCell) {
607
- if (!startCell) return [];
608
- if (!endCell || startCell === endCell) return [startCell];
609
- const table = startCell.closest('table');
610
- if (!table?.contains(endCell)) return [startCell];
611
-
612
- const { gridMap, cellPos } = buildGridMap(table);
613
- const sp = cellPos.get(startCell);
614
- const ep = cellPos.get(endCell);
615
- if (!sp || !ep) return [startCell];
616
-
617
- let minR = Math.min(sp.r, ep.r);
618
- let maxR = Math.max(sp.r + sp.rs - 1, ep.r + ep.rs - 1);
619
- let minC = Math.min(sp.c, ep.c);
620
- let maxC = Math.max(sp.c + sp.cs - 1, ep.c + ep.cs - 1);
621
-
622
- // Iteratively expand until stable — handles spans that exceed the current rect
623
- let changed = true;
624
- while (changed) {
625
- changed = false;
626
- for (let r = minR; r <= maxR; r++) {
627
- const rowMap = gridMap[r];
628
- if (!rowMap) continue;
629
- for (let c = minC; c <= maxC; c++) {
630
- const cell = rowMap[c];
631
- if (!cell) continue;
632
- const pos = cellPos.get(cell);
633
- if (!pos) continue;
634
- if (pos.r < minR) { minR = pos.r; changed = true; }
635
- if (pos.r + pos.rs - 1 > maxR) { maxR = pos.r + pos.rs - 1; changed = true; }
636
- if (pos.c < minC) { minC = pos.c; changed = true; }
637
- if (pos.c + pos.cs - 1 > maxC) { maxC = pos.c + pos.cs - 1; changed = true; }
638
- }
639
- }
640
- }
641
-
642
- // Collect unique cells in document order
643
- const seen = new Set();
644
- const result = [];
645
- for (let r = minR; r <= maxR; r++) {
646
- const rowMap = gridMap[r];
647
- if (!rowMap) continue;
648
- for (let c = minC; c <= maxC; c++) {
649
- const cell = rowMap[c];
650
- if (cell && !seen.has(cell)) {
651
- seen.add(cell);
652
- result.push(cell);
653
- }
654
- }
655
- }
656
- return result.length > 0 ? result : [startCell];
657
- }
658
-
659
- /**
660
- * Returns the active cell set: user-selected cells when available,
661
- * otherwise the single active/cursor cell.
662
- * @returns {HTMLTableCellElement[]}
663
- */
664
- _getSelectedCells() {
665
- return this._selectedCells.length > 0
666
- ? this._selectedCells
667
- : [this._getCell()].filter(Boolean);
668
- }
669
-
670
- // ---------------------------------------------------------------------------
671
- // Table operations
672
- // ---------------------------------------------------------------------------
673
-
674
- _addRow(position) {
675
- const cells = this._getSelectedCells();
676
- if (!cells.length) return;
677
- const table = cells[0].closest('table');
678
- if (!table) return;
679
- const allRows = Array.from(table.querySelectorAll('tr'));
680
- const selectedRows = [...new Set(cells.map((c) => c.closest('tr')).filter(Boolean))];
681
- // Reference row: topmost for 'above', bottommost for 'below'
682
- const refRow = selectedRows.reduce((best, r) => {
683
- const bi = allRows.indexOf(best);
684
- const ri = allRows.indexOf(r);
685
- if (position === 'above') return ri < bi ? r : best;
686
- return ri > bi ? r : best;
687
- }, selectedRows[0]);
688
- const colCount = Array.from(refRow.cells).reduce((sum, c) => sum + (c.colSpan || 1), 0);
689
- const newRow = document.createElement('tr');
690
- const refCells = Array.from(refRow.cells);
691
- for (let i = 0; i < colCount; i++) {
692
- const td = createElement('td', {}, ['\u00a0']);
693
- const ref = refCells[i];
694
- if (ref?.style.width) td.style.width = ref.style.width;
695
- if (ref?.style.minWidth) td.style.minWidth = ref.style.minWidth;
696
- newRow.appendChild(td);
697
- }
698
- if (position === 'above') refRow.parentElement?.insertBefore(newRow, refRow);
699
- else refRow.after(newRow);
700
- requestAnimationFrame(() => this._positionNear(this._activeTable));
701
- this.context.invoke('editor.afterCommand');
702
- }
703
-
704
- _addColumn(position) {
705
- const cells = this._getSelectedCells();
706
- if (!cells.length) return;
707
- const table = cells[0].closest('table');
708
- if (!table) return;
709
- const colIndices = cells.map((c) => getVisualColIndex(c));
710
- const targetColIdx = position === 'left' ? Math.min(...colIndices) : Math.max(...colIndices);
711
- const rows = Array.from(table.querySelectorAll('tr'));
712
- const refs = rows.map((r) => position === 'left'
713
- ? getCellAtVisualCol(r, targetColIdx)
714
- : getCellAfterVisualCol(r, targetColIdx));
715
- const isHeaders = rows.map((r) => r.closest('thead') !== null);
716
- rows.forEach((r, i) => {
717
- r.insertBefore(createElement(isHeaders[i] ? 'th' : 'td', {}, ['\u00a0']), refs[i]);
718
- });
719
- requestAnimationFrame(() => this._positionNear(this._activeTable));
720
- this.context.invoke('editor.afterCommand');
721
- }
722
-
723
- _deleteRow() {
724
- const cells = this._getSelectedCells();
725
- if (!cells.length) return;
726
- const table = cells[0].closest('table');
727
- if (!table) return;
728
- const tbody = table.querySelector('tbody');
729
- const totalBodyRows = tbody
730
- ? tbody.querySelectorAll('tr').length
731
- : table.querySelectorAll('tr').length;
732
- const selectedRows = [...new Set(cells.map((c) => c.closest('tr')).filter(Boolean))];
733
- const bodyRowsToDelete = selectedRows.filter((r) => r.closest('tbody'));
734
- // Guard: keep at least one body row
735
- if (bodyRowsToDelete.length >= totalBodyRows) return;
736
- this._activeCell = null;
737
- this._clearSelection();
738
- selectedRows.forEach((r) => r.remove());
739
- requestAnimationFrame(() => this._positionNear(this._activeTable));
740
- this.context.invoke('editor.afterCommand');
741
- }
742
-
743
- _deleteColumn() {
744
- const cells = this._getSelectedCells();
745
- if (!cells.length) return;
746
- const table = cells[0].closest('table');
747
- if (!table) return;
748
- const tableRows = Array.from(table.querySelectorAll('tr'));
749
- if (tableRows[0] && tableRows[0].cells.length <= 1) return; // guard: keep ≥1 col
750
- const colIndices = [...new Set(cells.map((c) => getVisualColIndex(c)))];
751
- if (colIndices.length >= (tableRows[0]?.cells.length ?? 1)) return;
752
- // Collect all cell references upfront before any removal (avoids stale visual indices)
753
- const cellsToDelete = [];
754
- colIndices.forEach((colIdx) => {
755
- tableRows.forEach((r) => {
756
- const c = getCellAtVisualCol(r, colIdx);
757
- if (c) cellsToDelete.push(c);
758
- });
759
- });
760
- this._activeCell = null;
761
- this._clearSelection();
762
- cellsToDelete.forEach((c) => c.remove());
763
- requestAnimationFrame(() => this._positionNear(this._activeTable));
764
- this.context.invoke('editor.afterCommand');
765
- }
766
-
767
- _mergeCells() {
768
- const cell = this._getCell();
769
- if (!cell) return;
770
- const table = cell.closest('table');
771
- if (!table) return;
772
-
773
- // Prefer user panel-selected cells; fall back to text-selection range
774
- let selected = this._getSelectedCells().filter((c) => table.contains(c));
775
- if (selected.length < 2) {
776
- const sel = globalThis.getSelection();
777
- if (!sel || sel.rangeCount === 0) return;
778
- const range = sel.getRangeAt(0);
779
- const allCells = Array.from(table.querySelectorAll('td, th'));
780
- selected = allCells.filter((c) => {
781
- try { return range.intersectsNode(c); } catch { return false; }
782
- });
783
- if (selected.length < 2) return;
784
- }
785
-
786
- const { gridMap, cellPos } = buildGridMap(table);
787
-
788
- // Bounding rect that encompasses every selected cell's full span
789
- let minR = Infinity, maxR = -Infinity, minC = Infinity, maxC = -Infinity;
790
- selected.forEach((c) => {
791
- const pos = cellPos.get(c);
792
- if (!pos) return;
793
- if (pos.r < minR) minR = pos.r;
794
- if (pos.r + pos.rs - 1 > maxR) maxR = pos.r + pos.rs - 1;
795
- if (pos.c < minC) minC = pos.c;
796
- if (pos.c + pos.cs - 1 > maxC) maxC = pos.c + pos.cs - 1;
797
- });
798
- if (minR === Infinity) return;
799
-
800
- // Collect every unique cell in the bounding rect
801
- const seen = new Set();
802
- const rectCells = [];
803
- for (let r = minR; r <= maxR; r++) {
804
- const rowMap = gridMap[r];
805
- if (!rowMap) continue;
806
- for (let c = minC; c <= maxC; c++) {
807
- const tc = rowMap[c];
808
- if (tc && !seen.has(tc)) { seen.add(tc); rectCells.push(tc); }
809
- }
810
- }
811
- if (rectCells.length < 2) return;
812
-
813
- const first = rectCells[0];
814
- first.colSpan = maxC - minC + 1;
815
- first.rowSpan = maxR - minR + 1;
816
- first.style.verticalAlign = 'middle';
817
- first.style.height = '';
818
- first.style.minHeight = '';
819
-
820
- // Merge content: collect non-empty inner HTML, join with <br> separator
821
- const parts = rectCells
822
- .map((c) => c.innerHTML.replace(/^(<br\s*\/?>|\s)+$/i, '').trim())
823
- .filter((h) => h !== '');
824
- first.innerHTML = parts.length ? parts.join('<br>') : '<br>';
825
-
826
- rectCells.slice(1).forEach((c) => {
827
- const row = c.parentElement;
828
- c.remove();
829
- // Remove the parent <tr> if it is now completely empty
830
- if (row && row.cells.length === 0) row.remove();
831
- });
832
-
833
- this._clearSelection();
834
- this.context.invoke('editor.afterCommand');
835
- }
836
-
837
- _deleteTable() {
838
- const table = this._activeTable;
839
- if (!table) return;
840
- this._hide();
841
- if (table.parentNode) table.remove();
842
- this.context.invoke('editor.afterCommand');
843
- }
844
-
845
- _unmergeCells() {
846
- const cells = this._getSelectedCells();
847
- if (!cells.length) return;
848
- const table = cells[0].closest('table');
849
- if (!table) return;
850
- const mergedCells = cells.filter(
851
- (c) => table.contains(c) && ((c.colSpan || 1) > 1 || (c.rowSpan || 1) > 1)
852
- );
853
- if (!mergedCells.length) return;
854
- mergedCells.forEach((cell) => {
855
- if (table.contains(cell)) this._unmergeOne(cell, table);
856
- });
857
- this._clearSelection();
858
- requestAnimationFrame(() => this._positionNear(this._activeTable));
859
- this.context.invoke('editor.afterCommand');
860
- }
861
-
862
- /**
863
- * Split a single merged cell (colspan/rowspan > 1) back into individual cells.
864
- * New cells are empty (&nbsp;); the original cell retains its content.
865
- * @param {HTMLTableCellElement} cell
866
- * @param {HTMLTableElement} table
867
- */
868
- _unmergeOne(cell, table) {
869
- const cs = cell.colSpan || 1;
870
- const rs = cell.rowSpan || 1;
871
- if (cs === 1 && rs === 1) return;
872
-
873
- // Build grid map from current DOM state (before any mutation)
874
- const { cellPos } = buildGridMap(table);
875
- const pos = cellPos.get(cell);
876
- if (!pos) return;
877
-
878
- const { r, c } = pos;
879
- const tableRows = Array.from(table.rows);
880
- const tag = cell.tagName.toLowerCase(); // preserve td / th
881
-
882
- // Reset the original cell
883
- cell.rowSpan = 1;
884
- cell.colSpan = 1;
885
- cell.style.verticalAlign = '';
886
-
887
- // Same row: insert (cs - 1) sibling cells after the original cell
888
- if (cs > 1) {
889
- const insertRef = cell.nextElementSibling;
890
- for (let dc = 1; dc < cs; dc++) {
891
- tableRows[r].insertBefore(createElement(tag, {}, ['\u00a0']), insertRef);
892
- }
893
- }
894
-
895
- // Rows below: insert cs cells at the correct visual column position (for rowspan)
896
- for (let dr = 1; dr < rs; dr++) {
897
- const targetRow = tableRows[r + dr];
898
- if (!targetRow) continue;
899
- // Find the first cell in this row whose visual-col origin is beyond c
900
- // (using cellPos built before mutations — valid for these untouched rows)
901
- let ref = null;
902
- for (const tc of targetRow.cells) {
903
- const tp = cellPos.get(tc);
904
- if (tp?.c > c) { ref = tc; break; }
905
- }
906
- for (let dc = 0; dc < cs; dc++) {
907
- targetRow.insertBefore(createElement(tag, {}, ['\u00a0']), ref);
908
- }
909
- }
910
- }
911
-
912
- // ---------------------------------------------------------------------------
913
- // Size popover (column width / row height)
914
- // ---------------------------------------------------------------------------
915
-
916
- _buildSizePopover() {
917
- const popover = createElement('div', { class: 'an-size-popover' });
918
- popover.style.display = 'none';
919
-
920
- const titleEl = createElement('div', { class: 'an-size-popover-title' });
921
- const body = createElement('div', { class: 'an-size-popover-body' });
922
- const inputEl = /** @type {HTMLInputElement} */ (createElement('input', {
923
- type: 'number', class: 'an-size-input', min: '1', max: '2000', step: '1',
924
- }));
925
- const unitEl = createElement('span', { class: 'an-size-unit' }, ['px']);
926
- body.appendChild(inputEl);
927
- body.appendChild(unitEl);
928
-
929
- const actionsEl = createElement('div', { class: 'an-size-popover-actions' });
930
- const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
931
- cancelBtn.textContent = this.context.locale.tooltips.table.cancelBtn;
932
- const applyBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
933
- applyBtn.textContent = this.context.locale.tooltips.table.applyBtn;
934
- actionsEl.appendChild(cancelBtn);
935
- actionsEl.appendChild(applyBtn);
936
-
937
- popover.appendChild(titleEl);
938
- popover.appendChild(body);
939
- popover.appendChild(actionsEl);
940
-
941
- this._sizeTitleEl = titleEl;
942
- this._sizeInputEl = inputEl;
943
- this._sizeApply = null;
944
-
945
- const d1 = on(applyBtn, 'click', () => {
946
- const val = Number.parseInt(this._sizeInputEl.value, 10);
947
- if (val >= 0 && typeof this._sizeApply === 'function') this._sizeApply(val);
948
- this._hideSizePopover();
949
- });
950
- const d2 = on(cancelBtn, 'click', () => this._hideSizePopover());
951
- const d3 = on(inputEl, 'keydown', (e) => {
952
- const ke = /** @type {KeyboardEvent} */ (e);
953
- if (ke.key === 'Enter') { e.preventDefault(); applyBtn.click(); }
954
- if (ke.key === 'Escape') this._hideSizePopover();
955
- });
956
- const d4 = on(document, 'click', (e) => {
957
- const et = /** @type {Node} */ (e.target);
958
- if (this._sizePopover &&
959
- this._sizePopover.style.display !== 'none' &&
960
- !this._sizePopover.contains(et) &&
961
- !this._el.contains(et)) {
962
- this._hideSizePopover();
963
- }
964
- });
965
- // Keep the tooltip/popover alive while the mouse is over the popover.
966
- const d5 = on(popover, 'mouseenter', () => this._clearTimers());
967
- const d6 = on(popover, 'mouseleave', () => this._scheduleHide());
968
- this._disposers.push(d1, d2, d3, d4, d5, d6);
969
- return popover;
970
- }
971
-
972
- _openSizePopover(type) {
973
- const cell = this._getCell();
974
- if (!cell || !this._sizePopover) return;
975
-
976
- if (type === 'border') {
977
- const table = cell.closest('table');
978
- if (!table) return;
979
- const firstCell = table.querySelector('td, th');
980
- const currentPx = firstCell
981
- ? (Number.parseInt(firstCell.style.borderWidth, 10) ||
982
- Number.parseInt(globalThis.getComputedStyle(firstCell).borderWidth, 10) || 1)
983
- : 1;
984
- this._sizeTitleEl.textContent = this.context.locale.tooltips.table.tableBorderWidthPx;
985
- this._sizeInputEl.min = '0';
986
- this._sizeInputEl.max = '10';
987
- this._sizeInputEl.value = String(currentPx);
988
- this._sizeApply = (val) => {
989
- const cells = Array.from(table.querySelectorAll('td, th'));
990
- if (val === 0) {
991
- cells.forEach((c) => { c.style.borderWidth = '0'; c.style.borderStyle = 'none'; });
992
- } else {
993
- cells.forEach((c) => { c.style.borderWidth = `${val}px`; c.style.borderStyle = 'solid'; });
994
- }
995
- this.context.invoke('editor.afterCommand');
996
- };
997
- } else if (type === 'cellPadding') {
998
- const cells = this._getSelectedCells();
999
- const firstCell = cells[0] || this._getCell();
1000
- const currentPad = firstCell
1001
- ? (Number.parseInt(firstCell.style.padding, 10) ||
1002
- Number.parseInt(firstCell.style.paddingTop, 10) || 4)
1003
- : 4;
1004
- this._sizeTitleEl.textContent = this.context.locale.tooltips.table.cellPaddingPx;
1005
- this._sizeInputEl.min = '0';
1006
- this._sizeInputEl.max = '40';
1007
- this._sizeInputEl.value = String(currentPad);
1008
- this._sizeApply = (val) => {
1009
- const activeCells = this._getSelectedCells();
1010
- activeCells.forEach((c) => { if (c) c.style.padding = `${val}px`; });
1011
- this.context.invoke('editor.afterCommand');
1012
- };
1013
- } else {
1014
- const isCol = type === 'col';
1015
- const activeCells = this._getSelectedCells().filter((c) => {
1016
- const t = c.closest('table');
1017
- return t && t === cell.closest('table');
1018
- });
1019
- this._sizeTitleEl.textContent = isCol
1020
- ? this.context.locale.tooltips.table.columnWidthPx
1021
- : this.context.locale.tooltips.table.rowHeightPx;
1022
- this._sizeInputEl.min = '1';
1023
- this._sizeInputEl.max = '2000';
1024
- if (isCol) {
1025
- this._sizeInputEl.value = cell.offsetWidth || 120;
1026
- } else {
1027
- const refRow = cell.closest('tr');
1028
- this._sizeInputEl.value = refRow ? (refRow.offsetHeight || 40) : 40;
1029
- }
1030
- this._sizeApply = (val) => {
1031
- const table = cell.closest('table');
1032
- if (!table) return;
1033
- if (isCol) {
1034
- // Apply to all selected columns (or just the current column)
1035
- const colIndices = [...new Set(activeCells.map((c) => getVisualColIndex(c)))];
1036
- const tableRows = Array.from(table.querySelectorAll('tr'));
1037
- colIndices.forEach((colIdx) => {
1038
- tableRows.forEach((r) => {
1039
- const c = getCellAtVisualCol(r, colIdx);
1040
- // F-1: skip merged cells — same reason as drag-resize _colCells.
1041
- if (c && (c.colSpan || 1) === 1) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
1042
- });
1043
- });
1044
- } else {
1045
- // Apply to all selected rows (or just the current row)
1046
- const selectedRows = [...new Set(activeCells.map((c) => c.closest('tr')).filter(Boolean))];
1047
- selectedRows.forEach((row) => {
1048
- for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
1049
- });
1050
- }
1051
- this.context.invoke('editor.afterCommand');
1052
- };
1053
- }
1054
-
1055
- this._sizePopover.style.display = 'block';
1056
- requestAnimationFrame(() => {
1057
- if (!this._sizePopover || !this._el) return;
1058
- const tipRect = this._el.getBoundingClientRect();
1059
- const pw = this._sizePopover.offsetWidth || 220;
1060
- const ph = this._sizePopover.offsetHeight || 110;
1061
- let left = tipRect.left;
1062
- let top = tipRect.bottom + 6;
1063
- if (left + pw > globalThis.innerWidth - 8) left = globalThis.innerWidth - pw - 8;
1064
- if (top + ph > globalThis.innerHeight - 8) top = tipRect.top - ph - 6;
1065
- this._sizePopover.style.left = `${left}px`;
1066
- this._sizePopover.style.top = `${top}px`;
1067
- if (this._sizeInputEl) { this._sizeInputEl.focus(); this._sizeInputEl.select(); }
1068
- });
1069
- }
1070
-
1071
- _hideSizePopover() {
1072
- if (this._sizePopover) this._sizePopover.style.display = 'none';
1073
- this._sizeApply = null;
1074
- }
1075
-
1076
- // ---------------------------------------------------------------------------
1077
- // Colour popovers (cell shading + table border colour)
1078
- // ---------------------------------------------------------------------------
1079
-
1080
- /**
1081
- * Builds one of the two colour popovers. Both are the same widget — a preset
1082
- * palette, a "no colour" reset row, and a custom `<input type="color">` — and
1083
- * they were previously two near-identical 55-line methods, so a fix applied
1084
- * to one silently missed the other. They differ only in the swatch the custom
1085
- * input starts on and in what the chosen colour is applied to.
1086
- *
1087
- * `getPopover` is read lazily rather than closed over: `destroy()` nulls the
1088
- * field, and the deferred handlers must see that instead of holding the
1089
- * detached element alive.
1090
- *
1091
- * @param {object} config
1092
- * @param {string} config.defaultColor - initial value of the custom input
1093
- * @param {(color: string) => void} config.apply - called with '' to clear
1094
- * @param {() => HTMLElement|null} config.getPopover
1095
- * @param {() => void} config.hide
1096
- * @returns {{ el: HTMLElement, titleEl: HTMLElement, noColorBtn: HTMLElement }}
1097
- */
1098
- _buildColorPopover({ defaultColor, apply, getPopover, hide }) {
1099
- const pop = createElement('div', { class: 'an-cell-shade-popover' });
1100
- pop.style.display = 'none';
1101
-
1102
- const titleEl = createElement('div', { class: 'an-size-popover-title' });
1103
- pop.appendChild(titleEl);
1104
-
1105
- // 24-color palette — reuse existing CSS classes
1106
- const palette = createElement('div', { class: 'an-context-color-palette' });
1107
- SHADE_PRESETS.forEach((color) => {
1108
- const sw = createElement('div', { class: 'an-context-color-swatch', title: color });
1109
- sw.style.background = color;
1110
- this._disposers.push(on(sw, 'click', (e) => {
1111
- e.stopPropagation();
1112
- apply(color);
1113
- }));
1114
- palette.appendChild(sw);
1115
- });
1116
- pop.appendChild(palette);
1117
-
1118
- // Reset row — clears the colour
1119
- const noColorRow = createElement('div', { class: 'an-context-color-custom' });
1120
- const noColorBtn = createElement('button', { type: 'button', class: 'an-shade-no-color' });
1121
- this._disposers.push(on(noColorBtn, 'click', () => apply('')));
1122
- noColorRow.appendChild(noColorBtn);
1123
- pop.appendChild(noColorRow);
1124
-
1125
- // Custom color input
1126
- const customRow = createElement('div', { class: 'an-context-color-custom' });
1127
- const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', class: 'an-shade-color-input', value: defaultColor }));
1128
- const customLabel = createElement('span');
1129
- customLabel.textContent = this.context.locale.contextMenu.customColorLabel;
1130
- this._disposers.push(on(colorInput, 'change', () => apply(colorInput.value)));
1131
- customRow.appendChild(colorInput);
1132
- customRow.appendChild(customLabel);
1133
- pop.appendChild(customRow);
1134
-
1135
- // Prevent mousedown from collapsing editor selection; keep tooltip alive while hovering
1136
- this._disposers.push(
1137
- on(pop, 'mousedown', (e) => e.preventDefault()),
1138
- on(pop, 'mouseenter', () => this._clearTimers()),
1139
- on(pop, 'mouseleave', () => this._scheduleHide()),
1140
- // Close on outside click
1141
- on(document, 'click', (e) => {
1142
- const et = /** @type {Node} */ (e.target);
1143
- const current = getPopover();
1144
- if (current &&
1145
- current.style.display !== 'none' &&
1146
- !current.contains(et) &&
1147
- !this._el?.contains(et)) {
1148
- hide();
1149
- }
1150
- }),
1151
- );
1152
-
1153
- return { el: pop, titleEl, noColorBtn };
1154
- }
1155
-
1156
- /**
1157
- * Shows a colour popover under the tooltip, flipping above it when there is
1158
- * no room below. Measurement has to wait a frame because the popover is
1159
- * display:none until now and would otherwise report zero size.
1160
- * @param {() => HTMLElement|null} getPopover
1161
- */
1162
- _showColorPopover(getPopover) {
1163
- const pop = getPopover();
1164
- if (!pop) return;
1165
- pop.style.display = 'block';
1166
- requestAnimationFrame(() => {
1167
- const current = getPopover();
1168
- if (!current || !this._el) return;
1169
- const pw = current.offsetWidth || 170;
1170
- const ph = current.offsetHeight || 120;
1171
- const tipRect = this._el.getBoundingClientRect();
1172
- let left = tipRect.left;
1173
- let top = tipRect.bottom + 6;
1174
- if (left + pw > globalThis.innerWidth - 8) left = globalThis.innerWidth - pw - 8;
1175
- if (top + ph > globalThis.innerHeight - 8) top = tipRect.top - ph - 6;
1176
- current.style.left = `${Math.max(8, left)}px`;
1177
- current.style.top = `${Math.max(8, top)}px`;
1178
- });
1179
- }
1180
-
1181
- // --- Cell background shade ---
1182
-
1183
- _buildCellShadePopover() {
1184
- const { el, titleEl, noColorBtn } = this._buildColorPopover({
1185
- defaultColor: '#ffffff',
1186
- apply: (color) => this._applyCellShade(color),
1187
- getPopover: () => this._shadePopover,
1188
- hide: () => this._hideCellShadePopover(),
1189
- });
1190
- this._shadeTitleEl = titleEl;
1191
- this._shadeNoBtn = noColorBtn;
1192
- return el;
1193
- }
1194
-
1195
- _openCellShadePopover() {
1196
- if (!this._shadePopover) return;
1197
- const L = this.context.locale.tooltips.table;
1198
- if (this._shadeTitleEl) this._shadeTitleEl.textContent = L.cellBackground;
1199
- if (this._shadeNoBtn) this._shadeNoBtn.textContent = L.noShading;
1200
- this._showColorPopover(() => this._shadePopover);
1201
- }
1202
-
1203
- _hideCellShadePopover() {
1204
- if (this._shadePopover) this._shadePopover.style.display = 'none';
1205
- }
1206
-
1207
- _applyCellShade(color) {
1208
- const cells = this._selectMode ? this._selectedCells : [this._getCell()];
1209
- cells.forEach((cell) => {
1210
- if (cell) cell.style.backgroundColor = color;
1211
- });
1212
- // Update the color strip on the shade button to reflect the applied color
1213
- if (this._shadeColorStrip) {
1214
- this._shadeColorStrip.style.background = color || 'transparent';
1215
- }
1216
- this._hideCellShadePopover();
1217
- this.context.invoke('editor.afterCommand');
1218
- }
1219
-
1220
- // --- Table border colour ---
1221
-
1222
- _buildBorderColorPopover() {
1223
- const { el, titleEl, noColorBtn } = this._buildColorPopover({
1224
- defaultColor: '#000000',
1225
- apply: (color) => this._applyBorderColor(color),
1226
- getPopover: () => this._borderColorPopover,
1227
- hide: () => this._hideBorderColorPopover(),
1228
- });
1229
- this._borderColorTitleEl = titleEl;
1230
- this._borderColorNoBtn = noColorBtn;
1231
- return el;
1232
- }
1233
-
1234
- _openBorderColorPopover() {
1235
- if (!this._borderColorPopover) return;
1236
- const L = this.context.locale.tooltips.table;
1237
- if (this._borderColorTitleEl) this._borderColorTitleEl.textContent = L.tableBorderColor;
1238
- if (this._borderColorNoBtn) this._borderColorNoBtn.textContent = L.noBorderColor;
1239
- this._showColorPopover(() => this._borderColorPopover);
1240
- }
1241
-
1242
- _hideBorderColorPopover() {
1243
- if (this._borderColorPopover) this._borderColorPopover.style.display = 'none';
1244
- }
1245
-
1246
- _applyBorderColor(color) {
1247
- const table = this._activeTable;
1248
- if (!table) return;
1249
- Array.from(table.querySelectorAll('td, th')).forEach((c) => { c.style.borderColor = color; });
1250
- if (this._borderColorStrip) {
1251
- this._borderColorStrip.style.background = color || 'transparent';
1252
- }
1253
- this._hideBorderColorPopover();
1254
- this.context.invoke('editor.afterCommand');
1255
- }
1256
-
1257
- _syncBorderColorStrip() {
1258
- if (!this._borderColorStrip || !this._el || this._el.style.display === 'none') return;
1259
- const firstCell = this._activeTable?.querySelector('td, th');
1260
- this._borderColorStrip.style.background = firstCell?.style.borderColor || 'transparent';
1261
- }
1262
-
1263
- // ---------------------------------------------------------------------------
1264
- // Cell text alignment
1265
- // ---------------------------------------------------------------------------
1266
-
1267
- _applyCellAlign(align) {
1268
- const cells = this._getSelectedCells();
1269
- cells.forEach((c) => { if (c) c.style.textAlign = align; });
1270
- this.context.invoke('editor.afterCommand');
1271
- }
1272
-
1273
- // ---------------------------------------------------------------------------
1274
- // Toggle header row
1275
- // ---------------------------------------------------------------------------
1276
-
1277
- _toggleHeaderRow() {
1278
- const table = this._activeTable;
1279
- if (!table) return;
1280
- const firstRow = table.querySelector('tr');
1281
- if (!firstRow) return;
1282
-
1283
- const isInThead = firstRow.closest('thead') !== null;
1284
-
1285
- if (isInThead) {
1286
- // Convert th → td and move row to tbody
1287
- let tbody = table.querySelector('tbody');
1288
- if (!tbody) {
1289
- tbody = document.createElement('tbody');
1290
- table.appendChild(tbody);
1291
- }
1292
- Array.from(firstRow.cells).forEach((cell) => {
1293
- const td = document.createElement('td');
1294
- td.innerHTML = cell.innerHTML;
1295
- td.style.cssText = cell.style.cssText;
1296
- firstRow.replaceChild(td, cell);
1297
- });
1298
- tbody.insertBefore(firstRow, tbody.firstChild);
1299
- const thead = table.querySelector('thead');
1300
- if (thead && thead.rows.length === 0) thead.remove();
1301
- } else {
1302
- // Convert td → th and move row to thead
1303
- let thead = table.querySelector('thead');
1304
- if (!thead) {
1305
- thead = document.createElement('thead');
1306
- table.insertBefore(thead, table.firstChild);
1307
- }
1308
- Array.from(firstRow.cells).forEach((cell) => {
1309
- const th = document.createElement('th');
1310
- th.innerHTML = cell.innerHTML;
1311
- th.style.cssText = cell.style.cssText;
1312
- firstRow.replaceChild(th, cell);
1313
- });
1314
- thead.appendChild(firstRow);
1315
- }
1316
-
1317
- this.context.invoke('editor.afterCommand');
1318
- }
1319
-
1320
- _sortColumn(direction) {
1321
- const table = this._activeTable;
1322
- if (!table) return;
1323
- const cell = this._getCell();
1324
- if (!cell) return;
1325
- const colIdx = getVisualColIndex(cell);
1326
- if (colIdx === -1) return;
1327
- const tbody = table.querySelector('tbody') || table;
1328
- const rows = Array.from(tbody.querySelectorAll(':scope > tr'));
1329
- if (rows.length < 2) return;
1330
- rows.sort((a, b) => {
1331
- const aCell = getCellAtVisualCol(a, colIdx);
1332
- const bCell = getCellAtVisualCol(b, colIdx);
1333
- const aText = (aCell?.textContent || '').trim();
1334
- const bText = (bCell?.textContent || '').trim();
1335
- const aNum = parseFloat(aText);
1336
- const bNum = parseFloat(bText);
1337
- if (!isNaN(aNum) && !isNaN(bNum)) {
1338
- return direction === 'asc' ? aNum - bNum : bNum - aNum;
1339
- }
1340
- return direction === 'asc'
1341
- ? aText.localeCompare(bText)
1342
- : bText.localeCompare(aText);
1343
- });
1344
- rows.forEach((row) => tbody.appendChild(row));
1345
- this._markSortIndicator(table, colIdx, direction);
1346
- this.context.invoke('editor.afterCommand');
1347
- }
1348
-
1349
- /**
1350
- * Marks the header cell of the sorted column with `an-sort-asc`/`an-sort-desc`
1351
- * so the active sort column and direction are visible, and clears any previous
1352
- * indicator. No-op for tables without a `<thead>` (no header row to mark).
1353
- * @param {HTMLTableElement} table
1354
- * @param {number} colIdx
1355
- * @param {'asc'|'desc'} direction
1356
- */
1357
- _markSortIndicator(table, colIdx, direction) {
1358
- const thead = table.querySelector('thead');
1359
- if (!thead) return;
1360
- thead.querySelectorAll('.an-sort-asc, .an-sort-desc').forEach((el) => {
1361
- el.classList.remove('an-sort-asc', 'an-sort-desc');
1362
- });
1363
- const headerRow = thead.querySelector('tr');
1364
- if (!headerRow) return;
1365
- const headerCell = getCellAtVisualCol(headerRow, colIdx);
1366
- if (headerCell) headerCell.classList.add(direction === 'asc' ? 'an-sort-asc' : 'an-sort-desc');
1367
- }
1368
-
1369
- _exportTableCSV() {
1370
- const table = this._activeTable;
1371
- if (!table) return;
1372
- const rows = Array.from(table.querySelectorAll('tr'));
1373
- const csv = rows.map((row) =>
1374
- Array.from(row.querySelectorAll('td, th'))
1375
- .map((cell) => {
1376
- const text = (cell.textContent || '').trim().replace(/"/g, '""');
1377
- return `"${text}"`;
1378
- })
1379
- .join(',')
1380
- ).join('\n');
1381
- const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' });
1382
- const url = URL.createObjectURL(blob);
1383
- const a = document.createElement('a');
1384
- a.href = url;
1385
- a.download = 'table.csv';
1386
- a.style.display = 'none';
1387
- document.body.appendChild(a);
1388
- a.click();
1389
- a.remove();
1390
- URL.revokeObjectURL(url);
1391
- }
1392
- }