autumnnote 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +4 -4
  2. package/dist/autumnnote.css +0 -2
  3. package/dist/autumnnote.es.js +468 -552
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +461 -546
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +21 -3
  8. package/src/js/Context.js +21 -16
  9. package/src/js/core/dom.js +9 -9
  10. package/src/js/core/env.js +1 -1
  11. package/src/js/core/func.js +1 -1
  12. package/src/js/core/lists.js +1 -1
  13. package/src/js/core/markdown.js +18 -18
  14. package/src/js/core/range.js +6 -6
  15. package/src/js/editing/History.js +4 -4
  16. package/src/js/editing/Style.js +32 -32
  17. package/src/js/editing/Table.js +2 -2
  18. package/src/js/editing/Typing.js +15 -15
  19. package/src/js/index.js +1 -1
  20. package/src/js/module/AutoSaveRestore.js +2 -4
  21. package/src/js/module/BaseDialog.js +126 -0
  22. package/src/js/module/BubbleToolbar.js +25 -25
  23. package/src/js/module/Buttons.js +4 -4
  24. package/src/js/module/Clipboard.js +12 -13
  25. package/src/js/module/CodeTooltip.js +14 -16
  26. package/src/js/module/Codeview.js +3 -5
  27. package/src/js/module/ContextMenu.js +27 -27
  28. package/src/js/module/Editor.js +17 -18
  29. package/src/js/module/EmojiDialog.js +5 -5
  30. package/src/js/module/FindReplace.js +8 -7
  31. package/src/js/module/IconDialog.js +13 -15
  32. package/src/js/module/ImageCropOverlay.js +7 -7
  33. package/src/js/module/ImageDialog.js +12 -82
  34. package/src/js/module/ImageResizer.js +4 -4
  35. package/src/js/module/ImageTooltip.js +14 -14
  36. package/src/js/module/LinkDialog.js +13 -79
  37. package/src/js/module/LinkTooltip.js +12 -12
  38. package/src/js/module/MarkdownShortcuts.js +11 -14
  39. package/src/js/module/Mention.js +8 -10
  40. package/src/js/module/Placeholder.js +1 -1
  41. package/src/js/module/ShortcutsDialog.js +2 -4
  42. package/src/js/module/Statusbar.js +3 -5
  43. package/src/js/module/TableTooltip.js +30 -32
  44. package/src/js/module/Toolbar.js +21 -21
  45. package/src/js/module/VideoDialog.js +17 -87
  46. package/src/js/module/VideoResizer.js +5 -7
  47. package/src/js/module/VideoTooltip.js +7 -7
  48. package/src/js/renderer.js +1 -1
  49. package/src/styles/autumnnote.scss +0 -3
@@ -109,9 +109,7 @@ export class Statusbar {
109
109
  this._dragDisposers.forEach((d) => d());
110
110
  this._dragDisposers = null;
111
111
  }
112
- if (this.el && this.el.parentNode) {
113
- this.el.parentNode.removeChild(this.el);
114
- }
112
+ this.el?.remove();
115
113
  this.el = null;
116
114
  }
117
115
 
@@ -217,7 +215,7 @@ export class Statusbar {
217
215
  // textContent is faster than innerText (no layout flush, no CSS visibility check)
218
216
  const text = editable.textContent || '';
219
217
  const words = _countWords(text);
220
- const chars = text.replace(/\n/g, '').length;
218
+ const chars = text.replaceAll('\n', '').length;
221
219
  const maxWords = this.options.maxWords || 0;
222
220
  const maxChars = this.options.maxChars || 0;
223
221
 
@@ -249,6 +247,6 @@ export class Statusbar {
249
247
  */
250
248
  getCharCount() {
251
249
  const editable = this.context.layoutInfo.editable;
252
- return ((editable.innerText || '').replace(/\n/g, '')).length;
250
+ return (editable.innerText || '').replaceAll('\n', '').length;
253
251
  }
254
252
  }
@@ -212,26 +212,26 @@ export class TableTooltip {
212
212
  if (!to || (
213
213
  !editable.contains(to) &&
214
214
  !this._el.contains(to) &&
215
- !(this._sizePopover && this._sizePopover.contains(to))
215
+ !this._sizePopover?.contains(to)
216
216
  )) {
217
217
  this._scheduleHide();
218
218
  }
219
219
  }, { passive: true }),
