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.
@@ -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
- const cell = e.target.closest('td, th');
111
- if (!cell || !editable.contains(cell)) { clearHover(); return; }
112
- const rect = cell.getBoundingClientRect();
113
- const onRight = Math.abs(e.clientX - rect.right) < HIT;
114
- const onBottom = Math.abs(e.clientY - rect.bottom) < HIT;
115
- if (onRight && onBottom) {
116
- // Corner — prefer col-resize
117
- cell.style.cursor = 'col-resize';
118
- _nearCell = cell; _nearEdge = 'col';
119
- } else if (onRight) {
120
- cell.style.cursor = 'col-resize';
121
- _nearCell = cell; _nearEdge = 'col';
122
- } else if (onBottom) {
123
- cell.style.cursor = 'row-resize';
124
- _nearCell = cell; _nearEdge = 'row';
125
- } else {
126
- clearHover();
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 = _nearCell.offsetWidth;
139
- _colIdx = Array.from(_nearCell.closest('tr').cells).indexOf(_nearCell);
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 (_edge === 'col') {
154
- const newW = Math.max(30, _startW + (e.clientX - _startX));
155
- if (_table && _colIdx >= 0) {
156
- Array.from(_table.querySelectorAll('tr')).forEach((r) => {
157
- const c = r.cells[_colIdx];
158
- if (c) { c.style.width = `${newW}px`; c.style.minWidth = `${newW}px`; }
159
- });
160
- }
161
- } else {
162
- const newH = Math.max(20, _startH + (e.clientY - _startY));
163
- if (_row) {
164
- Array.from(_row.cells).forEach((c) => {
165
- c.style.height = `${newH}px`;
166
- c.style.minHeight = `${newH}px`;
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 = null;
178
- _table = null;
179
- _row = null;
180
- _colIdx = -1;
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
- this._positionNear(this._activeTable);
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
- this._positionNear(this._activeTable);
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 (!row || !table) return;
394
- const colIndex = Array.from(row.cells).indexOf(cell);
395
- Array.from(table.querySelectorAll('tr')).forEach((r) => {
396
- const cells = Array.from(r.cells);
397
- const isHeader = r.closest('thead') !== null;
398
- const newCell = createElement(isHeader ? 'th' : 'td', {}, ['\u00a0']);
399
- const ref = position === 'left' ? cells[colIndex] : (cells[colIndex + 1] || null);
400
- r.insertBefore(newCell, ref);
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 (!row || !table) return;
428
- if (row.cells.length <= 1) return;
429
- const colIndex = Array.from(row.cells).indexOf(cell);
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')).forEach((r) => {
432
- const c = r.cells[colIndex];
433
- if (c) r.removeChild(c);
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 selected = Array.from(row.cells).filter((c) => {
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
- const first = selected[0];
452
- first.colSpan = selected.reduce((sum, c) => sum + (c.colSpan || 1), 0);
453
- first.innerHTML = selected.map((c) => c.innerHTML).join('');
454
- selected.slice(1).forEach((c) => row.removeChild(c));
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 = cell.closest('table');
536
- const colIndex = Array.from(cell.closest('tr').cells).indexOf(cell);
537
- Array.from(table.querySelectorAll('tr')).forEach((r) => {
538
- const c = r.cells[colIndex];
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) Array.from(row.cells).forEach((c) => { c.style.height = `${val}px`; c.style.minHeight = `${val}px`; });
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
  };
@@ -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
- this.context.invoke('editor.focus');
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
- this.context.invoke('editor.focus');
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, 'click', (e) => {
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, 'click', (e) => {
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
- if (isOpen && !wrap.contains(e.target)) closePopup();
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
- this._disposers.push(d1, d2, d3, d4, d5, d6);
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
- wrap.appendChild(popup);
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 = (typeof item === 'object') ? item.value : item;
339
- const label = (typeof item === 'object') ? item.label : item;
340
- const opt = createElement('option', { value }, [label]);
341
- if (def.name === 'fontFamily') opt.style.fontFamily = value;
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
- if (!value) return;
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
- if (document.querySelector('.fa, .fas, .far, .fal, .fab, .fa-solid')) return true;
506
- const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).map((l) => l.href || '').join(' ');
507
- return /fontawesome|font-awesome|use\.fontawesome|all\.css/.test(links);
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:inline-block;max-width:100%">` +
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:inline-block;max-width:100%">` +
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:inline-block;max-width:100%">` +
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>` +