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.
- package/README.md +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +661 -798
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +13 -4
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -25
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -231
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -312
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1521
- package/src/js/module/Toolbar.js +0 -750
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- 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 ( ); 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
|
-
}
|