220
220
  on(document, 'click', (e) => {
221
221
  const et = /** @type {Node} */ (e.target);
222
- if (this._selectMode && this._activeTable && this._activeTable.contains(et)) return;
222
+ if (this._selectMode && this._activeTable?.contains(et)) return;
223
223
  if (this._activeTable &&
224
224
  !this._activeTable.contains(et) &&
225
225
  !this._el.contains(et) &&
226
- !(this._sizePopover && this._sizePopover.contains(et))) {
226
+ !this._sizePopover?.contains(et)) {
227
227
  this._hide();
228
228
  }
229
229
  }),
230
230
  // Sync shade strip whenever selection moves to a different cell
231
231
  on(document, 'selectionchange', () => this._syncShadeStrip()),
232
232
  // Hide when the page scrolls or resizes — the tooltip position becomes stale
233
- on(window, 'scroll', () => this._hide(), { passive: true }),
234
- on(window, 'resize', () => this._hide(), { passive: true }),
233
+ on(globalThis, 'scroll', () => this._hide(), { passive: true }),
234
+ on(globalThis, 'resize', () => this._hide(), { passive: true }),
235
235
  );
236
236
 
237
237
  this._initResize();
@@ -386,14 +386,14 @@ export class TableTooltip {
386
386
  this._clearTimers();
387
387
  this._disposers.forEach((d) => d());
388
388
  this._disposers = [];
389
- if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
389
+ if (this._el && this._el.parentNode) this._el.remove();
390
390
  this._el = null;
391
391
  if (this._sizePopover && this._sizePopover.parentNode) {
392
- this._sizePopover.parentNode.removeChild(this._sizePopover);
392
+ this._sizePopover.remove();
393
393
  }
394
394
  this._sizePopover = null;
395
395
  if (this._shadePopover && this._shadePopover.parentNode) {
396
- this._shadePopover.parentNode.removeChild(this._shadePopover);
396
+ this._shadePopover.remove();
397
397
  }
398
398
  this._shadePopover = null;
399
399
  }
@@ -553,7 +553,7 @@ export class TableTooltip {
553
553
  _syncShadeStrip() {
554
554
  if (!this._shadeColorStrip || !this._el || this._el.style.display === 'none') return;
555
555
  const cell = this._getCell();
556
- this._shadeColorStrip.style.background = (cell && cell.style.backgroundColor) || 'transparent';
556
+ this._shadeColorStrip.style.background = cell?.style.backgroundColor || 'transparent';
557
557
  }
558
558
 
559
559
  _hide() {
@@ -590,7 +590,7 @@ export class TableTooltip {
590
590
  let top = rect.top - tipH - margin;
591
591
 
592
592
  if (top < margin) top = rect.bottom + margin;
593
- if (left + tipW > window.innerWidth - margin) left = window.innerWidth - tipW - margin;
593
+ if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
594
594
  if (left < margin) left = margin;
595
595
 
596
596
  this._el.style.left = `${left}px`;
@@ -603,17 +603,17 @@ export class TableTooltip {
603
603
 
604
604
  _getCell() {
605
605
  // Prefer the cell under the current text cursor (most intuitive for operations)
606
- const sel = window.getSelection();
606
+ const sel = globalThis.getSelection();
607
607
  if (sel && sel.rangeCount) {
608
608
  let container = sel.getRangeAt(0).commonAncestorContainer;
609
609
  if (container.nodeType === 3) container = container.parentElement;
610
- const cellFromSel = container && /** @type {Element} */ (container).closest('td, th');
611
- if (cellFromSel && this._activeTable && this._activeTable.contains(cellFromSel)) {
610
+ const cellFromSel = /** @type {Element} */ (container)?.closest('td, th');
611
+ if (cellFromSel && this._activeTable?.contains(cellFromSel)) {
612
612
  return cellFromSel;
613
613
  }
614
614
  }
615
615
  return this._activeCell
616
- || (this._activeTable && this._activeTable.querySelector('td, th'));
616
+ || this._activeTable?.querySelector('td, th');
617
617
  }
618
618
 
619
619
  // ---------------------------------------------------------------------------
@@ -735,7 +735,7 @@ export class TableTooltip {
735
735
  const bi = allRows.indexOf(best);
736
736
  const ri = allRows.indexOf(r);
737
737
  return position === 'above' ? (ri < bi ? r : best) : (ri > bi ? r : best);
738
- });
738
+ }, selectedRows[0]);
739
739
  const colCount = Array.from(refRow.cells).reduce((sum, c) => sum + (c.colSpan || 1), 0);
740
740
  const newRow = document.createElement('tr');
741
741
  const refCells = Array.from(refRow.cells);
@@ -786,7 +786,7 @@ export class TableTooltip {
786
786
  if (bodyRowsToDelete.length >= totalBodyRows) return;
787
787
  this._activeCell = null;
788
788
  this._clearSelection();
789
- selectedRows.forEach((r) => r.parentElement?.removeChild(r));
789
+ selectedRows.forEach((r) => r.remove());
790
790
  requestAnimationFrame(() => this._positionNear(this._activeTable));
791
791
  this.context.invoke('editor.afterCommand');
792
792
  }
@@ -810,7 +810,7 @@ export class TableTooltip {
810
810
  });
811
811
  this._activeCell = null;
812
812
  this._clearSelection();
813
- cellsToDelete.forEach((c) => c.parentElement?.removeChild(c));
813
+ cellsToDelete.forEach((c) => c.remove());
814
814
  requestAnimationFrame(() => this._positionNear(this._activeTable));
815
815
  this.context.invoke('editor.afterCommand');
816
816
  }
