autumnnote 1.5.0 → 1.6.1

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 (53) hide show
  1. package/README.md +10 -8
  2. package/dist/autumnnote.css +324 -4
  3. package/dist/autumnnote.es.js +943 -526
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +935 -519
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +21 -3
  8. package/src/js/Context.js +23 -16
  9. package/src/js/core/detectLang.js +98 -0
  10. package/src/js/core/dom.js +67 -11
  11. package/src/js/core/env.js +1 -1
  12. package/src/js/core/func.js +1 -1
  13. package/src/js/core/lists.js +1 -1
  14. package/src/js/core/markdown.js +32 -31
  15. package/src/js/core/range.js +8 -8
  16. package/src/js/editing/History.js +10 -10
  17. package/src/js/editing/Style.js +44 -44
  18. package/src/js/editing/Table.js +5 -7
  19. package/src/js/editing/Typing.js +19 -19
  20. package/src/js/i18n/en.js +2 -0
  21. package/src/js/i18n/vi.js +2 -0
  22. package/src/js/index.js +3 -2
  23. package/src/js/module/AutoSaveRestore.js +2 -4
  24. package/src/js/module/BubbleToolbar.js +51 -34
  25. package/src/js/module/Buttons.js +20 -18
  26. package/src/js/module/Clipboard.js +16 -17
  27. package/src/js/module/CodeTooltip.js +48 -24
  28. package/src/js/module/Codeview.js +5 -7
  29. package/src/js/module/ContextMenu.js +37 -37
  30. package/src/js/module/Editor.js +77 -26
  31. package/src/js/module/EmojiDialog.js +24 -18
  32. package/src/js/module/FindReplace.js +93 -66
  33. package/src/js/module/Fullscreen.js +1 -1
  34. package/src/js/module/IconDialog.js +39 -35
  35. package/src/js/module/ImageCropOverlay.js +13 -13
  36. package/src/js/module/ImageDialog.js +19 -13
  37. package/src/js/module/ImageResizer.js +5 -5
  38. package/src/js/module/ImageTooltip.js +20 -16
  39. package/src/js/module/LinkDialog.js +20 -14
  40. package/src/js/module/LinkTooltip.js +15 -12
  41. package/src/js/module/MarkdownShortcuts.js +11 -14
  42. package/src/js/module/Mention.js +11 -13
  43. package/src/js/module/Placeholder.js +1 -1
  44. package/src/js/module/ShortcutsDialog.js +2 -4
  45. package/src/js/module/Statusbar.js +5 -7
  46. package/src/js/module/TableTooltip.js +196 -36
  47. package/src/js/module/Toolbar.js +35 -34
  48. package/src/js/module/VideoDialog.js +16 -10
  49. package/src/js/module/VideoResizer.js +7 -9
  50. package/src/js/module/VideoTooltip.js +15 -10
  51. package/src/js/renderer.js +5 -3
  52. package/src/js/settings.js +53 -36
  53. package/src/styles/autumnnote.scss +332 -7
@@ -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 = 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 = 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,12 +308,12 @@ 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);
315
315
  document.body.appendChild(popup);
316
- return wrap;
316
+ return /** @type {HTMLDivElement} */ (wrap);
317
317
  }
318
318
 
319
319
  /**
@@ -384,7 +384,7 @@ export class Toolbar {
384
384
  });
385
385
 
386
386
  const customRow = createElement('div', { class: 'an-color-custom' });
387
- const colorInput = createElement('input', { type: 'color', value: currentColor, title: this.context.locale.toolbar.customColor || 'Custom color' });
387
+ const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', value: currentColor, title: this.context.locale.toolbar.customColor || 'Custom color' }));
388
388
  const customLabel = createElement('span', {}, [this.context.locale.toolbar.customColor || 'Custom color']);
389
389
  customRow.appendChild(colorInput);
390
390
  customRow.appendChild(customLabel);
@@ -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';
@@ -472,17 +472,17 @@ export class Toolbar {
472
472
  });
473
473
 
474
474
  const d3b = on(swatches, 'click', (e) => {
475
- const sw = e.target.closest('.an-color-swatch');
476
- if (sw) applyColor(sw.dataset.color);
475
+ const sw = /** @type {Element} */ (e.target)?.closest('.an-color-swatch');
476
+ if (sw) applyColor(/** @type {HTMLElement} */ (sw).dataset.color);
477
477
  });
