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