autumnnote 2.0.0 → 2.1.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/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +633 -536
- 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/package.json +2 -2
- package/src/js/core/env.js +23 -10
- package/src/js/core/sanitise.js +81 -8
- package/src/js/index.js +1 -1
- package/src/js/module/BaseResizer.js +19 -9
- package/src/js/module/TableTooltip.js +91 -220
- package/src/js/module/Toolbar.js +107 -2
- package/src/js/module/table-grid.js +102 -0
- package/src/js/module/table-icons.js +33 -0
- package/src/styles/autumnnote.scss +11 -0
|
@@ -2,132 +2,17 @@
|
|
|
2
2
|
// Shows a horizontal action bar above (or below) the hovered table,
|
|
3
3
|
// similar in appearance and interaction to ImageTooltip / VideoTooltip.
|
|
4
4
|
import { createElement, on } from '../core/dom.js';
|
|
5
|
+
import { ICONS } from './table-icons.js';
|
|
6
|
+
import {
|
|
7
|
+
getVisualColIndex,
|
|
8
|
+
getCellAtVisualCol,
|
|
9
|
+
getCellAfterVisualCol,
|
|
10
|
+
buildGridMap,
|
|
11
|
+
} from './table-grid.js';
|
|
5
12
|
|
|
6
13
|
const SHOW_DELAY = 120;
|
|
7
14
|
const HIDE_DELAY = 200;
|
|
8
15
|
|
|
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
16
|
const SHADE_PRESETS = [
|
|
132
17
|
'#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#efefef', '#ffffff',
|
|
133
18
|
'#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#9900ff', '#ff00ff',
|
|
@@ -1189,16 +1074,33 @@ export class TableTooltip {
|
|
|
1189
1074
|
}
|
|
1190
1075
|
|
|
1191
1076
|
// ---------------------------------------------------------------------------
|
|
1192
|
-
//
|
|
1077
|
+
// Colour popovers (cell shading + table border colour)
|
|
1193
1078
|
// ---------------------------------------------------------------------------
|
|
1194
1079
|
|
|
1195
|
-
|
|
1080
|
+
/**
|
|
1081
|
+
* Builds one of the two colour popovers. Both are the same widget — a preset
|
|
1082
|
+
* palette, a "no colour" reset row, and a custom `<input type="color">` — and
|
|
1083
|
+
* they were previously two near-identical 55-line methods, so a fix applied
|
|
1084
|
+
* to one silently missed the other. They differ only in the swatch the custom
|
|
1085
|
+
* input starts on and in what the chosen colour is applied to.
|
|
1086
|
+
*
|
|
1087
|
+
* `getPopover` is read lazily rather than closed over: `destroy()` nulls the
|
|
1088
|
+
* field, and the deferred handlers must see that instead of holding the
|
|
1089
|
+
* detached element alive.
|
|
1090
|
+
*
|
|
1091
|
+
* @param {object} config
|
|
1092
|
+
* @param {string} config.defaultColor - initial value of the custom input
|
|
1093
|
+
* @param {(color: string) => void} config.apply - called with '' to clear
|
|
1094
|
+
* @param {() => HTMLElement|null} config.getPopover
|
|
1095
|
+
* @param {() => void} config.hide
|
|
1096
|
+
* @returns {{ el: HTMLElement, titleEl: HTMLElement, noColorBtn: HTMLElement }}
|
|
1097
|
+
*/
|
|
1098
|
+
_buildColorPopover({ defaultColor, apply, getPopover, hide }) {
|
|
1196
1099
|
const pop = createElement('div', { class: 'an-cell-shade-popover' });
|
|
1197
1100
|
pop.style.display = 'none';
|
|
1198
1101
|
|
|
1199
|
-
const
|
|
1200
|
-
pop.appendChild(
|
|
1201
|
-
this._shadeTitleEl = title;
|
|
1102
|
+
const titleEl = createElement('div', { class: 'an-size-popover-title' });
|
|
1103
|
+
pop.appendChild(titleEl);
|
|
1202
1104
|
|
|
1203
1105
|
// 24-color palette — reuse existing CSS classes
|
|
1204
1106
|
const palette = createElement('div', { class: 'an-context-color-palette' });
|
|
@@ -1207,26 +1109,25 @@ export class TableTooltip {
|
|
|
1207
1109
|
sw.style.background = color;
|
|
1208
1110
|
this._disposers.push(on(sw, 'click', (e) => {
|
|
1209
1111
|
e.stopPropagation();
|
|
1210
|
-
|
|
1112
|
+
apply(color);
|
|
1211
1113
|
}));
|
|
1212
1114
|
palette.appendChild(sw);
|
|
1213
1115
|
});
|
|
1214
1116
|
pop.appendChild(palette);
|
|
1215
1117
|
|
|
1216
|
-
//
|
|
1217
|
-
const
|
|
1218
|
-
const
|
|
1219
|
-
this._disposers.push(on(
|
|
1220
|
-
|
|
1221
|
-
pop.appendChild(
|
|
1222
|
-
this._shadeNoBtn = noShadeBtn;
|
|
1118
|
+
// Reset row — clears the colour
|
|
1119
|
+
const noColorRow = createElement('div', { class: 'an-context-color-custom' });
|
|
1120
|
+
const noColorBtn = createElement('button', { type: 'button', class: 'an-shade-no-color' });
|
|
1121
|
+
this._disposers.push(on(noColorBtn, 'click', () => apply('')));
|
|
1122
|
+
noColorRow.appendChild(noColorBtn);
|
|
1123
|
+
pop.appendChild(noColorRow);
|
|
1223
1124
|
|
|
1224
1125
|
// Custom color input
|
|
1225
1126
|
const customRow = createElement('div', { class: 'an-context-color-custom' });
|
|
1226
|
-
const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', class: 'an-shade-color-input', value:
|
|
1127
|
+
const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', class: 'an-shade-color-input', value: defaultColor }));
|
|
1227
1128
|
const customLabel = createElement('span');
|
|
1228
1129
|
customLabel.textContent = this.context.locale.contextMenu.customColorLabel;
|
|
1229
|
-
this._disposers.push(on(colorInput, 'change', () =>
|
|
1130
|
+
this._disposers.push(on(colorInput, 'change', () => apply(colorInput.value)));
|
|
1230
1131
|
customRow.appendChild(colorInput);
|
|
1231
1132
|
customRow.appendChild(customLabel);
|
|
1232
1133
|
pop.appendChild(customRow);
|
|
@@ -1239,39 +1140,66 @@ export class TableTooltip {
|
|
|
1239
1140
|
// Close on outside click
|
|
1240
1141
|
on(document, 'click', (e) => {
|
|
1241
1142
|
const et = /** @type {Node} */ (e.target);
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1143
|
+
const current = getPopover();
|
|
1144
|
+
if (current &&
|
|
1145
|
+
current.style.display !== 'none' &&
|
|
1146
|
+
!current.contains(et) &&
|
|
1245
1147
|
!this._el?.contains(et)) {
|
|
1246
|
-
|
|
1148
|
+
hide();
|
|
1247
1149
|
}
|
|
1248
1150
|
}),
|
|
1249
1151
|
);
|
|
1250
1152
|
|
|
1251
|
-
return pop;
|
|
1153
|
+
return { el: pop, titleEl, noColorBtn };
|
|
1252
1154
|
}
|
|
1253
1155
|
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1156
|
+
/**
|
|
1157
|
+
* Shows a colour popover under the tooltip, flipping above it when there is
|
|
1158
|
+
* no room below. Measurement has to wait a frame because the popover is
|
|
1159
|
+
* display:none until now and would otherwise report zero size.
|
|
1160
|
+
* @param {() => HTMLElement|null} getPopover
|
|
1161
|
+
*/
|
|
1162
|
+
_showColorPopover(getPopover) {
|
|
1163
|
+
const pop = getPopover();
|
|
1164
|
+
if (!pop) return;
|
|
1165
|
+
pop.style.display = 'block';
|
|
1261
1166
|
requestAnimationFrame(() => {
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const
|
|
1167
|
+
const current = getPopover();
|
|
1168
|
+
if (!current || !this._el) return;
|
|
1169
|
+
const pw = current.offsetWidth || 170;
|
|
1170
|
+
const ph = current.offsetHeight || 120;
|
|
1265
1171
|
const tipRect = this._el.getBoundingClientRect();
|
|
1266
1172
|
let left = tipRect.left;
|
|
1267
1173
|
let top = tipRect.bottom + 6;
|
|
1268
1174
|
if (left + pw > globalThis.innerWidth - 8) left = globalThis.innerWidth - pw - 8;
|
|
1269
1175
|
if (top + ph > globalThis.innerHeight - 8) top = tipRect.top - ph - 6;
|
|
1270
|
-
|
|
1271
|
-
|
|
1176
|
+
current.style.left = `${Math.max(8, left)}px`;
|
|
1177
|
+
current.style.top = `${Math.max(8, top)}px`;
|
|
1272
1178
|
});
|
|
1273
1179
|
}
|
|
1274
1180
|
|
|
1181
|
+
// --- Cell background shade ---
|
|
1182
|
+
|
|
1183
|
+
_buildCellShadePopover() {
|
|
1184
|
+
const { el, titleEl, noColorBtn } = this._buildColorPopover({
|
|
1185
|
+
defaultColor: '#ffffff',
|
|
1186
|
+
apply: (color) => this._applyCellShade(color),
|
|
1187
|
+
getPopover: () => this._shadePopover,
|
|
1188
|
+
hide: () => this._hideCellShadePopover(),
|
|
1189
|
+
});
|
|
1190
|
+
this._shadeTitleEl = titleEl;
|
|
1191
|
+
this._shadeNoBtn = noColorBtn;
|
|
1192
|
+
return el;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
_openCellShadePopover() {
|
|
1196
|
+
if (!this._shadePopover) return;
|
|
1197
|
+
const L = this.context.locale.tooltips.table;
|
|
1198
|
+
if (this._shadeTitleEl) this._shadeTitleEl.textContent = L.cellBackground;
|
|
1199
|
+
if (this._shadeNoBtn) this._shadeNoBtn.textContent = L.noShading;
|
|
1200
|
+
this._showColorPopover(() => this._shadePopover);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1275
1203
|
_hideCellShadePopover() {
|
|
1276
1204
|
if (this._shadePopover) this._shadePopover.style.display = 'none';
|
|
1277
1205
|
}
|
|
@@ -1289,62 +1217,18 @@ export class TableTooltip {
|
|
|
1289
1217
|
this.context.invoke('editor.afterCommand');
|
|
1290
1218
|
}
|
|
1291
1219
|
|
|
1292
|
-
//
|
|
1293
|
-
// Table border color popover
|
|
1294
|
-
// ---------------------------------------------------------------------------
|
|
1220
|
+
// --- Table border colour ---
|
|
1295
1221
|
|
|
1296
1222
|
_buildBorderColorPopover() {
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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);
|
|
1223
|
+
const { el, titleEl, noColorBtn } = this._buildColorPopover({
|
|
1224
|
+
defaultColor: '#000000',
|
|
1225
|
+
apply: (color) => this._applyBorderColor(color),
|
|
1226
|
+
getPopover: () => this._borderColorPopover,
|
|
1227
|
+
hide: () => this._hideBorderColorPopover(),
|
|
1313
1228
|
});
|
|
1314
|
-
|
|
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);
|
|
1229
|
+
this._borderColorTitleEl = titleEl;
|
|
1321
1230
|
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;
|
|
1231
|
+
return el;
|
|
1348
1232
|
}
|
|
1349
1233
|
|
|
1350
1234
|
_openBorderColorPopover() {
|
|
@@ -1352,20 +1236,7 @@ export class TableTooltip {
|
|
|
1352
1236
|
const L = this.context.locale.tooltips.table;
|
|
1353
1237
|
if (this._borderColorTitleEl) this._borderColorTitleEl.textContent = L.tableBorderColor;
|
|
1354
1238
|
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
|
-
});
|
|
1239
|
+
this._showColorPopover(() => this._borderColorPopover);
|
|
1369
1240
|
}
|
|
1370
1241
|
|
|
1371
1242
|
_hideBorderColorPopover() {
|
package/src/js/module/Toolbar.js
CHANGED
|
@@ -128,11 +128,18 @@ export class Toolbar {
|
|
|
128
128
|
// ---------------------------------------------------------------------------
|
|
129
129
|
|
|
130
130
|
initialize() {
|
|
131
|
-
this.el = createElement('div', {
|
|
131
|
+
this.el = createElement('div', {
|
|
132
|
+
class: 'an-toolbar',
|
|
133
|
+
role: 'toolbar',
|
|
134
|
+
'aria-orientation': 'horizontal',
|
|
135
|
+
// Matches the hardcoded English label renderer.js gives the editable.
|
|
136
|
+
'aria-label': 'Editor toolbar',
|
|
137
|
+
});
|
|
132
138
|
// Detect FontAwesome once at toolbar build time to avoid re-querying the DOM
|
|
133
139
|
// for every button rendered.
|
|
134
140
|
this._faReady = this._detectFontAwesome();
|
|
135
141
|
this._buildButtons();
|
|
142
|
+
this._initRovingFocus();
|
|
136
143
|
this._btnMap = new Map(
|
|
137
144
|
(this.options.toolbar || []).flat()
|
|
138
145
|
.map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]),
|
|
@@ -606,6 +613,9 @@ export class Toolbar {
|
|
|
606
613
|
title: this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || '',
|
|
607
614
|
'data-btn': btnDef.name,
|
|
608
615
|
'aria-label': this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || btnDef.name,
|
|
616
|
+
// A button that reports an active state is a toggle, so its state has to
|
|
617
|
+
// be exposed to assistive tech and not only through the `.active` class.
|
|
618
|
+
...(typeof btnDef.isActive === 'function' ? { 'aria-pressed': 'false' } : {}),
|
|
609
619
|
});
|
|
610
620
|
|
|
611
621
|
// Render icon: prefer FontAwesome if enabled; otherwise fall back to SVG or text.
|
|
@@ -642,6 +652,93 @@ export class Toolbar {
|
|
|
642
652
|
return /** @type {HTMLButtonElement} */ (btn);
|
|
643
653
|
}
|
|
644
654
|
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
// Keyboard navigation — WAI-ARIA toolbar pattern
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Every control the toolbar contains, in visual order. Disabled ones are
|
|
661
|
+
* included so their tabindex can be cleared; `_navigable()` filters them out.
|
|
662
|
+
* @returns {HTMLElement[]}
|
|
663
|
+
*/
|
|
664
|
+
_controls() {
|
|
665
|
+
if (!this.el) return [];
|
|
666
|
+
return /** @type {HTMLElement[]} */ (Array.from(this.el.querySelectorAll('button, select')));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** @returns {HTMLElement[]} controls that can actually receive focus */
|
|
670
|
+
_navigable() {
|
|
671
|
+
return this._controls().filter(
|
|
672
|
+
(el) => !(/** @type {HTMLButtonElement} */ (el).disabled),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Enforces the roving-tabindex invariant: exactly one control sits in the tab
|
|
678
|
+
* order and the rest are reached with arrow keys. The default toolbar renders
|
|
679
|
+
* 39 controls, so without this a keyboard user pressed Tab 39 times to get
|
|
680
|
+
* past the toolbar and into the editable area.
|
|
681
|
+
*
|
|
682
|
+
* @param {HTMLElement} [focused] - control that should own the tab stop;
|
|
683
|
+
* omitted on refresh, where the user's current position is preserved.
|
|
684
|
+
*/
|
|
685
|
+
_syncRovingTabindex(focused) {
|
|
686
|
+
const all = this._controls();
|
|
687
|
+
const navigable = this._navigable();
|
|
688
|
+
if (!navigable.length) return;
|
|
689
|
+
const active = (focused && navigable.includes(focused))
|
|
690
|
+
? focused
|
|
691
|
+
: navigable.find((el) => el.getAttribute('tabindex') === '0') || navigable[0];
|
|
692
|
+
all.forEach((el) => el.setAttribute('tabindex', el === active ? '0' : '-1'));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Wires arrow-key navigation. Called on build and again after rebuild(). */
|
|
696
|
+
_initRovingFocus() {
|
|
697
|
+
if (!this.el) return;
|
|
698
|
+
this._syncRovingTabindex();
|
|
699
|
+
this._disposers.push(
|
|
700
|
+
on(this.el, 'keydown', (e) => this._onToolbarKeydown(/** @type {KeyboardEvent} */ (e))),
|
|
701
|
+
// Clicking or programmatically focusing a control moves the tab stop with
|
|
702
|
+
// it, so Tab always leaves from wherever the user actually is.
|
|
703
|
+
on(this.el, 'focusin', (e) => {
|
|
704
|
+
const el = /** @type {Element} */ (e.target)?.closest?.('button, select');
|
|
705
|
+
if (el) this._syncRovingTabindex(/** @type {HTMLElement} */ (el));
|
|
706
|
+
}),
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Home/End jump to the ends; Left/Right step between controls and wrap.
|
|
712
|
+
* Up/Down are deliberately left alone so `<select>` controls keep their
|
|
713
|
+
* native value-changing behaviour.
|
|
714
|
+
* @param {KeyboardEvent} event
|
|
715
|
+
*/
|
|
716
|
+
_onToolbarKeydown(event) {
|
|
717
|
+
if (!['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(event.key)) return;
|
|
718
|
+
const controls = this._navigable();
|
|
719
|
+
const current = /** @type {HTMLElement|null} */ (
|
|
720
|
+
/** @type {Element} */ (event.target)?.closest?.('button, select')
|
|
721
|
+
);
|
|
722
|
+
const idx = current ? controls.indexOf(current) : -1;
|
|
723
|
+
if (idx === -1) return;
|
|
724
|
+
|
|
725
|
+
let next;
|
|
726
|
+
if (event.key === 'Home') {
|
|
727
|
+
next = controls[0];
|
|
728
|
+
} else if (event.key === 'End') {
|
|
729
|
+
next = controls.at(-1);
|
|
730
|
+
} else {
|
|
731
|
+
// In RTL the arrow that points at the next control is the left one.
|
|
732
|
+
const rtl = this.options.direction === 'rtl';
|
|
733
|
+
const forward = (event.key === 'ArrowRight') !== rtl;
|
|
734
|
+
next = controls[(idx + (forward ? 1 : -1) + controls.length) % controls.length];
|
|
735
|
+
}
|
|
736
|
+
if (!next) return;
|
|
737
|
+
event.preventDefault();
|
|
738
|
+
this._syncRovingTabindex(next);
|
|
739
|
+
next.focus();
|
|
740
|
+
}
|
|
741
|
+
|
|
645
742
|
// ---------------------------------------------------------------------------
|
|
646
743
|
// FontAwesome detection (run once at initialize time)
|
|
647
744
|
// ---------------------------------------------------------------------------
|
|
@@ -687,13 +784,18 @@ export class Toolbar {
|
|
|
687
784
|
this.el.querySelectorAll('button[data-btn]').forEach((btn) => {
|
|
688
785
|
const def = btnMap.get(/** @type {HTMLElement} */ (btn).dataset.btn);
|
|
689
786
|
if (def && typeof def.isActive === 'function') {
|
|
690
|
-
|
|
787
|
+
const active = !!def.isActive(this.context);
|
|
788
|
+
btn.classList.toggle('active', active);
|
|
789
|
+
btn.setAttribute('aria-pressed', String(active));
|
|
691
790
|
}
|
|
692
791
|
if (def && typeof def.isDisabled === 'function') {
|
|
693
792
|
/** @type {HTMLButtonElement} */ (btn).disabled = !!def.isDisabled(this.context);
|
|
694
793
|
}
|
|
695
794
|
});
|
|
696
795
|
|
|
796
|
+
// A button disabled just now must not be the one holding the tab stop.
|
|
797
|
+
this._syncRovingTabindex();
|
|
798
|
+
|
|
697
799
|
// Sync select dropdowns (e.g. font family) with current cursor position
|
|
698
800
|
this.el.querySelectorAll('select[data-btn]').forEach((select) => {
|
|
699
801
|
const def = btnMap.get(/** @type {HTMLElement} */ (select).dataset.btn);
|
|
@@ -741,6 +843,9 @@ export class Toolbar {
|
|
|
741
843
|
if (this.el) this.el.innerHTML = '';
|
|
742
844
|
this._faReady = this._detectFontAwesome();
|
|
743
845
|
this._buildButtons();
|
|
846
|
+
// rebuild() cleared the disposers, taking the keydown/focusin listeners
|
|
847
|
+
// with them — re-arm navigation over the new controls.
|
|
848
|
+
this._initRovingFocus();
|
|
744
849
|
this._btnMap = new Map(
|
|
745
850
|
(this.options.toolbar || []).flat()
|
|
746
851
|
.map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]),
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* table-grid.js - Geometry helpers for tables inside the editor.
|
|
3
|
+
*
|
|
4
|
+
* A table's DOM order is not its visual order once colspan/rowspan are in play,
|
|
5
|
+
* so every structural operation (insert, delete, merge, sort) needs to reason
|
|
6
|
+
* about the *visual* grid rather than about `row.cells` indices. These helpers
|
|
7
|
+
* are pure functions of the table element, which keeps them independently
|
|
8
|
+
* testable and free of any tooltip state.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns the visual (logical) column index of a cell, taking colspan into
|
|
13
|
+
* account for all preceding cells in the same row.
|
|
14
|
+
* @param {HTMLTableCellElement} cell
|
|
15
|
+
* @returns {number} 0-based visual column index, or -1 on failure
|
|
16
|
+
*/
|
|
17
|
+
function getVisualColIndex(cell) {
|
|
18
|
+
const row = cell.closest('tr');
|
|
19
|
+
if (!row) return -1;
|
|
20
|
+
let visualIdx = 0;
|
|
21
|
+
for (const c of row.cells) {
|
|
22
|
+
if (c === cell) return visualIdx;
|
|
23
|
+
visualIdx += c.colSpan || 1;
|
|
24
|
+
}
|
|
25
|
+
return -1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Finds the first cell in a row whose visual start column equals visualIdx.
|
|
30
|
+
* Returns null if no exact match (e.g. the column is spanned by a merged cell).
|
|
31
|
+
* @param {HTMLTableRowElement} row
|
|
32
|
+
* @param {number} visualIdx
|
|
33
|
+
* @returns {HTMLTableCellElement|null}
|
|
34
|
+
*/
|
|
35
|
+
function getCellAtVisualCol(row, visualIdx) {
|
|
36
|
+
let vIdx = 0;
|
|
37
|
+
for (const c of row.cells) {
|
|
38
|
+
if (vIdx === visualIdx) return c;
|
|
39
|
+
if (vIdx > visualIdx) break;
|
|
40
|
+
vIdx += c.colSpan || 1;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Finds the first cell whose visual range ends after visualIdx
|
|
47
|
+
* (used for inserting a new column to the right of visualIdx).
|
|
48
|
+
* @param {HTMLTableRowElement} row
|
|
49
|
+
* @param {number} visualIdx
|
|
50
|
+
* @returns {HTMLTableCellElement|null} reference cell for insertBefore, or null = append
|
|
51
|
+
*/
|
|
52
|
+
function getCellAfterVisualCol(row, visualIdx) {
|
|
53
|
+
let vIdx = 0;
|
|
54
|
+
for (const c of row.cells) {
|
|
55
|
+
vIdx += c.colSpan || 1;
|
|
56
|
+
if (vIdx > visualIdx) {
|
|
57
|
+
// next cell after the one that starts at / spans visualIdx
|
|
58
|
+
const next = c.nextElementSibling;
|
|
59
|
+
return (next?.tagName === 'TD' || next?.tagName === 'TH') ? /** @type {HTMLTableCellElement} */ (next) : null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build a 2D grid map of the table, accounting for both rowspan and colspan.
|
|
67
|
+
*
|
|
68
|
+
* gridMap[r][c] = the DOM cell occupying visual grid position (r, c).
|
|
69
|
+
* cellPos = WeakMap: cell → { r, c, rs, cs } (top-left grid origin + span).
|
|
70
|
+
*
|
|
71
|
+
* Uses HTMLTableElement.rows which is scoped to the table itself and never
|
|
72
|
+
* includes rows from nested tables.
|
|
73
|
+
*
|
|
74
|
+
* @param {HTMLTableElement} table
|
|
75
|
+
* @returns {{ gridMap: Object, cellPos: WeakMap }}
|
|
76
|
+
*/
|
|
77
|
+
function buildGridMap(table) {
|
|
78
|
+
const rows = Array.from(table.rows);
|
|
79
|
+
const gridMap = {};
|
|
80
|
+
const cellPos = new WeakMap();
|
|
81
|
+
rows.forEach((row, r) => {
|
|
82
|
+
if (!gridMap[r]) gridMap[r] = {};
|
|
83
|
+
let c = 0;
|
|
84
|
+
for (const cell of row.cells) {
|
|
85
|
+
// Skip positions already occupied by a rowspan from a previous row
|
|
86
|
+
while (gridMap[r][c]) c++;
|
|
87
|
+
const rs = cell.rowSpan || 1;
|
|
88
|
+
const cs = cell.colSpan || 1;
|
|
89
|
+
cellPos.set(cell, { r, c, rs, cs });
|
|
90
|
+
for (let dr = 0; dr < rs; dr++) {
|
|
91
|
+
if (!gridMap[r + dr]) gridMap[r + dr] = {};
|
|
92
|
+
for (let dc = 0; dc < cs; dc++) {
|
|
93
|
+
gridMap[r + dr][c + dc] = cell;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
c += cs;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return { gridMap, cellPos };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { getVisualColIndex, getCellAtVisualCol, getCellAfterVisualCol, buildGridMap };
|