478
478
 
479
479
  const d4 = on(colorInput, 'change', (e) => {
480
- applyColor(e.target.value);
480
+ applyColor(/** @type {HTMLInputElement} */ (e.target).value);
481
481
  });
482
482
 
483
483
  const d5 = on(document, 'click', (e) => {
484
484
  // popup is in document.body, not inside wrap — check both
485
- if (isOpen && !wrap.contains(e.target) && !popup.contains(e.target)) closePopup();
485
+ if (isOpen && !wrap.contains(/** @type {Node} */ (e.target)) && !popup.contains(/** @type {Node} */ (e.target))) closePopup();
486
486
  });
487
487
 
488
488
  const d6 = on(popup, 'click', (e) => e.stopPropagation());
@@ -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
@@ -513,7 +513,7 @@ export class Toolbar {
513
513
  wrap.appendChild(applyBtn);
514
514
  wrap.appendChild(arrowBtn);
515
515
  document.body.appendChild(popup);
516
- return wrap;
516
+ return /** @type {HTMLDivElement} */ (wrap);
517
517
  }
518
518
 
519
519
  /**
@@ -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
  }
@@ -583,7 +583,7 @@ export class Toolbar {
583
583
  });
584
584
 
585
585
  this._disposers.push(dMousedown, disposer);
586
- return select;
586
+ return /** @type {HTMLSelectElement} */ (select);
587
587
  }
588
588
 
589
589
  /**
@@ -638,7 +638,7 @@ export class Toolbar {
638
638
  });
639
639
 
640
640
  this._disposers.push(disposer);
641
- return btn;
641
+ return /** @type {HTMLButtonElement} */ (btn);
642
642
  }
643
643
 
644
644
  // ---------------------------------------------------------------------------
@@ -659,7 +659,7 @@ export class Toolbar {
659
659
  // count as "the host page loaded FA" for toolbar icon rendering purposes.
660
660
  const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
661
661
  .filter((l) => l.id !== 'an-fontawesome-css')
662
- .map((l) => l.href || '').join(' ');
662
+ .map((l) => /** @type {HTMLLinkElement} */ (l).href || '').join(' ');
663
663
  _faPageLevelReady = /fontawesome|font-awesome|use\.fontawesome|all\.css/.test(links);
664
664
  return _faPageLevelReady;
665
665
  }
@@ -684,18 +684,18 @@ 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
  }
691
691
  if (def && typeof def.isDisabled === 'function') {
692
- btn.disabled = !!def.isDisabled(this.context);
692
+ /** @type {HTMLButtonElement} */ (btn).disabled = !!def.isDisabled(this.context);
693
693
  }
694
694
  });
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();
@@ -706,10 +706,11 @@ export class Toolbar {
706
706
  || '';
707
707
  }
708
708
  // Try to match against available options (case-insensitive)
709
- const matched = Array.from(select.options).find(
709
+ const sel = /** @type {HTMLSelectElement} */ (select);
710
+ const matched = Array.from(sel.options).find(
710
711
  (opt) => opt.value && opt.value.toLowerCase() === raw.toLowerCase()
711
712
  );
712
- select.value = matched ? matched.value : '';
713
+ sel.value = matched ? matched.value : '';
713
714
  });
714
715
  }
715
716
 
@@ -7,7 +7,7 @@
7
7
  * • Direct video URLs → <video> element (.mp4 / .webm / .ogg)
8
8
  */
9
9
 
10
- import { createElement, on, trapFocus } from '../core/dom.js';
10
+ import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
11
11
  import { withSavedRange } from '../core/range.js';
12
12
 
13
13
  export class VideoDialog {
@@ -35,7 +35,7 @@ export class VideoDialog {
35
35
  this._disposers.forEach((d) => d());
36
36
  this._disposers = [];
37
37
  if (this._dialog && this._dialog.parentNode) {
38
- this._dialog.parentNode.removeChild(this._dialog);
38
+ this._dialog.remove();
39
39
  }
40
40
  this._dialog = null;
41
41
  }
@@ -68,8 +68,13 @@ export class VideoDialog {
68
68
  });
69
69
  const box = createElement('div', { class: 'an-dialog-box' });