@@ -824,7 +824,7 @@ export class TableTooltip {
824
824
  // Prefer user panel-selected cells; fall back to text-selection range
825
825
  let selected = this._getSelectedCells().filter((c) => table.contains(c));
826
826
  if (selected.length < 2) {
827
- const sel = window.getSelection();
827
+ const sel = globalThis.getSelection();
828
828
  if (!sel || sel.rangeCount === 0) return;
829
829
  const range = sel.getRangeAt(0);
830
830
  const allCells = Array.from(table.querySelectorAll('td, th'));
@@ -866,7 +866,7 @@ export class TableTooltip {
866
866
  first.rowSpan = maxR - minR + 1;
867
867
  first.style.verticalAlign = 'middle';
868
868
  first.innerHTML = rectCells.map((c) => c.innerHTML).join('');
869
- rectCells.slice(1).forEach((c) => c.parentElement?.removeChild(c));
869
+ rectCells.slice(1).forEach((c) => c.remove());
870
870
 
871
871
  this._clearSelection();
872
872
  this.context.invoke('editor.afterCommand');
@@ -876,7 +876,7 @@ export class TableTooltip {
876
876
  const table = this._activeTable;
877
877
  if (!table) return;
878
878
  this._hide();
879
- if (table.parentNode) table.parentNode.removeChild(table);
879
+ if (table.parentNode) table.remove();
880
880
  this.context.invoke('editor.afterCommand');
881
881
  }
882
882
 
@@ -981,7 +981,7 @@ export class TableTooltip {
981
981
  this._sizeApply = null;
982
982
 
983
983
  const d1 = on(applyBtn, 'click', () => {
984
- const val = parseInt(this._sizeInputEl.value, 10);
984
+ const val = Number.parseInt(this._sizeInputEl.value, 10);
985
985
  if (val > 0 && typeof this._sizeApply === 'function') this._sizeApply(val);
986
986
  this._hideSizePopover();
987
987
  });
@@ -1016,8 +1016,8 @@ export class TableTooltip {
1016
1016
  if (!table) return;
1017
1017
  const firstCell = table.querySelector('td, th');
1018
1018
  const currentPx = firstCell
1019
- ? (parseInt(firstCell.style.borderWidth, 10) ||
1020
- parseInt(window.getComputedStyle(firstCell).borderWidth, 10) || 1)
1019
+ ? (Number.parseInt(firstCell.style.borderWidth, 10) ||
1020
+ Number.parseInt(globalThis.getComputedStyle(firstCell).borderWidth, 10) || 1)
1021
1021
  : 1;
1022
1022
  this._sizeTitleEl.textContent = this.context.locale.tooltips.table.tableBorderWidthPx;
1023
1023
  this._sizeInputEl.min = '0';
@@ -1079,8 +1079,8 @@ export class TableTooltip {
1079
1079
  const ph = this._sizePopover.offsetHeight || 110;
1080
1080
  let left = tipRect.left;
1081
1081
  let top = tipRect.bottom + 6;
1082
- if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
1083
- if (top + ph > window.innerHeight - 8) top = tipRect.top - ph - 6;
1082
+ if (left + pw > globalThis.innerWidth - 8) left = globalThis.innerWidth - pw - 8;
1083
+ if (top + ph > globalThis.innerHeight - 8) top = tipRect.top - ph - 6;
1084
1084
  this._sizePopover.style.left = `${left}px`;
1085
1085
  this._sizePopover.style.top = `${top}px`;
1086
1086
  if (this._sizeInputEl) { this._sizeInputEl.focus(); this._sizeInputEl.select(); }
@@ -1135,11 +1135,9 @@ export class TableTooltip {
1135
1135
  customRow.appendChild(customLabel);
1136
1136
  pop.appendChild(customRow);
1137
1137
 
1138
- // Prevent mousedown from collapsing editor selection
1139
- this._disposers.push(on(pop, 'mousedown', (e) => e.preventDefault()));
1140
-
1141
- // Keep tooltip alive while hovering popover
1138
+ // Prevent mousedown from collapsing editor selection; keep tooltip alive while hovering
1142
1139
  this._disposers.push(
1140
+ on(pop, 'mousedown', (e) => e.preventDefault()),
1143
1141
  on(pop, 'mouseenter', () => this._clearTimers()),
1144
1142
  on(pop, 'mouseleave', () => this._scheduleHide()),
1145
1143
  );
@@ -1150,7 +1148,7 @@ export class TableTooltip {
1150
1148
  if (this._shadePopover &&
1151
1149
  this._shadePopover.style.display !== 'none' &&
1152
1150
  !this._shadePopover.contains(et) &&
1153
- !(this._el && this._el.contains(et))) {
1151
+ !this._el?.contains(et)) {
1154
1152
  this._hideCellShadePopover();
1155
1153
  }
1156
1154
  }));
@@ -1172,8 +1170,8 @@ export class TableTooltip {
1172
1170
  const tipRect = this._el.getBoundingClientRect();
1173
1171
  let left = tipRect.left;
1174
1172
  let top = tipRect.bottom + 6;
1175
- if (left + pw > window.innerWidth - 8) left = window.innerWidth - pw - 8;
1176
- if (top + ph > window.innerHeight - 8) top = tipRect.top - ph - 6;
1173
+ if (left + pw > globalThis.innerWidth - 8) left = globalThis.innerWidth - pw - 8;
1174
+ if (top + ph > globalThis.innerHeight - 8) top = tipRect.top - ph - 6;
1177
1175
  this._shadePopover.style.left = `${Math.max(8, left)}px`;
1178
1176
  this._shadePopover.style.top = `${Math.max(8, top)}px`;
1179
1177
  });
@@ -146,7 +146,7 @@ export class Toolbar {
146
146
  this._disposers.forEach((d) => d());
147
147
  this._disposers = [];
148
148
  if (this.el && this.el.parentNode) {
149
- this.el.parentNode.removeChild(this.el);
149
+ this.el.remove();
150
150
  }
151
151
  this.el = null;
152
152
  }
@@ -244,8 +244,8 @@ export class Toolbar {
244
244
 
245
245
  const setHighlight = (rows, cols) => {
246
246
  cells.forEach((cell) => {
247
- const r = +cell.getAttribute('data-row');
248
- const c = +cell.getAttribute('data-col');
247
+ const r = +cell.dataset.row;
248
+ const c = +cell.dataset.col;
249
249
  cell.classList.toggle('active', r <= rows && c <= cols);
250
250
  });
251
251
  label.textContent = (rows && cols) ? `${rows} × ${cols}` : (this.context.locale.toolbar.insertTableLabel || 'Insert Table');
@@ -264,8 +264,8 @@ export class Toolbar {
264
264
 
265
265
  let left = rect.left;
266
266
  let top = rect.bottom + 4;
267
- if (left + pw > window.innerWidth - 8) left = Math.max(8, window.innerWidth - pw - 8);
268
- if (top + ph > window.innerHeight - 8) top = rect.top - ph - 4;
267
+ if (left + pw > globalThis.innerWidth - 8) left = Math.max(8, globalThis.innerWidth - pw - 8);
268
+ if (top + ph > globalThis.innerHeight - 8) top = rect.top - ph - 4;
269
269
 
270
270
  popup.style.left = `${left}px`;
271
271
  popup.style.top = `${top}px`;
@@ -286,18 +286,18 @@ export class Toolbar {
286
286
  });
287
287
 
288
288
  const d2 = on(grid, 'mouseover', (e) => {
289
- const cell = /** @type {Element} */ (e.target)?.closest('.an-table-cell');
289
+ const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
290
290
  if (!cell) return;
291
- setHighlight(+cell.getAttribute('data-row'), +cell.getAttribute('data-col'));
291
+ setHighlight(+cell.dataset.row, +cell.dataset.col);
292
292
  });
293
293
 
294
294
  const d3 = on(grid, 'mouseleave', () => setHighlight(0, 0));
295
295
 
296
296
  const d4 = on(grid, 'click', (e) => {
297
- const cell = /** @type {Element} */ (e.target)?.closest('.an-table-cell');
297
+ const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
298
298
  if (!cell) return;
299
- const rows = +cell.getAttribute('data-row');
300
- const cols = +cell.getAttribute('data-col');
299
+ const rows = +cell.dataset.row;
300
+ const cols = +cell.dataset.col;
301
301
  closePopup();
302
302
  this.context.invoke('editor.focus');
303
303
  def.action(this.context, rows, cols);
@@ -308,7 +308,7 @@ export class Toolbar {
308
308
  // Append popup to body so position:fixed is truly viewport-relative,
309
309
  // unaffected by any ancestor transform / filter (same pattern as color picker).
310
310
  this._disposers.push(d1, d2, d3, d4, d5, () => {
311
- if (popup.parentNode) popup.parentNode.removeChild(popup);
311
+ if (popup.parentNode) popup.remove();
312
312
  });
313
313
 
314
314
  wrap.appendChild(btn);
@@ -398,14 +398,14 @@ export class Toolbar {
398
398
  let savedRange = null;
399
399
 
400
400
  const saveSelection = () => {
401
- const sel = window.getSelection();
401
+ const sel = globalThis.getSelection();
402
402
  savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
403
403
  };
404
404
 
405
405
  const restoreSelection = () => {
406
406
  if (!savedRange) return;
407
407
  try {
408
- const sel = window.getSelection();
408
+ const sel = globalThis.getSelection();
409
409
  if (!sel) return;
410
410
  sel.removeAllRanges();
411
411
  sel.addRange(savedRange);
@@ -424,7 +424,7 @@ export class Toolbar {
424
424
  const rect = arrowBtn.getBoundingClientRect();
425
425
  const popupMinW = 184;
426
426
  let left = rect.left;
427
- if (left + popupMinW > window.innerWidth) left = rect.right - popupMinW;
427
+ if (left + popupMinW > globalThis.innerWidth) left = rect.right - popupMinW;
428
428
  popup.style.top = `${rect.bottom + 4}px`;
429
429
  popup.style.left = `${Math.max(4, left)}px`;
430
430
  popup.style.display = 'block';
@@ -491,13 +491,13 @@ export class Toolbar {
491
491
  // popup doesn't drift away from the button it belongs to.
492
492
  const onScrollResize = () => { if (isOpen) closePopup(); };
493
493
  document.addEventListener('scroll', onScrollResize, { passive: true, capture: true });
494
- window.addEventListener('resize', onScrollResize, { passive: true });
494
+ globalThis.addEventListener('resize', onScrollResize, { passive: true });
495
495
 
496
496
  this._disposers.push(d1, d2, d2b, d3, d3b, d4, d5, d6,
497
497
  () => document.removeEventListener('scroll', onScrollResize, { capture: true }),
498
- () => window.removeEventListener('resize', onScrollResize),
498
+ () => globalThis.removeEventListener('resize', onScrollResize),
499
499
  // Remove popup from body on editor destroy
500
- () => { if (popup.parentNode) popup.parentNode.removeChild(popup); },
500
+ () => { if (popup.parentNode) popup.remove(); },
501
501
  );
502
502
 
503
503
  // Register this popup's closer so other color pickers can close it
@@ -562,7 +562,7 @@ export class Toolbar {
562
562
  /** @type {Range|null} */
563
563
  let _savedRange = null;
564
564
  const dMousedown = on(select, 'mousedown', () => {
565
- const sel = window.getSelection();
565
+ const sel = globalThis.getSelection();
566
566
  _savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
567
567
  });
568
568
 
@@ -574,7 +574,7 @@ export class Toolbar {
574
574
  // Restore selection saved on mousedown so the action targets the correct text.
575
575
  if (_savedRange) {
576
576
  try {
577
- const sel = window.getSelection();
577
+ const sel = globalThis.getSelection();
578
578
  if (sel) { sel.removeAllRanges(); sel.addRange(_savedRange); }
579
579
  } catch (_) { /* range may be stale if DOM changed */ }
580
580
  }
@@ -684,7 +684,7 @@ export class Toolbar {
684
684
 
685
685
  // Sync button active states
686
686
  this.el.querySelectorAll('button[data-btn]').forEach((btn) => {
687
- const def = btnMap.get(btn.getAttribute('data-btn'));
687
+ const def = btnMap.get(/** @type {HTMLElement} */ (btn).dataset.btn);
688
688
  if (def && typeof def.isActive === 'function') {
689
689
  btn.classList.toggle('active', !!def.isActive(this.context));
690
690
  }
@@ -695,7 +695,7 @@ export class Toolbar {
695
695
 
696
696
  // Sync select dropdowns (e.g. font family) with current cursor position
697
697
  this.el.querySelectorAll('select[data-btn]').forEach((select) => {
698
- const def = btnMap.get(select.getAttribute('data-btn'));
698
+ const def = btnMap.get(/** @type {HTMLElement} */ (select).dataset.btn);
699
699
  if (!def || typeof def.getValue !== 'function') return;
700
700
  // queryCommandValue returns the font name, possibly quoted — strip quotes
701
701
  let raw = (def.getValue(this.context) || '').replace(/["']/g, '').trim();
@@ -7,47 +7,18 @@
7
7
  * • Direct video URLs → <video> element (.mp4 / .webm / .ogg)
8
8
  */
9
9
 
10
- import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
11
- import { withSavedRange } from '../core/range.js';
10
+ import { createElement, on } from '../core/dom.js';
11
+ import { BaseDialog } from './BaseDialog.js';
12
12
 
13
- export class VideoDialog {
14
- /** @param {import('../Context.js').Context} context */
15
- constructor(context) {
16
- this.context = context;
17
- this.options = context.options;
18
- /** @type {HTMLElement|null} */
19
- this._dialog = null;
20
- this._disposers = [];
21
- this._savedRange = null;
22
- }
23
-
24
- // ---------------------------------------------------------------------------
25
- // Lifecycle
26
- // ---------------------------------------------------------------------------
27
-
28
- initialize() {
29
- this._dialog = this._buildDialog();
30
- document.body.appendChild(this._dialog);
31
- return this;
32
- }
33
-
34
- destroy() {
35
- this._disposers.forEach((d) => d());
36
- this._disposers = [];
37
- if (this._dialog && this._dialog.parentNode) {
38
- this._dialog.parentNode.removeChild(this._dialog);
39
- }
40
- this._dialog = null;
41
- }
13
+ const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`;
42
14
 
15
+ export class VideoDialog extends BaseDialog {
43
16
  // ---------------------------------------------------------------------------
44
17
  // Public API
45
18
  // ---------------------------------------------------------------------------
46
19
 
47
20
  show() {
48
- withSavedRange((range) => {
49
- this._savedRange = range;
50
- });
21
+ this._saveRange();
51
22
  this._urlInput.value = '';
52
23
  this._widthInput.value = '560';
53
24
  this._hintEl.textContent = '';
@@ -60,21 +31,7 @@ export class VideoDialog {
60
31
 
61
32
  _buildDialog() {
62
33
  const L = this.context.locale.videoDialog;
63
- const overlay = createElement('div', {
64
- class: 'an-dialog-overlay',
65
- role: 'dialog',
66
- 'aria-modal': 'true',
67
- 'aria-label': L.ariaLabel,
68
- });
69
- const box = createElement('div', { class: 'an-dialog-box' });
70
-
71
- const header = createElement('div', { class: 'an-dialog-header' });
72
- const iconEl = createElement('span', { class: 'an-dialog-icon' });
73
- iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`;
74
- const title = createElement('h3', { class: 'an-dialog-title' });
75
- title.textContent = L.title;
76
- header.appendChild(iconEl);
77
- header.appendChild(title);
34
+ const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
78
35
 
79
36
  // URL input
80
37
  const urlLabel = createElement('label', { class: 'an-label' });
@@ -86,6 +43,7 @@ export class VideoDialog {
86
43
  autocomplete: 'off',
87
44
  }));
88
45
  this._urlInput = urlInput;
46
+ this._firstInput = urlInput;
89
47
 
90
48
  // Hint (detected source)
91
49
  const hintEl = createElement('p', { class: 'an-dialog-hint' });
@@ -104,30 +62,16 @@ export class VideoDialog {
104
62
  }));
105
63
  this._widthInput = widthInput;
106
64
 
107
- // Buttons
108
- const btnRow = createElement('div', { class: 'an-dialog-actions' });
109
- const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
110
- insertBtn.textContent = L.insertBtn;
111
- const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
112
- cancelBtn.textContent = L.cancelBtn;
113
- btnRow.appendChild(insertBtn);
114
- btnRow.appendChild(cancelBtn);
115
-
116
- box.append(header, urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
117
- overlay.appendChild(box);
118
- makeDraggable(header, box);
65
+ const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
66
+ box.append(urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
119
67
 
120
68
  // Live URL hint
121
69
  const d0 = on(urlInput, 'input', () => {
122
70
  const info = this._parseVideoUrl(urlInput.value.trim());
123
71
  hintEl.textContent = info ? this.context.locale.videoDialog.detected(info.type) : (urlInput.value ? this.context.locale.videoDialog.unknownFormat : '');
124
72
  });
125
-
126
- const d1 = on(insertBtn, 'click', () => this._onInsert());
127
- const d2 = on(cancelBtn, 'click', () => this._close());
128
- const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
129
73
  const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
130
- this._disposers.push(d0, d1, d2, d3, d4);
74
+ this._disposers.push(d0, d4);
131
75
 
132
76
  return overlay;
133
77
  }
@@ -138,7 +82,7 @@ export class VideoDialog {
138
82
 
139
83
  _onInsert() {
140
84
  const rawUrl = this._urlInput.value.trim();
141
- const width = Math.max(80, parseInt(this._widthInput.value, 10) || 560);
85
+ const width = Math.max(80, Number.parseInt(this._widthInput.value, 10) || 560);
142
86
 
143
87
  if (!rawUrl) {
144
88
  this._urlInput.focus();
@@ -157,20 +101,6 @@ export class VideoDialog {
157
101
  this._close();
158
102
  }
159
103
 
160
- _open() {
161
- if (this._dialog) {
162
- this._dialog.style.display = 'flex';
163
- this._removeTrap = trapFocus(this._dialog, () => this._close());
164
- setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
165
- }
166
- }
167
-
168
- _close() {
169
- if (this._dialog) this._dialog.style.display = 'none';
170
- if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
171
- this._savedRange = null;
172
- }
173
-
174
104
  // ---------------------------------------------------------------------------
175
105
  // URL parsing & HTML building
176
106
  // ---------------------------------------------------------------------------
@@ -190,19 +120,19 @@ export class VideoDialog {
190
120
  } catch { return null; }
191
121
 
192
122
  // YouTube watch: https://www.youtube.com/watch?v=ID
193
- const ytWatch = url.match(/(?:youtube\.com\/watch\?(?:.*&)?v=|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/);
123
+ const ytWatch = /(?:youtube\.com\/watch\?(?:.*&)?v=|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/.exec(url);
194
124
  if (ytWatch) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytWatch[1]}` };
195
125
 
196
126
  // YouTube short: https://youtu.be/ID
197
- const ytShort = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
127
+ const ytShort = /youtu\.be\/([a-zA-Z0-9_-]{11})/.exec(url);
198
128
  if (ytShort) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytShort[1]}` };
199
129
 
200
130
  // YouTube Shorts: https://www.youtube.com/shorts/ID
201
- const ytShorts = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/);
131
+ const ytShorts = /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/.exec(url);
202
132
  if (ytShorts) return { type: 'YouTube Shorts', embedUrl: `https://www.youtube.com/embed/${ytShorts[1]}` };
203
133
 
204
134
  // Vimeo: https://vimeo.com/ID
205
- const vimeo = url.match(/vimeo\.com\/(\d+)/);
135
+ const vimeo = /vimeo\.com\/(\d+)/.exec(url);
206
136
  if (vimeo) return { type: 'Vimeo', embedUrl: `https://player.vimeo.com/video/${vimeo[1]}` };
207
137
 
208
138
  // Direct video file
@@ -238,7 +168,7 @@ export class VideoDialog {
238
168
  }
239
169
 
240
170
  if (info && info.type === 'Direct video') {
241
- const src = info.embedUrl.replace(/"/g, '%22');
171
+ const src = info.embedUrl.replaceAll('"', '%22');
242
172
  return (
243
173
  `<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
244
174
  `<video src="${src}" width="${width}" height="${height}" controls ` +
@@ -258,7 +188,7 @@ export class VideoDialog {
258
188
  })();
259
189
  if (!safeSrc) return null;
260
190
 
261
- const escapedSrc = safeSrc.replace(/"/g, '%22');
191
+ const escapedSrc = safeSrc.replaceAll('"', '%22');
262
192
  return (
263
193
  `<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
264
194
  `<video src="${escapedSrc}" width="${width}" height="${height}" controls ` +
@@ -46,8 +46,8 @@ export class VideoResizer {
46
46
  if (wrapper) this._select(wrapper);
47
47
  }),
48
48
  on(document, 'click', (e) => this._onDocClick(e)),
49
- on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
50
- on(window, 'resize', onWindowResize),
49
+ on(globalThis, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
50
+ on(globalThis, 'resize', onWindowResize),
51
51
  on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
52
52
  // D1: Prevent native browser drag of video wrappers. Without this, a user
53
53
  // can hold-and-drag to produce a "copy" that lands outside .an-editable,
@@ -75,9 +75,7 @@ export class VideoResizer {
75
75
  this._positionRaf = null;
76
76
  }
77
77
  this._deselect();
78
- if (this._overlay && this._overlay.parentNode) {
79
- this._overlay.parentNode.removeChild(this._overlay);
80
- }
78
+ this._overlay?.remove();
81
79
  this._overlay = null;
82
80
  }
83
81
 
@@ -111,7 +109,7 @@ export class VideoResizer {
111
109
  _findWrapper(el) {
112
110
  if (!el || !(el instanceof Element)) return null;
113
111
  // Direct hit on wrapper
114
- if (el.classList && el.classList.contains('an-video-wrapper')) return /** @type {HTMLElement} */ (el);
112
+ if (el.classList?.contains('an-video-wrapper')) return /** @type {HTMLElement} */ (el);
115
113
  // Child element (iframe, video, or nested)
116
114
  const w = el.closest('.an-video-wrapper');
117
115
  if (w) return /** @type {HTMLElement} */ (w);
@@ -152,7 +150,7 @@ export class VideoResizer {
152
150
  _onDocClick(e) {
153
151
  if (!this._activeWrapper) return;
154
152
  if (this._activeWrapper.contains(e.target)) return;
155
- if (this._overlay && this._overlay.contains(e.target)) return;
153
+ if (this._overlay?.contains(e.target)) return;
156
154
  if (e.target.closest('.an-contextmenu')) return;
157
155
  this._deselect();
158
156
  }
@@ -40,7 +40,7 @@ export class VideoTooltip {
40
40
  if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
41
41
  // The shield div sits on top of iframes — we detect hover via it or the wrapper
42
42
  const target = /** @type {Element} */ (e.target);
43
- const wrapper = target && target.closest ? target.closest('.an-video-wrapper') : null;
43
+ const wrapper = target?.closest('.an-video-wrapper');
44
44
  if (wrapper && editable.contains(wrapper)) {
45
45
  this._scheduleShow(wrapper);
46
46
  }
@@ -62,8 +62,8 @@ export class VideoTooltip {
62
62
  }
63
63
  }),
64
64
  // Hide when the page scrolls or resizes — the tooltip position becomes stale
65
- on(window, 'scroll', () => this._hide(), { passive: true }),
66
- on(window, 'resize', () => this._hide(), { passive: true }),
65
+ on(globalThis, 'scroll', () => this._hide(), { passive: true }),
66
+ on(globalThis, 'resize', () => this._hide(), { passive: true }),
67
67
  );
68
68
 
69
69
  return this;
@@ -74,7 +74,7 @@ export class VideoTooltip {
74
74
  this._clearTimers();
75
75
  this._disposers.forEach((d) => d());
76
76
  this._disposers = [];
77
- if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
77
+ this._el?.remove();
78
78
  this._el = null;
79
79
  }
80
80
 
@@ -210,8 +210,8 @@ export class VideoTooltip {
210
210
  let top = rect.bottom + margin;
211
211
  let left = rect.left + (rect.width - tipW) / 2;
212
212
 
213
- if (top + tipH > window.innerHeight - margin) top = rect.top - tipH - margin;
214
- if (left + tipW > window.innerWidth - margin) left = window.innerWidth - tipW - margin;
213
+ if (top + tipH > globalThis.innerHeight - margin) top = rect.top - tipH - margin;
214
+ if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
215
215
  if (left < margin) left = margin;
216
216
 
217
217
  this._el.style.top = `${top}px`;
@@ -268,7 +268,7 @@ export class VideoTooltip {
268
268
  if (!wrapper) return;
269
269
  this._hide();
270
270
  this.context.invoke('videoResizer.deselect');
271
- if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
271
+ wrapper.remove();
272
272
  this.context.invoke('editor.afterCommand');
273
273
  }
274
274
 
@@ -46,7 +46,7 @@ export function renderLayout(targetEl, options) {
46
46
  editable.innerHTML = sanitiseHTML(initialContent, { allowIframes: true });
47
47
 
48
48
  // Apply default font family so the editable renders in the configured font
49
- const defaultFont = options.defaultFontFamily || (options.fontFamilies && options.fontFamilies[0]);
49
+ const defaultFont = options.defaultFontFamily || options.fontFamilies?.[0];
50
50
  if (defaultFont) {
51
51
  editable.style.fontFamily = defaultFont;
52
52
  }