autumnnote 1.0.1 → 1.0.4
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 +6 -2
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +2254 -1828
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +11 -11
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/js/core/sanitise.js +43 -0
- package/src/js/editing/History.js +10 -2
- package/src/js/editing/Style.js +65 -4
- package/src/js/editing/Typing.js +243 -37
- package/src/js/index.umd.js +12 -12
- package/src/js/module/Buttons.js +15 -5
- package/src/js/module/CodeTooltip.js +18 -7
- package/src/js/module/Codeview.js +6 -1
- package/src/js/module/ContextMenu.js +13 -6
- package/src/js/module/Editor.js +70 -2
- package/src/js/module/IconDialog.js +27 -13
- package/src/js/module/ImageResizer.js +63 -29
- package/src/js/module/ImageTooltip.js +77 -16
- package/src/js/module/Placeholder.js +2 -1
- package/src/js/module/Statusbar.js +16 -2
- package/src/js/module/TableTooltip.js +197 -76
- package/src/js/module/Toolbar.js +103 -17
- package/src/js/module/VideoDialog.js +3 -3
- package/src/js/module/VideoResizer.js +67 -36
- package/src/js/module/VideoTooltip.js +82 -6
- package/src/js/renderer.js +5 -0
- package/src/js/settings.js +4 -0
- package/src/styles/autumnnote.scss +61 -31
- package/types/index.d.ts +4 -4
|
@@ -6,6 +6,64 @@ import { createElement, on } from '../core/dom.js';
|
|
|
6
6
|
const SHOW_DELAY = 120;
|
|
7
7
|
const HIDE_DELAY = 200;
|
|
8
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 && next.tagName === 'TD' || next && next.tagName === 'TH') ? next : null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
9
67
|
const ICONS = {
|
|
10
68
|
rowAbove: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="12" x2="21" y2="12"/><path d="M12 3v7"/><path d="M9 7l3-4 3 4"/></svg>`,
|
|
11
69
|
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>`,
|
|
@@ -52,7 +110,7 @@ export class TableTooltip {
|
|
|
52
110
|
if (cell) this._activeCell = cell;
|
|
53
111
|
this._scheduleShow(table);
|
|
54
112
|
}
|
|
55
|
-
}),
|
|
113
|
+
}, { passive: true }),
|
|
56
114
|
on(editable, 'mouseout', (e) => {
|
|
57
115
|
const to = e.relatedTarget;
|
|
58
116
|
if (!to || (
|
|
@@ -62,7 +120,7 @@ export class TableTooltip {
|
|
|
62
120
|
)) {
|
|
63
121
|
this._scheduleHide();
|
|
64
122
|
}
|
|
65
|
-
}),
|
|
123
|
+
}, { passive: true }),
|
|
66
124
|
on(document, 'click', (e) => {
|
|
67
125
|
if (this._activeTable &&
|
|
68
126
|
!this._activeTable.contains(e.target) &&
|
|
@@ -97,8 +155,11 @@ export class TableTooltip {
|
|
|
97
155
|
let _startW = 0;
|
|
98
156
|
let _startH = 0;
|
|
99
157
|
let _colIdx = -1;
|
|
158
|
+
let _colCells = null; // cells cached at drag-start — avoids querySelectorAll every frame
|
|
100
159
|
let _row = null;
|
|
101
160
|
let _table = null;
|
|
161
|
+
let _rafDocMove = null; // pending rAF handle for resize drag
|
|
162
|
+
let _rafEditorMove = null; // pending rAF handle for cursor detection
|
|
102
163
|
|
|
103
164
|
const clearHover = () => {
|
|
104
165
|
if (_nearCell) { _nearCell.style.cursor = ''; _nearCell = null; }
|
|
@@ -107,24 +168,29 @@ export class TableTooltip {
|
|
|
107
168
|
|
|
108
169
|
const onEditorMove = (e) => {
|
|
109
170
|
if (_resizing) return;
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
cell
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
171
|
+
// Throttle to one check per animation frame — getBoundingClientRect forces reflow
|
|
172
|
+
if (_rafEditorMove !== null) return;
|
|
173
|
+
const target = e.target;
|
|
174
|
+
const clientX = e.clientX;
|
|
175
|
+
const clientY = e.clientY;
|
|
176
|
+
_rafEditorMove = requestAnimationFrame(() => {
|
|
177
|
+
_rafEditorMove = null;
|
|
178
|
+
const cell = target.closest('td, th');
|
|
179
|
+
if (!cell || !editable.contains(cell)) { clearHover(); return; }
|
|
180
|
+
if (_nearCell && _nearCell !== cell) _nearCell.style.cursor = '';
|
|
181
|
+
const rect = cell.getBoundingClientRect();
|
|
182
|
+
const onRight = Math.abs(clientX - rect.right) < HIT;
|
|
183
|
+
const onBottom = Math.abs(clientY - rect.bottom) < HIT;
|
|
184
|
+
if (onRight) {
|
|
185
|
+
cell.style.cursor = 'col-resize';
|
|
186
|
+
_nearCell = cell; _nearEdge = 'col';
|
|
187
|
+
} else if (onBottom) {
|
|
188
|
+
cell.style.cursor = 'row-resize';
|
|
189
|
+
_nearCell = cell; _nearEdge = 'row';
|
|
190
|
+
} else {
|
|
191
|
+
clearHover();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
128
194
|
};
|
|
129
195
|
|
|
130
196
|
const onEditorDown = (e) => {
|
|
@@ -135,8 +201,14 @@ export class TableTooltip {
|
|
|
135
201
|
_startY = e.clientY;
|
|
136
202
|
_table = _nearCell.closest('table');
|
|
137
203
|
if (_edge === 'col') {
|
|
138
|
-
_startW
|
|
139
|
-
_colIdx
|
|
204
|
+
_startW = _nearCell.offsetWidth;
|
|
205
|
+
_colIdx = getVisualColIndex(_nearCell);
|
|
206
|
+
// Cache column cells once so onDocMove never runs querySelectorAll per frame
|
|
207
|
+
_colCells = _colIdx >= 0
|
|
208
|
+
? Array.from(_table.querySelectorAll('tr'))
|
|
209
|
+
.map(r => getCellAtVisualCol(r, _colIdx))
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
: [];
|
|
140
212
|
document.body.style.cursor = 'col-resize';
|
|
141
213
|
} else {
|
|
142
214
|
_row = _nearCell.closest('tr');
|
|
@@ -150,34 +222,42 @@ export class TableTooltip {
|
|
|
150
222
|
|
|
151
223
|
const onDocMove = (e) => {
|
|
152
224
|
if (!_resizing) return;
|
|
153
|
-
if
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
225
|
+
// Skip if a frame is already scheduled — avoids per-pixel style thrashing
|
|
226
|
+
if (_rafDocMove !== null) return;
|
|
227
|
+
const clientX = e.clientX;
|
|
228
|
+
const clientY = e.clientY;
|
|
229
|
+
_rafDocMove = requestAnimationFrame(() => {
|
|
230
|
+
_rafDocMove = null;
|
|
231
|
+
if (_edge === 'col') {
|
|
232
|
+
const newW = Math.max(30, _startW + (clientX - _startX));
|
|
233
|
+
for (const c of _colCells) {
|
|
234
|
+
c.style.width = `${newW}px`;
|
|
235
|
+
c.style.minWidth = `${newW}px`;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
const newH = Math.max(20, _startH + (clientY - _startY));
|
|
239
|
+
if (_row) {
|
|
240
|
+
for (const c of _row.cells) {
|
|
241
|
+
c.style.height = `${newH}px`;
|
|
242
|
+
c.style.minHeight = `${newH}px`;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
168
245
|
}
|
|
169
|
-
}
|
|
246
|
+
});
|
|
170
247
|
};
|
|
171
248
|
|
|
172
249
|
const onDocUp = () => {
|
|
173
250
|
if (!_resizing) return;
|
|
251
|
+
// Cancel any in-flight rAF so stale writes don't land after mouseup
|
|
252
|
+
if (_rafDocMove !== null) { cancelAnimationFrame(_rafDocMove); _rafDocMove = null; }
|
|
174
253
|
_resizing = false;
|
|
175
254
|
document.body.style.userSelect = '';
|
|
176
255
|
document.body.style.cursor = '';
|
|
177
|
-
_edge
|
|
178
|
-
_table
|
|
179
|
-
_row
|
|
180
|
-
_colIdx
|
|
256
|
+
_edge = null;
|
|
257
|
+
_table = null;
|
|
258
|
+
_row = null;
|
|
259
|
+
_colIdx = -1;
|
|
260
|
+
_colCells = null;
|
|
181
261
|
this.context.invoke('editor.afterCommand');
|
|
182
262
|
};
|
|
183
263
|
|
|
@@ -314,7 +394,10 @@ export class TableTooltip {
|
|
|
314
394
|
_show() {
|
|
315
395
|
if (!this._activeTable) return;
|
|
316
396
|
this._el.style.display = 'flex';
|
|
317
|
-
|
|
397
|
+
// Defer: offsetWidth on a newly-visible element forces layout synchronously
|
|
398
|
+
requestAnimationFrame(() => {
|
|
399
|
+
if (this._activeTable) this._positionNear(this._activeTable);
|
|
400
|
+
});
|
|
318
401
|
}
|
|
319
402
|
|
|
320
403
|
_hide() {
|
|
@@ -356,6 +439,16 @@ export class TableTooltip {
|
|
|
356
439
|
// ---------------------------------------------------------------------------
|
|
357
440
|
|
|
358
441
|
_getCell() {
|
|
442
|
+
// Prefer the cell under the current text cursor (most intuitive for operations)
|
|
443
|
+
const sel = window.getSelection();
|
|
444
|
+
if (sel && sel.rangeCount) {
|
|
445
|
+
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
446
|
+
if (container.nodeType === 3) container = container.parentElement;
|
|
447
|
+
const cellFromSel = container && container.closest && container.closest('td, th');
|
|
448
|
+
if (cellFromSel && this._activeTable && this._activeTable.contains(cellFromSel)) {
|
|
449
|
+
return cellFromSel;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
359
452
|
return this._activeCell
|
|
360
453
|
|| (this._activeTable && this._activeTable.querySelector('td, th'));
|
|
361
454
|
}
|
|
@@ -381,25 +474,27 @@ export class TableTooltip {
|
|
|
381
474
|
}
|
|
382
475
|
if (position === 'above') row.parentElement?.insertBefore(newRow, row);
|
|
383
476
|
else row.insertAdjacentElement('afterend', newRow);
|
|
384
|
-
|
|
477
|
+
// Defer positioning until the browser has painted the new layout
|
|
478
|
+
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
385
479
|
this.context.invoke('editor.afterCommand');
|
|
386
480
|
}
|
|
387
481
|
|
|
388
482
|
_addColumn(position) {
|
|
389
483
|
const cell = this._getCell();
|
|
390
484
|
if (!cell) return;
|
|
391
|
-
const row = cell.closest('tr');
|
|
392
485
|
const table = cell.closest('table');
|
|
393
|
-
if (!
|
|
394
|
-
const
|
|
395
|
-
Array.from(table.querySelectorAll('tr'))
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
486
|
+
if (!table) return;
|
|
487
|
+
const visualColIdx = getVisualColIndex(cell);
|
|
488
|
+
const rows = Array.from(table.querySelectorAll('tr'));
|
|
489
|
+
// Batch reads first, then writes — prevents layout thrashing inside the loop
|
|
490
|
+
const refs = rows.map(r => position === 'left'
|
|
491
|
+
? getCellAtVisualCol(r, visualColIdx)
|
|
492
|
+
: getCellAfterVisualCol(r, visualColIdx));
|
|
493
|
+
const isHeaders = rows.map(r => r.closest('thead') !== null);
|
|
494
|
+
rows.forEach((r, i) => {
|
|
495
|
+
r.insertBefore(createElement(isHeaders[i] ? 'th' : 'td', {}, ['\u00a0']), refs[i]);
|
|
401
496
|
});
|
|
402
|
-
this._positionNear(this._activeTable);
|
|
497
|
+
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
403
498
|
this.context.invoke('editor.afterCommand');
|
|
404
499
|
}
|
|
405
500
|
|
|
@@ -415,43 +510,65 @@ export class TableTooltip {
|
|
|
415
510
|
if (bodyRows <= 1 && row.closest('tbody')) return;
|
|
416
511
|
this._activeCell = null;
|
|
417
512
|
row.parentElement?.removeChild(row);
|
|
418
|
-
this._positionNear(this._activeTable);
|
|
513
|
+
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
419
514
|
this.context.invoke('editor.afterCommand');
|
|
420
515
|
}
|
|
421
516
|
|
|
422
517
|
_deleteColumn() {
|
|
423
518
|
const cell = this._getCell();
|
|
424
519
|
if (!cell) return;
|
|
425
|
-
const row = cell.closest('tr');
|
|
426
520
|
const table = cell.closest('table');
|
|
427
|
-
if (!
|
|
428
|
-
|
|
429
|
-
|
|
521
|
+
if (!table) return;
|
|
522
|
+
const row = cell.closest('tr');
|
|
523
|
+
if (row && row.cells.length <= 1) return;
|
|
524
|
+
const visualColIdx = getVisualColIndex(cell);
|
|
430
525
|
this._activeCell = null;
|
|
431
|
-
Array.from(table.querySelectorAll('tr'))
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
});
|
|
435
|
-
this._positionNear(this._activeTable);
|
|
526
|
+
const rows = Array.from(table.querySelectorAll('tr'));
|
|
527
|
+
// Batch reads before writes to avoid forced reflows inside the loop
|
|
528
|
+
const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
|
|
529
|
+
cells.forEach((c, i) => { if (c) rows[i].removeChild(c); });
|
|
530
|
+
requestAnimationFrame(() => this._positionNear(this._activeTable));
|
|
436
531
|
this.context.invoke('editor.afterCommand');
|
|
437
532
|
}
|
|
438
533
|
|
|
439
534
|
_mergeCells() {
|
|
440
535
|
const cell = this._getCell();
|
|
441
536
|
if (!cell) return;
|
|
442
|
-
const row = cell.closest('tr');
|
|
443
|
-
if (!row) return;
|
|
444
537
|
const sel = window.getSelection();
|
|
445
538
|
if (!sel || sel.rangeCount === 0) return;
|
|
446
539
|
const range = sel.getRangeAt(0);
|
|
447
|
-
const
|
|
540
|
+
const table = cell.closest('table');
|
|
541
|
+
if (!table) return;
|
|
542
|
+
|
|
543
|
+
// Collect all cells (in any row) that intersect the selection
|
|
544
|
+
const allCells = Array.from(table.querySelectorAll('td, th'));
|
|
545
|
+
const selected = allCells.filter((c) => {
|
|
448
546
|
try { return range.intersectsNode(c); } catch { return false; }
|
|
449
547
|
});
|
|
450
548
|
if (selected.length < 2) return;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
549
|
+
|
|
550
|
+
// Determine if all selected cells are in the same row (horizontal merge)
|
|
551
|
+
const rows = [...new Set(selected.map((c) => c.closest('tr')))];
|
|
552
|
+
if (rows.length === 1) {
|
|
553
|
+
// Horizontal merge within a single row
|
|
554
|
+
const row = rows[0];
|
|
555
|
+
const rowSelected = Array.from(row.cells).filter((c) => selected.includes(c));
|
|
556
|
+
if (rowSelected.length < 2) return;
|
|
557
|
+
const first = rowSelected[0];
|
|
558
|
+
first.colSpan = rowSelected.reduce((sum, c) => sum + (c.colSpan || 1), 0);
|
|
559
|
+
first.innerHTML = rowSelected.map((c) => c.innerHTML).join('');
|
|
560
|
+
rowSelected.slice(1).forEach((c) => row.removeChild(c));
|
|
561
|
+
} else {
|
|
562
|
+
// Vertical merge across rows — merge into first selected cell (rowspan)
|
|
563
|
+
const visualCols = [...new Set(selected.map((c) => getVisualColIndex(c)))];
|
|
564
|
+
if (visualCols.length !== 1) return; // only support single-column vertical merge
|
|
565
|
+
const first = selected[0];
|
|
566
|
+
first.rowSpan = selected.reduce((sum, c) => sum + (c.rowSpan || 1), 0);
|
|
567
|
+
first.innerHTML = selected.map((c) => c.innerHTML).join('');
|
|
568
|
+
selected.slice(1).forEach((c) => {
|
|
569
|
+
if (c.closest('tr')) c.closest('tr').removeChild(c);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
455
572
|
this.context.invoke('editor.afterCommand');
|
|
456
573
|
}
|
|
457
574
|
|
|
@@ -532,15 +649,19 @@ export class TableTooltip {
|
|
|
532
649
|
|
|
533
650
|
this._sizeApply = (val) => {
|
|
534
651
|
if (isCol) {
|
|
535
|
-
const table
|
|
536
|
-
const
|
|
537
|
-
Array.from(table.querySelectorAll('tr'))
|
|
538
|
-
|
|
652
|
+
const table = cell.closest('table');
|
|
653
|
+
const visualColIdx = getVisualColIndex(cell);
|
|
654
|
+
const rows = Array.from(table.querySelectorAll('tr'));
|
|
655
|
+
// Batch reads, then writes
|
|
656
|
+
const cells = rows.map(r => getCellAtVisualCol(r, visualColIdx));
|
|
657
|
+
cells.forEach(c => {
|
|
539
658
|
if (c) { c.style.width = `${val}px`; c.style.minWidth = `${val}px`; }
|
|
540
659
|
});
|
|
541
660
|
} else {
|
|
542
661
|
const row = cell.closest('tr');
|
|
543
|
-
if (row)
|
|
662
|
+
if (row) {
|
|
663
|
+
for (const c of row.cells) { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; }
|
|
664
|
+
}
|
|
544
665
|
}
|
|
545
666
|
this.context.invoke('editor.afterCommand');
|
|
546
667
|
};
|
package/src/js/module/Toolbar.js
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { createElement, on } from '../core/dom.js';
|
|
7
7
|
|
|
8
|
+
// Module-level cache for FontAwesome detection.
|
|
9
|
+
// Evaluated once per page load so all Toolbar instances on the same page agree
|
|
10
|
+
// on whether the HOST PAGE included FA — regardless of whether IconDialog later
|
|
11
|
+
// auto-injects its own FA <link> for the icon-picker glyph rendering.
|
|
12
|
+
let _faPageLevelReady = null;
|
|
13
|
+
|
|
8
14
|
export class Toolbar {
|
|
9
15
|
/**
|
|
10
16
|
* @param {import('../Context.js').Context} context
|
|
@@ -16,6 +22,8 @@ export class Toolbar {
|
|
|
16
22
|
this.el = null;
|
|
17
23
|
/** @type {Array<() => void>} disposers */
|
|
18
24
|
this._disposers = [];
|
|
25
|
+
/** @type {Array<() => void>} closers for all open color picker popups */
|
|
26
|
+
this._colorPickerClosers = [];
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
// ---------------------------------------------------------------------------
|
|
@@ -253,9 +261,36 @@ export class Toolbar {
|
|
|
253
261
|
|
|
254
262
|
// ---- State ----
|
|
255
263
|
let isOpen = false;
|
|
264
|
+
/** @type {Range|null} saved selection range before popup opens */
|
|
265
|
+
let savedRange = null;
|
|
266
|
+
|
|
267
|
+
const saveSelection = () => {
|
|
268
|
+
const sel = window.getSelection();
|
|
269
|
+
savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const restoreSelection = () => {
|
|
273
|
+
if (!savedRange) return;
|
|
274
|
+
const sel = window.getSelection();
|
|
275
|
+
if (sel) {
|
|
276
|
+
sel.removeAllRanges();
|
|
277
|
+
sel.addRange(savedRange);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
256
280
|
|
|
257
281
|
const openPopup = () => {
|
|
282
|
+
// Close any other open color picker before opening this one
|
|
283
|
+
this._colorPickerClosers.forEach((fn) => { if (fn !== closePopup) fn(); });
|
|
284
|
+
saveSelection();
|
|
258
285
|
isOpen = true;
|
|
286
|
+
// Use fixed positioning so the popup escapes any overflow-clipping ancestor
|
|
287
|
+
// (notably toolbar scroll mode, where overflow-x:auto coerces overflow-y)
|
|
288
|
+
const rect = arrowBtn.getBoundingClientRect();
|
|
289
|
+
const popupMinW = 184;
|
|
290
|
+
let left = rect.left;
|
|
291
|
+
if (left + popupMinW > window.innerWidth) left = rect.right - popupMinW;
|
|
292
|
+
popup.style.top = `${rect.bottom + 4}px`;
|
|
293
|
+
popup.style.left = `${Math.max(4, left)}px`;
|
|
259
294
|
popup.style.display = 'block';
|
|
260
295
|
arrowBtn.setAttribute('aria-expanded', 'true');
|
|
261
296
|
};
|
|
@@ -263,6 +298,8 @@ export class Toolbar {
|
|
|
263
298
|
const closePopup = () => {
|
|
264
299
|
isOpen = false;
|
|
265
300
|
popup.style.display = 'none';
|
|
301
|
+
popup.style.top = '';
|
|
302
|
+
popup.style.left = '';
|
|
266
303
|
arrowBtn.setAttribute('aria-expanded', 'false');
|
|
267
304
|
};
|
|
268
305
|
|
|
@@ -270,7 +307,7 @@ export class Toolbar {
|
|
|
270
307
|
currentColor = color;
|
|
271
308
|
strip.style.background = color;
|
|
272
309
|
colorInput.value = color;
|
|
273
|
-
|
|
310
|
+
restoreSelection();
|
|
274
311
|
def.action(this.context, color);
|
|
275
312
|
this.context.invoke('editor.afterCommand');
|
|
276
313
|
closePopup();
|
|
@@ -278,17 +315,27 @@ export class Toolbar {
|
|
|
278
315
|
|
|
279
316
|
const d1 = on(applyBtn, 'click', (e) => {
|
|
280
317
|
e.preventDefault();
|
|
281
|
-
|
|
318
|
+
restoreSelection();
|
|
282
319
|
def.action(this.context, currentColor);
|
|
283
320
|
this.context.invoke('editor.afterCommand');
|
|
284
321
|
});
|
|
285
322
|
|
|
286
|
-
const d2 = on(arrowBtn, '
|
|
323
|
+
const d2 = on(arrowBtn, 'mousedown', (e) => {
|
|
324
|
+
// Prevent editor blur so selection is preserved when the popup opens
|
|
325
|
+
e.preventDefault();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const d2b = on(arrowBtn, 'click', (e) => {
|
|
287
329
|
e.stopPropagation();
|
|
288
330
|
if (isOpen) closePopup(); else openPopup();
|
|
289
331
|
});
|
|
290
332
|
|
|
291
|
-
const d3 = on(swatches, '
|
|
333
|
+
const d3 = on(swatches, 'mousedown', (e) => {
|
|
334
|
+
// Prevent blur before the click handler fires
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const d3b = on(swatches, 'click', (e) => {
|
|
292
339
|
const sw = e.target.closest('.an-color-swatch');
|
|
293
340
|
if (sw) applyColor(sw.dataset.color);
|
|
294
341
|
});
|
|
@@ -298,16 +345,38 @@ export class Toolbar {
|
|
|
298
345
|
});
|
|
299
346
|
|
|
300
347
|
const d5 = on(document, 'click', (e) => {
|
|
301
|
-
|
|
348
|
+
// popup is in document.body, not inside wrap — check both
|
|
349
|
+
if (isOpen && !wrap.contains(e.target) && !popup.contains(e.target)) closePopup();
|
|
302
350
|
});
|
|
303
351
|
|
|
304
352
|
const d6 = on(popup, 'click', (e) => e.stopPropagation());
|
|
305
353
|
|
|
306
|
-
|
|
354
|
+
// Close the popup when the viewport scrolls or resizes so the fixed-position
|
|
355
|
+
// popup doesn't drift away from the button it belongs to.
|
|
356
|
+
const onScrollResize = () => { if (isOpen) closePopup(); };
|
|
357
|
+
document.addEventListener('scroll', onScrollResize, { passive: true, capture: true });
|
|
358
|
+
window.addEventListener('resize', onScrollResize, { passive: true });
|
|
359
|
+
|
|
360
|
+
this._disposers.push(d1, d2, d2b, d3, d3b, d4, d5, d6,
|
|
361
|
+
() => document.removeEventListener('scroll', onScrollResize, { capture: true }),
|
|
362
|
+
() => window.removeEventListener('resize', onScrollResize),
|
|
363
|
+
// Remove popup from body on editor destroy
|
|
364
|
+
() => { if (popup.parentNode) popup.parentNode.removeChild(popup); },
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Register this popup's closer so other color pickers can close it
|
|
368
|
+
this._colorPickerClosers.push(closePopup);
|
|
369
|
+
this._disposers.push(() => {
|
|
370
|
+
const idx = this._colorPickerClosers.indexOf(closePopup);
|
|
371
|
+
if (idx !== -1) this._colorPickerClosers.splice(idx, 1);
|
|
372
|
+
});
|
|
307
373
|
|
|
374
|
+
// Append popup to document.body so it escapes all overflow-clipping and
|
|
375
|
+
// contain:layout ancestors (contain:layout makes the container a fixed-pos
|
|
376
|
+
// containing block per the CSS Contain spec, breaking viewport coordinates).
|
|
308
377
|
wrap.appendChild(applyBtn);
|
|
309
378
|
wrap.appendChild(arrowBtn);
|
|
310
|
-
|
|
379
|
+
document.body.appendChild(popup);
|
|
311
380
|
return wrap;
|
|
312
381
|
}
|
|
313
382
|
|
|
@@ -329,22 +398,27 @@ export class Toolbar {
|
|
|
329
398
|
'aria-label': def.tooltip || def.name,
|
|
330
399
|
});
|
|
331
400
|
|
|
332
|
-
// Blank "placeholder" option
|
|
401
|
+
// Blank "placeholder" option (non-selectable header)
|
|
333
402
|
const placeholderText = def.placeholder || 'Font';
|
|
334
|
-
const placeholder = createElement('option', { value: '' }, [placeholderText]);
|
|
403
|
+
const placeholder = createElement('option', { value: '', disabled: '', hidden: '' }, [placeholderText]);
|
|
335
404
|
select.appendChild(placeholder);
|
|
336
405
|
|
|
337
406
|
items.forEach((item) => {
|
|
338
|
-
const value
|
|
339
|
-
const label
|
|
340
|
-
const
|
|
341
|
-
|
|
407
|
+
const value = (typeof item === 'object') ? item.value : item;
|
|
408
|
+
const label = (typeof item === 'object') ? item.label : item;
|
|
409
|
+
const isHeader = (typeof item === 'object') && !!item.disabled;
|
|
410
|
+
const attrs = { value };
|
|
411
|
+
if (isHeader) attrs.disabled = '';
|
|
412
|
+
const opt = createElement('option', attrs, [label]);
|
|
413
|
+
// Only apply fontFamily face preview on real (non-header) entries
|
|
414
|
+
if (def.name === 'fontFamily' && !isHeader) opt.style.fontFamily = value;
|
|
342
415
|
select.appendChild(opt);
|
|
343
416
|
});
|
|
344
417
|
|
|
345
418
|
const disposer = on(select, 'change', (e) => {
|
|
346
419
|
const value = e.target.value;
|
|
347
|
-
|
|
420
|
+
const selectedOpt = e.target.options[e.target.selectedIndex];
|
|
421
|
+
if (!value || selectedOpt.disabled) return;
|
|
348
422
|
this.context.invoke('editor.focus');
|
|
349
423
|
def.action(this.context, value);
|
|
350
424
|
this.context.invoke('editor.afterCommand');
|
|
@@ -502,9 +576,21 @@ export class Toolbar {
|
|
|
502
576
|
|
|
503
577
|
_detectFontAwesome() {
|
|
504
578
|
if (!this.options.useFontAwesome) return false;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
579
|
+
// Return cached result when available. This ensures that a later-initialised
|
|
580
|
+
// toolbar sees the same detection state as the first one — even if IconDialog
|
|
581
|
+
// has since injected its own FA <link> into <head> for the icon-picker UI.
|
|
582
|
+
if (_faPageLevelReady !== null) return _faPageLevelReady;
|
|
583
|
+
if (document.querySelector('.fa, .fas, .far, .fal, .fab, .fa-solid')) {
|
|
584
|
+
_faPageLevelReady = true;
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
// Exclude the editor-self-injected link (id='an-fontawesome-css') so it doesn't
|
|
588
|
+
// count as "the host page loaded FA" for toolbar icon rendering purposes.
|
|
589
|
+
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
590
|
+
.filter((l) => l.id !== 'an-fontawesome-css')
|
|
591
|
+
.map((l) => l.href || '').join(' ');
|
|
592
|
+
_faPageLevelReady = /fontawesome|font-awesome|use\.fontawesome|all\.css/.test(links);
|
|
593
|
+
return _faPageLevelReady;
|
|
508
594
|
}
|
|
509
595
|
|
|
510
596
|
// ---------------------------------------------------------------------------
|
|
@@ -219,7 +219,7 @@ export class VideoDialog {
|
|
|
219
219
|
if (info && (info.type === 'YouTube' || info.type === 'YouTube Shorts' || info.type === 'Vimeo')) {
|
|
220
220
|
const iframeTitle = `${info.type} video player`;
|
|
221
221
|
return (
|
|
222
|
-
`<div class="an-video-wrapper" style="position:relative;display:
|
|
222
|
+
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
223
223
|
`<iframe src="${info.embedUrl}" width="${width}" height="${height}" ` +
|
|
224
224
|
`title="${iframeTitle}" ` +
|
|
225
225
|
`frameborder="0" allowfullscreen ` +
|
|
@@ -233,7 +233,7 @@ export class VideoDialog {
|
|
|
233
233
|
if (info && info.type === 'Direct video') {
|
|
234
234
|
const src = info.embedUrl.replace(/"/g, '%22');
|
|
235
235
|
return (
|
|
236
|
-
`<div class="an-video-wrapper" style="position:relative;display:
|
|
236
|
+
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
237
237
|
`<video src="${src}" width="${width}" height="${height}" controls ` +
|
|
238
238
|
`style="display:block;max-width:100%"></video>` +
|
|
239
239
|
`<div class="an-video-shield"></div>` +
|
|
@@ -253,7 +253,7 @@ export class VideoDialog {
|
|
|
253
253
|
|
|
254
254
|
const escapedSrc = safeSrc.replace(/"/g, '%22');
|
|
255
255
|
return (
|
|
256
|
-
`<div class="an-video-wrapper" style="position:relative;display:
|
|
256
|
+
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
257
257
|
`<video src="${escapedSrc}" width="${width}" height="${height}" controls ` +
|
|
258
258
|
`style="display:block;max-width:100%"></video>` +
|
|
259
259
|
`<div class="an-video-shield"></div>` +
|