70
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>`;
71
74
  const title = createElement('h3', { class: 'an-dialog-title' });
72
75
  title.textContent = L.title;
76
+ header.appendChild(iconEl);
77
+ header.appendChild(title);
73
78
 
74
79
  // URL input
75
80
  const urlLabel = createElement('label', { class: 'an-label' });
@@ -108,8 +113,9 @@ export class VideoDialog {
108
113
  btnRow.appendChild(insertBtn);
109
114
  btnRow.appendChild(cancelBtn);
110
115
 
111
- box.append(title, urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
116
+ box.append(header, urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
112
117
  overlay.appendChild(box);
118
+ makeDraggable(header, box);
113
119
 
114
120
  // Live URL hint
115
121
  const d0 = on(urlInput, 'input', () => {
@@ -132,7 +138,7 @@ export class VideoDialog {
132
138
 
133
139
  _onInsert() {
134
140
  const rawUrl = this._urlInput.value.trim();
135
- const width = Math.max(80, parseInt(this._widthInput.value, 10) || 560);
141
+ const width = Math.max(80, Number.parseInt(this._widthInput.value, 10) || 560);
136
142
 
137
143
  if (!rawUrl) {
138
144
  this._urlInput.focus();
@@ -184,19 +190,19 @@ export class VideoDialog {
184
190
  } catch { return null; }
185
191
 
186
192
  // YouTube watch: https://www.youtube.com/watch?v=ID
187
- const ytWatch = url.match(/(?:youtube\.com\/watch\?(?:.*&)?v=|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/);
193
+ const ytWatch = /(?:youtube\.com\/watch\?(?:.*&)?v=|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/.exec(url);
188
194
  if (ytWatch) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytWatch[1]}` };
189
195
 
190
196
  // YouTube short: https://youtu.be/ID
191
- const ytShort = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
197
+ const ytShort = /youtu\.be\/([a-zA-Z0-9_-]{11})/.exec(url);
192
198
  if (ytShort) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytShort[1]}` };
193
199
 
194
200
  // YouTube Shorts: https://www.youtube.com/shorts/ID
195
- const ytShorts = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/);
201
+ const ytShorts = /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/.exec(url);
196
202
  if (ytShorts) return { type: 'YouTube Shorts', embedUrl: `https://www.youtube.com/embed/${ytShorts[1]}` };
197
203
 
198
204
  // Vimeo: https://vimeo.com/ID
199
- const vimeo = url.match(/vimeo\.com\/(\d+)/);
205
+ const vimeo = /vimeo\.com\/(\d+)/.exec(url);
200
206
  if (vimeo) return { type: 'Vimeo', embedUrl: `https://player.vimeo.com/video/${vimeo[1]}` };
201
207
 
202
208
  // Direct video file
@@ -232,7 +238,7 @@ export class VideoDialog {
232
238
  }
233
239
 
