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
@@ -14,8 +14,8 @@ import { currentRange } from '../core/range.js';
14
14
  // at ~120+ events/sec during normal typing.
15
15
  // ---------------------------------------------------------------------------
16
16
  const _FA_PATTERN = /\bfa-/;
17
- const isFAIcon = (n) => !!(n && n.nodeName === 'I' && _FA_PATTERN.test(n.className || ''));
18
- const isZwsAnchor = (n) => !!(n && n.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
17
+ const isFAIcon = (n) => !!(n?.nodeName === 'I' && _FA_PATTERN.test(n.className || ''));
18
+ const isZwsAnchor = (n) => !!(n?.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
19
19
 
20
20
  /**
21
21
  * Handles special keydown behaviour inside the editor.
@@ -26,7 +26,7 @@ const isZwsAnchor = (n) => !!(n && n.nodeType === Node.TEXT_NODE && (n.textConte
26
26
  */
27
27
  export function handleKeydown(event, editable, options = {}) {
28
28
  const moveCaret = (setFn) => {
29
- const sel = window.getSelection();
29
+ const sel = globalThis.getSelection();
30
30
  if (!sel) return false;
31
31
  const nr = document.createRange();
32
32
  setFn(nr);
@@ -40,8 +40,8 @@ export function handleKeydown(event, editable, options = {}) {
40
40
  // Backspace key — one-press deletion of a preceding FA icon (<i> element)
41
41
  // -------------------------------------------------------------------------
42
42
  if (isKey(event, key.BACKSPACE)) {
43
- const sel = window.getSelection();
44
- if (sel && sel.rangeCount > 0) {
43
+ const sel = globalThis.getSelection();
44
+ if (sel?.rangeCount > 0) {
45
45
  const r = sel.getRangeAt(0);
46
46
  if (r.collapsed && r.startContainer.nodeType === Node.TEXT_NODE) {
47
47
  const textNode = /** @type {ChildNode} */ (r.startContainer);
@@ -89,7 +89,7 @@ export function handleKeydown(event, editable, options = {}) {
89
89
  // ArrowLeft / ArrowRight — one-press navigation across FA icon nodes
90
90
  // -------------------------------------------------------------------------
91
91
  if (isKey(event, key.LEFT) || isKey(event, key.RIGHT)) {
92
- const sel = window.getSelection();
92
+ const sel = globalThis.getSelection();
93
93
  if (!sel || sel.rangeCount === 0) return false;
94
94
 
95
95
  const r = sel.getRangeAt(0);
@@ -207,7 +207,7 @@ export function handleKeydown(event, editable, options = {}) {
207
207
  }
208
208
 
209
209
  // In a pre/code block, insert spaces using configured tabSize
210
- if (para && para.nodeName.toUpperCase() === 'PRE') {
210
+ if (para?.nodeName.toUpperCase() === 'PRE') {
211
211
  if (event.shiftKey) return false;
212
212
  event.preventDefault();
213
213
  execCommand('insertText', ' '.repeat(options.tabSize || 4));
@@ -248,18 +248,18 @@ export function handleKeydown(event, editable, options = {}) {
248
248
  // and leave an orphan <i> in the new paragraph — visually an "auto-created
249
249
  // icon". Push the cursor to just after the <i> first, then fall through so
250
250
  // the browser fires its default Enter at a safe text boundary.
251
- if (el && el.nodeName === 'I' && /\bfa-/.test(el.className || '')) {
251
+ if (el?.nodeName === 'I' && /\bfa-/.test(el.className || '')) {
252
252
  const nr = document.createRange();
253
253
  nr.setStartAfter(el);
254
254
  nr.collapse(true);
255
- const selI = window.getSelection();
255
+ const selI = globalThis.getSelection();
256
256
  if (selI) { selI.removeAllRanges(); selI.addRange(nr); }
257
257
  return false; // cursor is now outside <i> — let browser default handle Enter
258
258
  }
259
259
 
260
260
  // Video wrapper — Enter should create a new paragraph after the wrapper,
261
261
  // not split the wrapper's container and produce an empty video clone.
262
- const videoWrapper = el && el.closest('.an-video-wrapper');
262
+ const videoWrapper = el?.closest('.an-video-wrapper');
263
263
  if (videoWrapper) {
264
264
  event.preventDefault();
265
265
  const p = document.createElement('p');
@@ -268,18 +268,18 @@ export function handleKeydown(event, editable, options = {}) {
268
268
  const nr = document.createRange();
269
269
  nr.setStart(p, 0);
270
270
  nr.collapse(true);
271
- const sel = window.getSelection();
271
+ const sel = globalThis.getSelection();
272
272
  sel.removeAllRanges();
273
273
  sel.addRange(nr);
274
274
  return true;
275
275
  }
276
276
 
277
277
  // Checklist — Enter creates new item; empty item exits the list
278
- const checkLi = el && el.closest('.an-checklist li');
278
+ const checkLi = el?.closest('.an-checklist li');
279
279
  if (checkLi) {
280
280
  event.preventDefault();
281
281
  const ul = checkLi.closest('.an-checklist');
282
- const sel = window.getSelection();
282
+ const sel = globalThis.getSelection();
283
283
  let nativeRange = sel.getRangeAt(0);
284
284
 
285
285
  // Helper: get trimmed text content of a li, excluding the checkbox INPUT.
@@ -358,14 +358,14 @@ export function handleKeydown(event, editable, options = {}) {
358
358
  const para = closestPara(range.sc, editable);
359
359
 
360
360
  // Enter in a pre/code block: insert a literal newline instead of a new block
361
- if (para && para.nodeName.toUpperCase() === 'PRE') {
361
+ if (para?.nodeName.toUpperCase() === 'PRE') {
362
362
  event.preventDefault();
363
363
  execCommand('insertText', '\n');
364
364
  return true;
365
365
  }
366
366
 
367
367
  // Pressing Enter at the end of a blockquote should exit it
368
- if (para && para.nodeName.toUpperCase() === 'BLOCKQUOTE') {
368
+ if (para?.nodeName.toUpperCase() === 'BLOCKQUOTE') {
369
369
  const native = range.toNativeRange();
370
370
  native.setEnd(para, para.childNodes.length);
371
371
  if (native.toString() === '' && range.isCollapsed()) {
package/src/js/index.js CHANGED
@@ -141,7 +141,7 @@ const AutumnNote = {
141
141
  registerButton(btnDef) { registerButton(btnDef); return this; },
142
142
 
143
143
  /** Library version */
144
- version: '1.5.0',
144
+ version: '1.6.2',
145
145
  };
146
146
 
147
147
  // ---------------------------------------------------------------------------
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Activated when both `autoSave` and `autoSaveRestore` options are true.
6
6
  * On initialize it checks localStorage for a draft that is within the
7
- * `autoSaveRestoreTimeout` day window. If one is found a dismissible banner
7
+ * `autoSaveRestoreTimeout` day globalThis. If one is found a dismissible banner
8
8
  * is prepended to the editor container.
9
9
  */
10
10
 
@@ -118,9 +118,7 @@ export class AutoSaveRestore {
118
118
  }
119
119
 
120
120
  _removeBanner() {
121
- if (this._banner && this._banner.parentNode) {
122
- this._banner.parentNode.removeChild(this._banner);
123
- }
121
+ this._banner?.remove();
124
122
  this._banner = null;
125
123
  }
126
124
  }
@@ -0,0 +1,126 @@
1
+ import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
2
+ import { withSavedRange } from '../core/range.js';
3
+
4
+ /**
5
+ * Shared lifecycle and shell-building logic for all modal dialogs.
6
+ * Subclasses implement _buildDialog() for their specific form fields.
7
+ */
8
+ export class BaseDialog {
9
+ /** @param {import('../Context.js').Context} context */
10
+ constructor(context) {
11
+ this.context = context;
12
+ this.options = context.options;
13
+ /** @type {HTMLElement|null} */
14
+ this._dialog = null;
15
+ this._disposers = [];
16
+ this._savedRange = null;
17
+ /** @type {HTMLElement|null} First focusable input; set by subclass in _buildDialog(). */
18
+ this._firstInput = null;
19
+ this._removeTrap = null;
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Lifecycle (shared)
24
+ // ---------------------------------------------------------------------------
25
+
26
+ initialize() {
27
+ this._dialog = this._buildDialog();
28
+ document.body.appendChild(this._dialog);
29
+ return this;
30
+ }
31
+
32
+ destroy() {
33
+ this._disposers.forEach((d) => d());
34
+ this._disposers = [];
35
+ if (this._dialog?.parentNode) {
36
+ this._dialog.remove();
37
+ }
38
+ this._dialog = null;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Shared show helpers
43
+ // ---------------------------------------------------------------------------
44
+
45
+ /** Saves the current selection range before opening the dialog. */
46
+ _saveRange() {
47
+ withSavedRange((range) => { this._savedRange = range; });
48
+ }
49
+
50
+ _open() {
51
+ if (this._dialog) {
52
+ this._dialog.style.display = 'flex';
53
+ this._removeTrap = trapFocus(this._dialog, () => this._close());
54
+ setTimeout(() => this._firstInput && this._firstInput.focus(), 50);
55
+ }
56
+ }
57
+
58
+ _close() {
59
+ if (this._dialog) this._dialog.style.display = 'none';
60
+ if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
61
+ this._savedRange = null;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Shell builders (used by subclass _buildDialog())
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * Builds the overlay + box + header shell common to all dialogs.
70
+ * Also wires up draggable and overlay-click-to-close.
71
+ * @param {string} ariaLabel
72
+ * @param {string} iconHtml Raw SVG string for the dialog icon
73
+ * @param {string} titleText
74
+ * @returns {{ overlay: HTMLElement, box: HTMLElement }}
75
+ */
76
+ _buildDialogShell(ariaLabel, iconHtml, titleText) {
77
+ const overlay = createElement('div', {
78
+ class: 'an-dialog-overlay',
79
+ role: 'dialog',
80
+ 'aria-modal': 'true',
81
+ 'aria-label': ariaLabel,
82
+ });
83
+ const box = createElement('div', { class: 'an-dialog-box' });
84
+
85
+ const header = createElement('div', { class: 'an-dialog-header' });
86
+ const iconEl = createElement('span', { class: 'an-dialog-icon' });
87
+ iconEl.innerHTML = iconHtml;
88
+ const titleEl = createElement('h3', { class: 'an-dialog-title' });
89
+ titleEl.textContent = titleText;
90
+ header.appendChild(iconEl);
91
+ header.appendChild(titleEl);
92
+
93
+ box.appendChild(header);
94
+ overlay.appendChild(box);
95
+ makeDraggable(header, box);
96
+
97
+ const d = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
98
+ this._disposers.push(d);
99
+
100
+ return { overlay, box };
101
+ }
102
+
103
+ /**
104
+ * Builds an action button row with a primary insert button and a cancel button.
105
+ * Disposers are registered automatically.
106
+ * @param {string} insertLabel
107
+ * @param {string} cancelLabel
108
+ * @param {() => void} onInsert
109
+ * @returns {HTMLElement}
110
+ */
111
+ _buildButtonRow(insertLabel, cancelLabel, onInsert) {
112
+ const btnRow = createElement('div', { class: 'an-dialog-actions' });
113
+ const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
114
+ insertBtn.textContent = insertLabel;
115
+ const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
116
+ cancelBtn.textContent = cancelLabel;
117
+ btnRow.appendChild(insertBtn);
118
+ btnRow.appendChild(cancelBtn);
119
+
120
+ const d1 = on(insertBtn, 'click', onInsert);
121
+ const d2 = on(cancelBtn, 'click', () => this._close());
122
+ this._disposers.push(d1, d2);
123
+
124
+ return btnRow;
125
+ }
126
+ }
@@ -44,13 +44,13 @@ const _ACTIONS = {
44
44
  strikethrough: (ctx) => ctx.invoke('editor.strikethrough'),
45
45
  link: (ctx) => ctx.invoke('linkDialog.show'),
46
46
  removeFormat: (ctx) => {
47
- const editable = ctx.layoutInfo && ctx.layoutInfo.editable;
47
+ const editable = ctx.layoutInfo?.editable;
48
48
  if (!editable) return;
49
49
  editable.focus();
50
50
  document.execCommand('removeFormat');
51
51
  // Also strip inline style attributes which execCommand('removeFormat') misses
52
- const sel = window.getSelection();
53
- if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
52
+ const sel = globalThis.getSelection();
53
+ if (sel?.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
54
54
  const range = sel.getRangeAt(0);
55
55
  const ancestor = range.commonAncestorContainer;
56
56
  const root = /** @type {Element|null} */ (ancestor.nodeType === 1 ? ancestor : ancestor.parentElement);
@@ -115,16 +115,16 @@ export class BubbleToolbar {
115
115
  const d6 = this.context.on('contextMenu:hide', () => {
116
116
  this._contextMenuOpen = false;
117
117
  });
118
- const d7 = on(window, 'scroll', () => this._hide(), { passive: true });
119
- const d8 = on(window, 'resize', () => this._hide(), { passive: true });
118
+ const d7 = on(globalThis, 'scroll', () => this._hide(), { passive: true });
119
+ const d8 = on(globalThis, 'resize', () => this._hide(), { passive: true });
120
120
  this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8);
121
121
  return this;
122
122
  }
123
123
 
124
124
  destroy() {
125
- if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
125
+ this._el?.remove();
126
126
  this._el = null;
127
- if (this._picker && this._picker.parentNode) this._picker.parentNode.removeChild(this._picker);
127
+ this._picker?.remove();
128
128
  this._picker = null;
129
129
  this._disposers.forEach((d) => d());
130
130
  this._disposers = [];
@@ -259,8 +259,8 @@ export class BubbleToolbar {
259
259
 
260
260
  _openColorPicker(type, anchorBtn) {
261
261
  // Save current selection before the picker might shift focus
262
- const sel = window.getSelection();
263
- if (sel && sel.rangeCount > 0) {
262
+ const sel = globalThis.getSelection();
263
+ if (sel?.rangeCount > 0) {
264
264
  this._savedRange = sel.getRangeAt(0).cloneRange();
265
265
  }
266
266
 
@@ -272,8 +272,8 @@ export class BubbleToolbar {
272
272
  const noColorBtn = pickerAny._noColorBtn;
273
273
  if (type === 'hiliteColor') {
274
274
  if (!palette.contains(noColorBtn)) palette.appendChild(noColorBtn);
275
- } else {
276
- if (palette.contains(noColorBtn)) palette.removeChild(noColorBtn);
275
+ } else if (palette.contains(noColorBtn)) {
276
+ noColorBtn.remove();
277
277
  }
278
278
 
279
279
  // Seed the custom color input
@@ -292,7 +292,7 @@ export class BubbleToolbar {
292
292
  // Align horizontally with the clicked button, clamped to viewport
293
293
  const btnRect = anchorBtn.getBoundingClientRect();
294
294
  let left = btnRect.left;
295
- left = Math.max(8, Math.min(left, window.innerWidth - pw - 8));
295
+ left = Math.max(8, Math.min(left, globalThis.innerWidth - pw - 8));
296
296
 
297
297
  this._picker.style.left = `${left}px`;
298
298
  this._picker.style.top = `${top}px`;
@@ -305,11 +305,11 @@ export class BubbleToolbar {
305
305
 
306
306
  /** Restore the saved selection, apply execCommand, update the color strip, then close the picker. */
307
307
  _applyColor(type, color) {
308
- const editable = this.context.layoutInfo && this.context.layoutInfo.editable;
308
+ const editable = this.context.layoutInfo?.editable;
309
309
  if (!editable || !this._savedRange) return;
310
310
 
311
311
  editable.focus();
312
- const sel = window.getSelection();
312
+ const sel = globalThis.getSelection();
313
313
  sel.removeAllRanges();
314
314
  try { sel.addRange(this._savedRange.cloneRange()); } catch (_) { return; }
315
315
 
@@ -322,8 +322,8 @@ export class BubbleToolbar {
322
322
 
323
323
  // Update the color strip on the corresponding button
324
324
  const name = type === 'hiliteColor' ? 'hiliteColor' : 'foreColor';
325
- const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
326
- const strip = btn && btn.querySelector('.an-bubble-color-strip');
325
+ const btn = this._el?.querySelector(`[data-name="${name}"]`);
326
+ const strip = btn?.querySelector('.an-bubble-color-strip');
327
327
  if (strip) /** @type {HTMLElement} */ (strip).style.background = color === 'transparent' ? 'transparent' : color;
328
328
 
329
329
  this._closeColorPicker();
@@ -350,7 +350,7 @@ export class BubbleToolbar {
350
350
  let left = rect.left + rect.width / 2 - bw / 2;
351
351
  let top = rect.top - bh - gap;
352
352
 
353
- left = Math.max(8, Math.min(left, window.innerWidth - bw - 8));
353
+ left = Math.max(8, Math.min(left, globalThis.innerWidth - bw - 8));
354
354
 
355
355
  if (top < 8) {
356
356
  top = rect.bottom + gap;
@@ -365,7 +365,7 @@ export class BubbleToolbar {
365
365
  const overlapsVertically = top < ttRect.bottom + gap && top + bh > ttRect.top - gap;
366
366
  if (overlapsVertically) {
367
367
  top = rect.bottom + gap;
368
- if (top + bh > window.innerHeight - 8) top = ttRect.bottom + gap;
368
+ if (top + bh > globalThis.innerHeight - 8) top = ttRect.bottom + gap;
369
369
  }
370
370
  }
371
371
 
@@ -396,19 +396,19 @@ export class BubbleToolbar {
396
396
  /** Read the current selection's color and update the color-strip indicators. */
397
397
  _syncColorStrips() {
398
398
  if (!this._el) return;
399
- const sel = window.getSelection();
399
+ const sel = globalThis.getSelection();
400
400
  if (!sel || !sel.rangeCount) return;
401
401
  let node = sel.getRangeAt(0).startContainer;
402
402
  if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
403
403
  if (!node) return;
404
- const cs = window.getComputedStyle(/** @type {Element} */ (node));
404
+ const cs = globalThis.getComputedStyle(/** @type {Element} */ (node));
405
405
 
406
406
  const foreBtn = this._el.querySelector('[data-name="foreColor"]');
407
- const foreStrip = foreBtn && foreBtn.querySelector('.an-bubble-color-strip');
407
+ const foreStrip = foreBtn?.querySelector('.an-bubble-color-strip');
408
408
  if (foreStrip) /** @type {HTMLElement} */ (foreStrip).style.background = cs.color || '#000000';
409
409
 
410
410
  const hiliteBtn = this._el.querySelector('[data-name="hiliteColor"]');
411
- const hiliteStrip = hiliteBtn && hiliteBtn.querySelector('.an-bubble-color-strip');
411
+ const hiliteStrip = hiliteBtn?.querySelector('.an-bubble-color-strip');
412
412
  if (hiliteStrip) {
413
413
  const bg = cs.backgroundColor;
414
414
  /** @type {HTMLElement} */ (hiliteStrip).style.background = (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') ? 'transparent' : bg;
@@ -426,7 +426,7 @@ export class BubbleToolbar {
426
426
  // Keep toolbar visible while color picker is open
427
427
  if (this._picker && this._picker.style.display !== 'none') return;
428
428
 
429
- const sel = window.getSelection();
429
+ const sel = globalThis.getSelection();
430
430
  if (!sel || sel.isCollapsed || !sel.rangeCount) {
431
431
  this._hide();
432
432
  return;
@@ -458,8 +458,8 @@ export class BubbleToolbar {
458
458
  _onMousedown(e) {
459
459
  // Hide when clicking outside both the editable, the bubble toolbar, and the color picker
460
460
  if (!this._visible) return;
461
- if (this._el && this._el.contains(e.target)) return;
462
- if (this._picker && this._picker.contains(e.target)) return;
461
+ if (this._el?.contains(e.target)) return;
462
+ if (this._picker?.contains(e.target)) return;
463
463
  const editable = this.context.layoutInfo.editable;
464
464
  if (editable.contains(e.target)) return;
465
465
  this._hide();
@@ -101,7 +101,7 @@ export const underlineBtn = btn('underline', 'underline', 'Underline (Ctrl+U)',
101
101
  // also check for a <u> ancestor in the DOM using startContainer for
102
102
  // consistent behaviour across both collapsed and range selections.
103
103
  if (document.queryCommandState('underline')) return true;
104
- const sel = window.getSelection();
104
+ const sel = globalThis.getSelection();
105
105
  if (!sel || !sel.rangeCount) return false;
106
106
  let sc = sel.getRangeAt(0).startContainer;
107
107
  if (sc.nodeType === 3) sc = sc.parentElement;
@@ -179,7 +179,7 @@ export const fontSizeBtn = {
179
179
  action: (ctx, value) => Style.fontSize(value, ctx.layoutInfo.editable),
180
180
  getValue: (ctx) => {
181
181
  try {
182
- const sel = window.getSelection();
182
+ const sel = globalThis.getSelection();
183
183
  if (sel && sel.rangeCount) {
184
184
  let el = /** @type {Element|null} */ (sel.getRangeAt(0).startContainer);
185
185
  if (el && el.nodeType === 3) el = el.parentElement;
@@ -188,7 +188,7 @@ export const fontSizeBtn = {
188
188
  if (size) return size;
189
189
  }
190
190
  // Fallback: read the base font size from the editable element itself
191
- const editable = ctx && ctx.layoutInfo && ctx.layoutInfo.editable;
191
+ const editable = ctx?.layoutInfo?.editable;
192
192
  if (editable) return editable.style.fontSize || '';
193
193
  return '';
194
194
  } catch { return ''; }
@@ -280,7 +280,7 @@ export const lineHeightBtn = {
280
280
  action: (_ctx, value) => Style.lineHeight(value),
281
281
  getValue: () => {
282
282
  try {
283
- const sel = window.getSelection();
283
+ const sel = globalThis.getSelection();
284
284
  if (!sel || !sel.rangeCount) return '';
285
285
  const BLOCKS = new Set(['P','DIV','H1','H2','H3','H4','H5','H6','LI','BLOCKQUOTE','PRE','TD','TH']);
286
286
  let el = /** @type {Element|null} */ (sel.getRangeAt(0).startContainer);
@@ -139,7 +139,7 @@ export class Clipboard {
139
139
  // Unwrap — replace el with its children
140
140
  const parent = el.parentNode;
141
141
  while (el.firstChild) parent.insertBefore(el.firstChild, el);
142
- parent.removeChild(el);
142
+ el.remove();
143
143
  }
144
144
  // Strip class and all data-* attributes from every remaining element
145
145
  doc.querySelectorAll('*').forEach((el) => {
@@ -180,7 +180,7 @@ export class Clipboard {
180
180
  }
181
181
 
182
182
  _onPaste(event) {
183
- const clipboardData = event.clipboardData || /** @type {any} */ (window).clipboardData;
183
+ const clipboardData = event.clipboardData || /** @type {any} */ (globalThis).clipboardData;
184
184
  if (!clipboardData) return;
185
185
 
186
186
  // Consume and reset the one-shot plain-paste flag
@@ -247,7 +247,6 @@ export class Clipboard {
247
247
  if (this.options.pasteStripAttributes) html = this._stripAttributes(html);
248
248
  execCommand('insertHTML', html);
249
249
  this.context.invoke('editor.afterCommand');
250
- return;
251
250
  }
252
251
 
253
252
  // Otherwise let the browser handle paste natively
@@ -300,11 +299,11 @@ export class Clipboard {
300
299
  }
301
300
 
302
301
  // C2: Reject image formats that browsers cannot decode/display.
303
- const UNSUPPORTED = ['image/tiff', 'image/x-tiff', 'image/bmp', 'image/x-bmp', 'image/x-ms-bmp'];
302
+ const UNSUPPORTED = new Set(['image/tiff', 'image/x-tiff', 'image/bmp', 'image/x-bmp', 'image/x-ms-bmp']);
304
303
  const maxBytes = (this.options.maxImageSize || 5) * 1024 * 1024;
305
304
  files.forEach((file) => {
306
305
  if (!file || !file.type.startsWith('image/')) return;
307
- if (UNSUPPORTED.includes(file.type)) {
306
+ if (UNSUPPORTED.has(file.type)) {
308
307
  const message = `Image format "${file.type}" is not supported for display in web browsers. Please convert to PNG, JPEG, or WebP first.`;
309
308
  this.context.triggerEvent('imageError', { file, message });
310
309
  console.warn('[AutumnNote]', message);
@@ -351,10 +350,10 @@ export class Clipboard {
351
350
  */
352
351
  _dataUrlToBlob(dataUrl) {
353
352
  const [header, b64] = dataUrl.split(',');
354
- const mime = header.match(/:(.*?);/)?.[1] ?? 'image/png';
353
+ const mime = /:(.*?);/.exec(header)?.[1] ?? 'image/png';
355
354
  const binary = atob(b64);
356
355
  const arr = new Uint8Array(binary.length);
357
- for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
356
+ for (let i = 0; i < binary.length; i++) arr[i] = binary.codePointAt(i);
358
357
  return new Blob([arr], { type: mime });
359
358
  }
360
359
 
@@ -438,7 +437,7 @@ export class Clipboard {
438
437
  }
439
438
  }
440
439
  if (!range) return;
441
- const sel = window.getSelection();
440
+ const sel = globalThis.getSelection();
442
441
  if (sel) {
443
442
  sel.removeAllRanges();
444
443
  sel.addRange(range);
@@ -456,10 +455,10 @@ export class Clipboard {
456
455
  */
457
456
  _escapeHTML(str) {
458
457
  return str
459
- .replace(/&/g, '&amp;')
460
- .replace(/</g, '&lt;')
461
- .replace(/>/g, '&gt;')
462
- .replace(/"/g, '&quot;')
463
- .replace(/'/g, '&#039;');
458
+ .replaceAll('&', '&amp;')
459
+ .replaceAll('<', '&lt;')
460
+ .replaceAll('>', '&gt;')
461
+ .replaceAll('"', '&quot;')
462
+ .replaceAll("'", '&#039;');
464
463
  }
465
464
  }
@@ -68,7 +68,7 @@ export class CodeTooltip {
68
68
  this._clearTimers();
69
69
  this._disposers.forEach((d) => d());
70
70
  this._disposers = [];
71
- if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
71
+ this._el?.remove();
72
72
  this._el = null;
73
73
  }
74
74
 
@@ -226,7 +226,7 @@ export class CodeTooltip {
226
226
  let left = rect.left + (rect.width - tipW) / 2;
227
227
 
228
228
  if (top < margin) top = rect.bottom + margin;
229
- if (left + tipW > window.innerWidth - margin) left = window.innerWidth - tipW - margin;
229
+ if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
230
230
  if (left < margin) left = margin;
231
231
 
232
232
  // Tooltip uses position:fixed, so viewport coordinates are used directly.
@@ -241,7 +241,7 @@ export class CodeTooltip {
241
241
  _syncWrapBtn() {
242
242
  if (!this._activePre || !this._wrapBtn) return;
243
243
  const wrapped = (this._activePre.style.whiteSpace || '').includes('pre-wrap')
244
- || window.getComputedStyle(this._activePre).whiteSpace === 'pre-wrap';
244
+ || globalThis.getComputedStyle(this._activePre).whiteSpace === 'pre-wrap';
245
245
  this._wrapBtn.classList.toggle('active', wrapped);
246
246
  this._wrapBtn.title = wrapped
247
247
  ? this.context.locale.tooltips.code.disableWordWrap
@@ -251,7 +251,7 @@ export class CodeTooltip {
251
251
  _syncLangSelect() {
252
252
  if (!this._activePre || !this._langSelect) return;
253
253
  const codeEl = this._activePre.querySelector('code');
254
- const fromAttr = this._activePre.getAttribute('data-language') || '';
254
+ const fromAttr = this._activePre.dataset.language || '';
255
255
  const fromClass = codeEl ? (_LANG_CLASS_RE.exec(codeEl.className) || [])[1] || '' : '';
256
256
  this._langSelect.value = fromAttr || fromClass || '';
257
257
  }
@@ -274,7 +274,7 @@ export class CodeTooltip {
274
274
  document.body.appendChild(ta);
275
275
  ta.select();
276
276
  try { document.execCommand('copy'); this._flashCopied(); } catch (_) {}
277
- document.body.removeChild(ta);
277
+ ta.remove();
278
278
  }
279
279
  }
280
280
 
@@ -310,8 +310,7 @@ export class CodeTooltip {
310
310
  applyLanguage(pre, lang) {
311
311
  if (!pre || !lang) return;
312
312
  // Temporarily set activePre so _onLangChange can target it
313
- const savedPre = this._activePre;
314
- const savedSelect = this._langSelect ? this._langSelect.value : '';
313
+ const savedPre = this._activePre;
315
314
  this._activePre = pre;
316
315
  if (this._langSelect) this._langSelect.value = lang;
317
316
  this._onLangChange();
@@ -320,14 +319,13 @@ export class CodeTooltip {
320
319
  this._activePre = savedPre || pre;
321
320
  // Don't restore savedPre if it was null — keep `pre` as activePre so that
322
321
  // the tooltip select is correct the first time the user hovers over it.
323
- void savedSelect;
324
322
  }
325
323
 
326
324
  _onLangChange() {
327
325
  const pre = this._activePre;
328
326
  if (!pre) return;
329
327
  const lang = this._langSelect.value;
330
- const _w = /** @type {any} */ (window);
328
+ const _w = /** @type {any} */ (globalThis);
331
329
 
332
330
  // Ensure a <code> child exists (Prism targets <pre><code class="language-xxx">)
333
331
  let codeEl = pre.querySelector('code');
@@ -342,9 +340,9 @@ export class CodeTooltip {
342
340
  // Mirror language class on <pre> so Prism CSS theme targets it (pre[class*='language-'])
343
341
  pre.className = lang ? `language-${lang}` : '';
344
342
  if (lang) {
345
- pre.setAttribute('data-language', lang);
343
+ pre.dataset.language = lang;
346
344
  } else {
347
- pre.removeAttribute('data-language');
345
+ delete pre.dataset.language;
348
346
  }
349
347
 
350
348
  // Trigger Prism if available.
@@ -357,7 +355,7 @@ export class CodeTooltip {
357
355
  };
358
356
 
359
357
  if (lang) {
360
- if (typeof _w.Prism !== 'undefined') {
358
+ if (_w.Prism !== undefined) {
361
359
  // Grammar already loaded — highlight immediately
362
360
  if (_w.Prism.languages[lang]) {
363
361
  applyPrism();
@@ -387,7 +385,7 @@ export class CodeTooltip {
387
385
  * Called once at initialize time. Fire-and-forget; errors are silent.
388
386
  */
389
387
  _ensurePrism() {
390
- const _w = /** @type {any} */ (window);
388
+ const _w = /** @type {any} */ (globalThis);
391
389
  if (!this.context.options.codeHighlight || _w.Prism) return;
392
390
  const cdn = this.context.options.codeHighlightCDN;
393
391
  const themeHref = `${cdn}/themes/prism-tomorrow.min.css`;
@@ -422,14 +420,14 @@ export class CodeTooltip {
422
420
  * @param {Function} cb – called once the grammar is ready
423
421
  */
424
422
  _loadPrismComponent(lang, cb) {
425
- const _w = /** @type {any} */ (window);
423
+ const _w = /** @type {any} */ (globalThis);
426
424
  const cdn = this.context.options.codeHighlightCDN;
427
425
  const src = `${cdn}/components/prism-${lang}.min.js`;
428
426
  // Avoid loading the same component twice
429
427
  if (document.querySelector(`script[src="${src}"]`)) {
430
428
  // Already in DOM — might still be loading; poll briefly then call cb
431
429
  const poll = setInterval(() => {
432
- if (_w.Prism && _w.Prism.languages[lang]) {
430
+ if (_w.Prism?.languages[lang]) {
433
431
  clearInterval(poll);
434
432
  cb();
435
433
  }
@@ -466,7 +464,7 @@ export class CodeTooltip {
466
464
  const pre = this._activePre;
467
465
  if (!pre) return;
468
466
  this._hide();
469
- if (pre.parentNode) pre.parentNode.removeChild(pre);
467
+ pre.remove();
470
468
  this.context.invoke('editor.afterCommand');
471
469
  }
472
470
  }