234
240
  if (info && info.type === 'Direct video') {
235
- const src = info.embedUrl.replace(/"/g, '%22');
241
+ const src = info.embedUrl.replaceAll('"', '%22');
236
242
  return (
237
243
  `<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
238
244
  `<video src="${src}" width="${width}" height="${height}" controls ` +
@@ -252,7 +258,7 @@ export class VideoDialog {
252
258
  })();
253
259
  if (!safeSrc) return null;
254
260
 
255
- const escapedSrc = safeSrc.replace(/"/g, '%22');
261
+ const escapedSrc = safeSrc.replaceAll('"', '%22');
256
262
  return (
257
263
  `<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
258
264
  `<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,10 +109,10 @@ 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 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
- if (w) return w;
115
+ if (w) return /** @type {HTMLElement} */ (w);
118
116
  return null;
119
117
  }
120
118
 
@@ -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
  }
@@ -200,7 +198,7 @@ export class VideoResizer {
200
198
  const wrapper = this._activeWrapper;
201
199
  if (!wrapper) return;
202
200
 
203
- const embed = wrapper.querySelector('iframe, video');
201
+ const embed = /** @type {HTMLElement|null} */ (wrapper.querySelector('iframe, video'));
204
202
  const startX = e.clientX;
205
203
  const startY = e.clientY;
206
204
  const startW = wrapper.offsetWidth || 560;
@@ -39,26 +39,31 @@ export class VideoTooltip {
39
39
  on(editable, 'mouseover', (e) => {
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
- const wrapper = e.target.closest('.an-video-wrapper');
42
+ const target = /** @type {Element} */ (e.target);
43
+ const wrapper = target?.closest('.an-video-wrapper');
43
44
  if (wrapper && editable.contains(wrapper)) {
44
45
  this._scheduleShow(wrapper);
45
46
  }
46
47
  }, { passive: true }),
47
48
  on(editable, 'mouseout', (e) => {
48
- const to = e.relatedTarget;
49
- if (!to || (!editable.contains(to) && !this._el.contains(to))) {
49
+ const to = /** @type {MouseEvent} */ (e).relatedTarget;
50
+ if (!to || (!editable.contains(/** @type {Node} */ (to)) && !this._el.contains(/** @type {Node} */ (to)))) {
50
51
  this._scheduleHide();
51
52
  }
52
53
  }, { passive: true }),
53
54
  on(document, 'click', (e) => {
55
+ const target = /** @type {Node} */ (e.target);
54
56
  if (
55
57
  this._activeWrapper &&
56
- !this._activeWrapper.contains(e.target) &&
57
- !this._el.contains(e.target)
58
+ !this._activeWrapper.contains(target) &&
59
+ !this._el.contains(target)
58
60
  ) {
59
61
  this._hide();
60
62
  }
61
63
  }),
64
+ // Hide when the page scrolls or resizes — the tooltip position becomes stale
65
+ on(globalThis, 'scroll', () => this._hide(), { passive: true }),
66
+ on(globalThis, 'resize', () => this._hide(), { passive: true }),
62
67
  );
63
68
 
64
69
  return this;
@@ -69,7 +74,7 @@ export class VideoTooltip {
69
74
  this._clearTimers();
70
75
  this._disposers.forEach((d) => d());
71
76
  this._disposers = [];
72
- if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
77
+ this._el?.remove();
73
78
  this._el = null;
74
79
  }
75
80
 
@@ -174,7 +179,7 @@ export class VideoTooltip {
174
179
  this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
175
180
  }
176
181
 
177
- _show(wrapper) {
182
+ _show(_wrapper) {
178
183
  this._el.style.display = 'flex';
179
184
  // Defer: offsetWidth on a newly-visible element forces synchronous layout
180
185
  requestAnimationFrame(() => {
@@ -205,8 +210,8 @@ export class VideoTooltip {
205
210
  let top = rect.bottom + margin;
206
211
  let left = rect.left + (rect.width - tipW) / 2;
207
212
 
208
- if (top + tipH > window.innerHeight - margin) top = rect.top - tipH - margin;
209
- 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;
210
215
  if (left < margin) left = margin;
211
216
 
212
217
  this._el.style.top = `${top}px`;
@@ -263,7 +268,7 @@ export class VideoTooltip {
263
268
  if (!wrapper) return;
264
269
  this._hide();
265
270
  this.context.invoke('videoResizer.deselect');
266
- if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
271
+ wrapper.remove();
267
272
  this.context.invoke('editor.afterCommand');
268
273
  }
269
274
 
@@ -40,13 +40,13 @@ export function renderLayout(targetEl, options) {
40
40
  }
41
41
  if (!initialContent) {
42
42
  initialContent = targetEl.tagName === 'TEXTAREA'
43
- ? (targetEl.value || '').trim()
43
+ ? ((/** @type {HTMLTextAreaElement} */ (targetEl)).value || '').trim()
44
44
  : (targetEl.innerHTML || '').trim();
45
45
  }
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
  }
@@ -70,9 +70,11 @@ export function renderLayout(targetEl, options) {
70
70
 
71
71
  container.appendChild(editable);
72
72
 
73
- // Apply dark theme
73
+ // Apply dark theme — also add to body so floating elements (dialogs, tooltips,
74
+ // popovers) appended to document.body inherit the dark CSS rules.
74
75
  if (options.theme === 'dark') {
75
76
  container.classList.add('an-theme-dark');
77
+ document.body.classList.add('an-theme-dark');
76
78
  }
77
79
 
78
80
  // Read-only mode
@@ -7,48 +7,65 @@ import { defaultToolbar } from './module/Buttons.js';
7
7
 
8
8
  /**
9
9
  * @typedef {object} AsnOptions
10
- * @property {string} [placeholder] - Placeholder text when editor is empty
11
- * @property {number} [height] - Editor height in px (min)
12
- * @property {number} [minHeight] - Minimum height in px
13
- * @property {number} [maxHeight] - Maximum height in px (0 = unlimited)
14
- * @property {boolean} [focus] - Auto-focus on init
15
- * @property {boolean} [resizable] - Show resize handle
16
- * @property {Array} [toolbar] - Toolbar button group config
17
- * @property {boolean} [pasteAsPlainText] - Force plain-text paste
18
- * @property {boolean} [pasteCleanHTML] - Sanitise HTML on paste
10
+ * @property {string} [placeholder] - Placeholder text when editor is empty
11
+ * @property {number} [height] - Editor height in px (min)
12
+ * @property {number} [minHeight] - Minimum height in px
13
+ * @property {number} [maxHeight] - Maximum height in px (0 = unlimited)
14
+ * @property {boolean} [focus] - Auto-focus on init
15
+ * @property {boolean} [resizable] - Show resize handle
16
+ * @property {Array} [toolbar] - Toolbar button group config
17
+ * @property {boolean} [useBootstrap] - Use Bootstrap button classes on toolbar buttons
18
+ * @property {string} [toolbarButtonClass] - CSS classes for Bootstrap toolbar buttons
19
+ * @property {boolean} [useFontAwesome] - Use Font Awesome icons (default: true)
20
+ * @property {string} [fontAwesomeClass] - Font Awesome prefix class, e.g. 'fas' or 'fa-solid'
21
+ * @property {boolean} [pasteAsPlainText] - Force plain-text paste
22
+ * @property {boolean} [pasteCleanHTML] - Sanitise HTML on paste
19
23
  * @property {boolean} [pasteStripAttributes] - Strip class/style/data-* from pasted HTML (default: false)
20
- * @property {boolean} [allowImageUpload] - Allow file upload in image dialog
21
- * @property {number} [maxImageSize] - Max upload size in MB
22
- * @property {number} [tabSize] - Spaces per tab in non-list context
23
- * @property {Function} [onChange] - Callback on content change
24
- * @property {Function} [onFocus] - Callback on focus
25
- * @property {Function} [onBlur] - Callback on blur
26
- * @property {Function} [onImageUpload] - Custom upload handler: (files) => void
27
- * @property {boolean} [stickyToolbar] - Stick the toolbar to the viewport top when scrolling
28
- * @property {number} [stickyToolbarOffset] - Top offset in px for sticky toolbar (e.g. fixed nav height)
29
- * @property {string} [theme] - 'light' (default) | 'dark'
30
- * @property {boolean} [codeHighlight] - Auto-load Prism.js for syntax highlighting of code blocks
31
- * @property {string} [codeHighlightCDN] - CDN base URL for Prism assets (defaults to cdnjs)
32
- * @property {boolean} [markdownPaste] - Convert pasted Markdown text to HTML (default: true)
33
- * @property {boolean} [readOnly] - Start editor in read-only / non-editable mode
34
- * @property {boolean} [spellcheck] - Enable browser spellcheck in the editable area (default: true)
35
- * @property {string} [direction] - Text direction: 'ltr' (default) | 'rtl'
36
- * @property {string} [toolbarOverflow] - Toolbar overflow strategy: 'wrap' (default) | 'scroll'
37
- * @property {boolean} [autoSave] - Auto-save content to localStorage on change
38
- * @property {string} [autoSaveKey] - localStorage key used for auto-save (default: 'autumnnote-autosave')
39
- * @property {number} [maxChars] - Maximum character count (0 = unlimited). Shows warning in statusbar.
40
- * @property {number} [maxWords] - Maximum word count (0 = unlimited). Shows warning in statusbar.
41
- * @property {boolean} [tableHeaderRow] - Insert a header row (<thead><th>) when creating tables
42
- * @property {Function} [onPaste] - Callback fired on every paste: ({ text, html }) => void
43
- * @property {Function} [onSelectionChange] - Callback fired on cursor/selection change: (context) => void
44
- * @property {string[]} [colorSwatches] - Custom brand colour swatches prepended to the colour-picker palette
24
+ * @property {boolean} [allowImageUpload] - Allow file upload in image dialog
25
+ * @property {number} [maxImageSize] - Max upload size in MB
26
+ * @property {number} [tabSize] - Spaces per tab in non-list context
27
+ * @property {number} [historyLimit] - Maximum undo/redo history steps
28
+ * @property {string} [defaultFontFamily] - Default font family applied to the editable area on init
29
+ * @property {string} [defaultFontSize] - Default font size applied to the editable area on init (e.g. '14px')
30
+ * @property {string[]} [fontFamilies] - Font families shown in the font-family toolbar dropdown
31
+ * @property {Function} [onChange] - Callback on content change
32
+ * @property {Function} [onFocus] - Callback on focus
33
+ * @property {Function} [onBlur] - Callback on blur
34
+ * @property {Function} [onInit] - Callback after the editor has initialised
35
+ * @property {Function} [onImageUpload] - Custom upload handler: (files) => void
36
+ * @property {Function} [onImageError] - Callback when an image upload error occurs
37
+ * @property {boolean} [stickyToolbar] - Stick the toolbar to the viewport top when scrolling
38
+ * @property {number} [stickyToolbarOffset] - Top offset in px for sticky toolbar (e.g. fixed nav height)
39
+ * @property {string} [theme] - 'light' (default) | 'dark'
40
+ * @property {boolean} [codeHighlight] - Auto-load Prism.js for syntax highlighting of code blocks
41
+ * @property {string} [codeHighlightCDN] - CDN base URL for Prism assets (defaults to cdnjs)
42
+ * @property {boolean} [markdownPaste] - Convert pasted Markdown text to HTML (default: true)
43
+ * @property {boolean} [readOnly] - Start editor in read-only / non-editable mode
44
+ * @property {boolean} [spellcheck] - Enable browser spellcheck in the editable area (default: true)
45
+ * @property {string} [direction] - Text direction: 'ltr' (default) | 'rtl'
46
+ * @property {string} [toolbarOverflow] - Toolbar overflow strategy: 'wrap' (default) | 'scroll'
47
+ * @property {boolean} [autoSave] - Auto-save content to localStorage on change
48
+ * @property {string} [autoSaveKey] - localStorage key used for auto-save (default: 'autumnnote-autosave')
49
+ * @property {number} [maxChars] - Maximum character count (0 = unlimited). Shows warning in statusbar.
50
+ * @property {number} [maxWords] - Maximum word count (0 = unlimited). Shows warning in statusbar.
51
+ * @property {boolean} [tableHeaderRow] - Insert a header row (<thead><th>) when creating tables
52
+ * @property {Function} [onPaste] - Callback fired on every paste: ({ text, html }) => void
53
+ * @property {Function} [onSelectionChange] - Callback fired on cursor/selection change: (context) => void
54
+ * @property {string[]} [colorSwatches] - Custom brand colour swatches prepended to the colour-picker palette
45
55
  * @property {Function} [onDestroy] - Callback fired when the editor is destroyed: (context) => void
46
56
  * @property {Function} [onCharLimitReached] - Callback fired when the character limit is hit: (context) => void
47
57
  * @property {Function} [onWordLimitReached] - Callback fired when the word limit is hit: (context) => void
48
- * @property {string} [focusColor] - Custom focus ring colour, e.g. '#f97316'. Overrides the default blue.
58
+ * @property {string} [focusColor] - Custom focus ring colour, e.g. '#f97316'. Overrides the default blue.
59
+ * @property {boolean} [autoSaveRestore] - Show a restore banner when a previously auto-saved draft exists
60
+ * @property {number} [autoSaveRestoreTimeout] - Maximum age in days for a draft to be offered for restore (0 = no expiry)
61
+ * @property {Function} [onAutoSaveRestore] - Callback fired after the user chooses to restore a draft
62
+ * @property {boolean} [markdownShortcuts] - Convert markdown syntax typed inline to HTML
63
+ * @property {boolean} [bubbleToolbar] - Show a mini floating toolbar above text selections
64
+ * @property {string[]} [bubbleToolbarItems] - Button names for the bubble toolbar
65
+ * @property {object|null} [mention] - @mention configuration (onSearch, minChars, ...)
66
+ * @property {string} [lang] - Display language or partial locale object override
49
67
  */
50
68
 
51
- /** @type {AsnOptions} */
52
69
  export const defaultOptions = {
53
70
  placeholder: '',
54
71
  height: 200,