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
@@ -3,49 +3,18 @@
3
3
  * Inspired by Summernote's ImageDialog — rewritten without jQuery
4
4
  */
5
5
 
6
- import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
7
- import { withSavedRange } from '../core/range.js';
8
-
9
- export class ImageDialog {
10
- /**
11
- * @param {import('../Context.js').Context} context
12
- */
13
- constructor(context) {
14
- this.context = context;
15
- this.options = context.options;
16
- /** @type {HTMLElement|null} */
17
- this._dialog = null;
18
- this._disposers = [];
19
- this._savedRange = null;
20
- }
6
+ import { createElement, on } from '../core/dom.js';
7
+ import { BaseDialog } from './BaseDialog.js';
21
8
 
22
- // ---------------------------------------------------------------------------
23
- // Lifecycle
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 && this._dialog.parentNode) {
36
- this._dialog.parentNode.removeChild(this._dialog);
37
- }
38
- this._dialog = null;
39
- }
9
+ 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"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`;
40
10
 
11
+ export class ImageDialog extends BaseDialog {
41
12
  // ---------------------------------------------------------------------------
42
13
  // Public API
43
14
  // ---------------------------------------------------------------------------
44
15
 
45
16
  show() {
46
- withSavedRange((range) => {
47
- this._savedRange = range;
48
- });
17
+ this._saveRange();
49
18
  this._urlInput.value = '';
50
19
  this._altInput.value = '';
51
20
  if (this._fileInput) this._fileInput.value = '';
@@ -58,23 +27,9 @@ export class ImageDialog {
58
27
 
59
28
  _buildDialog() {
60
29
  const L = this.context.locale.imageDialog;
61
- const overlay = createElement('div', {
62
- class: 'an-dialog-overlay',
63
- role: 'dialog',
64
- 'aria-modal': 'true',
65
- 'aria-label': L.ariaLabel,
66
- });
67
- const box = createElement('div', { class: 'an-dialog-box' });
68
-
69
- const header = createElement('div', { class: 'an-dialog-header' });
70
- const iconEl = createElement('span', { class: 'an-dialog-icon' });
71
- 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"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>`;
72
- const title = createElement('h3', { class: 'an-dialog-title' });
73
- title.textContent = L.title;
74
- header.appendChild(iconEl);
75
- header.appendChild(title);
30
+ const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
76
31
 
77
- // URL tab
32
+ // URL field
78
33
  const urlLabel = createElement('label', { class: 'an-label' });
79
34
  urlLabel.textContent = L.imageUrl;
80
35
  const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
@@ -84,6 +39,7 @@ export class ImageDialog {
84
39
  autocomplete: 'off',
85
40
  }));
86
41
  this._urlInput = urlInput;
42
+ this._firstInput = urlInput;
87
43
 
88
44
  // Alt text
89
45
  const altLabel = createElement('label', { class: 'an-label' });
@@ -96,7 +52,7 @@ export class ImageDialog {
96
52
  }));
97
53
  this._altInput = altInput;
98
54
 
99
- box.append(header, urlLabel, urlInput, altLabel, altInput);
55
+ box.append(urlLabel, urlInput, altLabel, altInput);
100
56
 
101
57
  // Alignment
102
58
  const alignLabel = createElement('label', { class: 'an-label' });
@@ -118,6 +74,7 @@ export class ImageDialog {
118
74
  });
119
75
  this._alignRow = alignRow;
120
76
  box.append(alignLabel, alignRow);
77
+
121
78
  if (this.options.allowImageUpload !== false) {
122
79
  const fileLabel = createElement('label', { class: 'an-label' });
123
80
  fileLabel.textContent = L.uploadLabel;
@@ -135,25 +92,12 @@ export class ImageDialog {
135
92
  box.append(fileLabel, fileInput, fileHint);
136
93
  }
137
94
 
138
- // Buttons
139
- const btnRow = createElement('div', { class: 'an-dialog-actions' });
140
- const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
141
- insertBtn.textContent = L.insertBtn;
142
- const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
143
- cancelBtn.textContent = L.cancelBtn;
144
- btnRow.appendChild(insertBtn);
145
- btnRow.appendChild(cancelBtn);
146
-
95
+ const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
147
96
  box.append(btnRow);
148
- overlay.appendChild(box);
149
- makeDraggable(header, box);
150
97
 
151
- const d1 = on(insertBtn, 'click', () => this._onInsert());
152
- const d2 = on(cancelBtn, 'click', () => this._close());
153
- const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
154
98
  const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
155
99
  const d5 = on(altInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
156
- this._disposers.push(d1, d2, d3, d4, d5);
100
+ this._disposers.push(d4, d5);
157
101
 
158
102
  return overlay;
159
103
  }
@@ -217,18 +161,4 @@ export class ImageDialog {
217
161
  this.context.invoke('editor.insertImage', src, alt, align);
218
162
  this._close();
219
163
  }
220
-
221
- _open() {
222
- if (this._dialog) {
223
- this._dialog.style.display = 'flex';
224
- this._removeTrap = trapFocus(this._dialog, () => this._close());
225
- setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
226
- }
227
- }
228
-
229
- _close() {
230
- if (this._dialog) this._dialog.style.display = 'none';
231
- if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
232
- this._savedRange = null;
233
- }
234
164
  }
@@ -38,7 +38,7 @@ export class ImageResizer {
38
38
 
39
39
  const editable = this.context.layoutInfo.editable;
40
40
 
41
- // Debounce window resize — _updateOverlayPosition already has rAF gating but
41
+ // Debounce globalThis resize — _updateOverlayPosition already has rAF gating but
42
42
  // every call cancels + re-schedules it; a debounce reduces that churn.
43
43
  let _resizeDebounce = null;
44
44
  const onWindowResize = () => {
@@ -55,8 +55,8 @@ export class ImageResizer {
55
55
  if (img) this._select(img);
56
56
  }),
57
57
  on(document, 'click', (e) => this._onDocClick(e)),
58
- on(window, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
59
- on(window, 'resize', onWindowResize, { passive: true }),
58
+ on(globalThis, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
59
+ on(globalThis, 'resize', onWindowResize, { passive: true }),
60
60
  on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
61
61
  );
62
62
 
@@ -76,7 +76,7 @@ export class ImageResizer {
76
76
  }
77
77
  this._deselect();
78
78
  if (this._overlay && this._overlay.parentNode) {
79
- this._overlay.parentNode.removeChild(this._overlay);
79
+ this._overlay.remove();
80
80
  }
81
81
  this._overlay = null;
82
82
  }
@@ -1,4 +1,4 @@
1
- // ImageTooltip.js - Hover tooltip for images inside the editor
1
+ // ImageTooltip.js - Hover tooltip for images inside the editor
2
2
  // Displays a horizontal action bar below (or above) the selected image,
3
3
  // similar in appearance and interaction to LinkTooltip.
4
4
  import { createElement, on } from '../core/dom.js';
@@ -59,8 +59,8 @@ export class ImageTooltip {
59
59
  }
60
60
  }),
61
61
  // Hide when the page scrolls or resizes — the tooltip position becomes stale
62
- on(window, 'scroll', () => this._hide(), { passive: true }),
63
- on(window, 'resize', () => this._hide(), { passive: true }),
62
+ on(globalThis, 'scroll', () => this._hide(), { passive: true }),
63
+ on(globalThis, 'resize', () => this._hide(), { passive: true }),
64
64
  );
65
65
 
66
66
  return this;
@@ -70,7 +70,7 @@ export class ImageTooltip {
70
70
  this._clearTimers();
71
71
  this._disposers.forEach((d) => d());
72
72
  this._disposers = [];
73
- if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
73
+ this._el?.remove();
74
74
  this._el = null;
75
75
  }
76
76
 
@@ -215,8 +215,8 @@ export class ImageTooltip {
215
215
  let top = rect.bottom + margin;
216
216
  let left = rect.left + (rect.width - tipW) / 2;
217
217
 
218
- if (top + tipH > window.innerHeight - margin) top = rect.top - tipH - margin;
219
- if (left + tipW > window.innerWidth - margin) left = window.innerWidth - tipW - margin;
218
+ if (top + tipH > globalThis.innerHeight - margin) top = rect.top - tipH - margin;
219
+ if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
220
220
  if (left < margin) left = margin;
221
221
 
222
222
  this._el.style.top = `${top}px`;
@@ -279,8 +279,8 @@ export class ImageTooltip {
279
279
  if (!img) return;
280
280
  // Parse current rotation angle from inline transform
281
281
  const current = img.style.transform || '';
282
- const match = current.match(/rotate\((-?[\d.]+)deg\)/);
283
- const prev = match ? parseFloat(match[1]) : 0;
282
+ const match = /rotate\((-?[\d.]+)deg\)/.exec(current);
283
+ const prev = match ? Number.parseFloat(match[1]) : 0;
284
284
  const next = (prev + delta + 360) % 360; // normalise to [0, 360)
285
285
  // Preserve any other transform functions (e.g. scale), replace only rotate()
286
286
  const cleaned = current.replace(/rotate\(-?[\d.]+deg\)/, '').trim();
@@ -298,10 +298,10 @@ export class ImageTooltip {
298
298
  this._hide();
299
299
  this.context.invoke('imageResizer.deselect');
300
300
  const figure = img.closest('figure.an-figure');
301
- if (figure && figure.parentNode) {
302
- figure.parentNode.removeChild(figure);
303
- } else if (img.parentNode) {
304
- img.parentNode.removeChild(img);
301
+ if (figure) {
302
+ figure.remove();
303
+ } else {
304
+ img.remove();
305
305
  }
306
306
  this.context.invoke('editor.afterCommand');
307
307
  }
@@ -327,7 +327,7 @@ export class ImageTooltip {
327
327
  this._hide();
328
328
  const range = document.createRange();
329
329
  range.selectNodeContents(cap);
330
- const sel = window.getSelection();
330
+ const sel = globalThis.getSelection();
331
331
  if (sel) { sel.removeAllRanges(); sel.addRange(range); }
332
332
  }
333
333
  return;
@@ -367,7 +367,7 @@ export class ImageTooltip {
367
367
  // Select caption text immediately
368
368
  const range = document.createRange();
369
369
  range.selectNodeContents(figcaption);
370
- const sel = window.getSelection();
370
+ const sel = globalThis.getSelection();
371
371
  if (sel) { sel.removeAllRanges(); sel.addRange(range); }
372
372
 
373
373
  this.context.invoke('editor.afterCommand');
@@ -3,41 +3,12 @@
3
3
  * Inspired by Summernote's LinkDialog — rewritten without jQuery
4
4
  */
5
5
 
6
- import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
7
- import { withSavedRange } from '../core/range.js';
6
+ import { createElement, on } from '../core/dom.js';
7
+ import { BaseDialog } from './BaseDialog.js';
8
8
 
9
- export class LinkDialog {
10
- /**
11
- * @param {import('../Context.js').Context} context
12
- */
13
- constructor(context) {
14
- this.context = context;
15
- this.options = context.options;
16
- /** @type {HTMLElement|null} */
17
- this._dialog = null;
18
- this._disposers = [];
19
- this._savedRange = null;
20
- }
21
-
22
- // ---------------------------------------------------------------------------
23
- // Lifecycle
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 && this._dialog.parentNode) {
36
- this._dialog.parentNode.removeChild(this._dialog);
37
- }
38
- this._dialog = null;
39
- }
9
+ 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"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
40
10
 
11
+ export class LinkDialog extends BaseDialog {
41
12
  // ---------------------------------------------------------------------------
42
13
  // Public API
43
14
  // ---------------------------------------------------------------------------
@@ -47,9 +18,7 @@ export class LinkDialog {
47
18
  * Pre-fills with the currently selected link if present.
48
19
  */
49
20
  show() {
50
- withSavedRange((range) => {
51
- this._savedRange = range;
52
- });
21
+ this._saveRange();
53
22
  this._prefill();
54
23
  this._open();
55
24
  }
@@ -60,16 +29,7 @@ export class LinkDialog {
60
29
 
61
30
  _buildDialog() {
62
31
  const L = this.context.locale.linkDialog;
63
- const overlay = createElement('div', { class: 'an-dialog-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': L.ariaLabel });
64
- const box = createElement('div', { class: 'an-dialog-box' });
65
-
66
- const header = createElement('div', { class: 'an-dialog-header' });
67
- const iconEl = createElement('span', { class: 'an-dialog-icon' });
68
- 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"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`;
69
- const title = createElement('h3', { class: 'an-dialog-title' });
70
- title.textContent = L.title;
71
- header.appendChild(iconEl);
72
- header.appendChild(title);
32
+ const { overlay, box } = this._buildDialogShell(L.ariaLabel, ICON_SVG, L.title);
73
33
 
74
34
  // URL field
75
35
  const urlLabel = createElement('label', { class: 'an-label' });
@@ -83,6 +43,7 @@ export class LinkDialog {
83
43
  autocomplete: 'off',
84
44
  }));
85
45
  this._urlInput = urlInput;
46
+ this._firstInput = urlInput;
86
47
 
87
48
  // Text field
88
49
  const textLabel = createElement('label', { class: 'an-label' });
@@ -108,26 +69,13 @@ export class LinkDialog {
108
69
  tabLabel.appendChild(tabCheckbox);
109
70
  tabLabel.appendChild(document.createTextNode(' ' + L.openInNewTab));
110
71
 
111
- // Buttons
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 = L.insertBtn;
115
- const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
116
- cancelBtn.textContent = L.cancelBtn;
117
- btnRow.appendChild(insertBtn);
118
- btnRow.appendChild(cancelBtn);
119
-
120
- box.append(header, urlLabel, urlInput, textLabel, textInput, tabLabel, btnRow);
121
- overlay.appendChild(box);
122
- makeDraggable(header, box);
123
-
124
- // Events
125
- const d1 = on(insertBtn, 'click', () => this._onInsert());
126
- const d2 = on(cancelBtn, 'click', () => this._close());
127
- const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
72
+ const btnRow = this._buildButtonRow(L.insertBtn, L.cancelBtn, () => this._onInsert());
73
+
74
+ box.append(urlLabel, urlInput, textLabel, textInput, tabLabel, btnRow);
75
+
128
76
  const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
129
77
  const d5 = on(textInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
130
- this._disposers.push(d1, d2, d3, d4, d5);
78
+ this._disposers.push(d4, d5);
131
79
 
132
80
  return overlay;
133
81
  }
@@ -138,7 +86,7 @@ export class LinkDialog {
138
86
 
139
87
  _prefill() {
140
88
  // Try to find currently selected anchor
141
- const sel = window.getSelection();
89
+ const sel = globalThis.getSelection();
142
90
  let anchor = null;
143
91
  if (sel && sel.rangeCount > 0) {
144
92
  let node = sel.getRangeAt(0).startContainer;
@@ -194,18 +142,4 @@ export class LinkDialog {
194
142
  this.context.invoke('editor.insertLink', url, text, newTab);
195
143
  this._close();
196
144
  }
197
-
198
- _open() {
199
- if (this._dialog) {
200
- this._dialog.style.display = 'flex';
201
- this._removeTrap = trapFocus(this._dialog, () => this._close());
202
- setTimeout(() => this._urlInput && this._urlInput.focus(), 50);
203
- }
204
- }
205
-
206
- _close() {
207
- if (this._dialog) this._dialog.style.display = 'none';
208
- if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
209
- this._savedRange = null;
210
- }
211
145
  }
@@ -46,8 +46,8 @@ export class LinkTooltip {
46
46
  }
47
47
  }),
48
48
  // Hide when the page scrolls or resizes — the tooltip position becomes stale
49
- on(window, 'scroll', () => this._hide(), { passive: true }),
50
- on(window, 'resize', () => this._hide(), { passive: true }),
49
+ on(globalThis, 'scroll', () => this._hide(), { passive: true }),
50
+ on(globalThis, 'resize', () => this._hide(), { passive: true }),
51
51
  );
52
52
 
53
53
  return this;
@@ -57,7 +57,7 @@ export class LinkTooltip {
57
57
  this._clearTimers();
58
58
  this._disposers.forEach((d) => d());
59
59
  this._disposers = [];
60
- if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
60
+ if (this._el && this._el.parentNode) this._el.remove();
61
61
  this._el = null;
62
62
  }
63
63
 
@@ -163,11 +163,11 @@ export class LinkTooltip {
163
163
  let top = rect.bottom + margin;
164
164
  let left = rect.left;
165
165
 
166
- if (top + tipH > window.innerHeight - margin) {
166
+ if (top + tipH > globalThis.innerHeight - margin) {
167
167
  top = rect.top - tipH - margin;
168
168
  }
169
- if (left + tipW > window.innerWidth - margin) {
170
- left = window.innerWidth - tipW - margin;
169
+ if (left + tipW > globalThis.innerWidth - margin) {
170
+ left = globalThis.innerWidth - tipW - margin;
171
171
  }
172
172
  if (left < margin) left = margin;
173
173
 
@@ -190,13 +190,13 @@ export class LinkTooltip {
190
190
  // ---------------------------------------------------------------------------
191
191
 
192
192
  _openLink() {
193
- const url = this._activeAnchor && this._activeAnchor.getAttribute('href');
194
- if (url) window.open(url, '_blank', 'noopener,noreferrer');
193
+ const url = this._activeAnchor?.getAttribute('href');
194
+ if (url) globalThis.open(url, '_blank', 'noopener,noreferrer');
195
195
  this._hide();
196
196
  }
197
197
 
198
198
  _copyLink() {
199
- const url = this._activeAnchor && this._activeAnchor.getAttribute('href');
199
+ const url = this._activeAnchor?.getAttribute('href');
200
200
  if (url) {
201
201
  navigator.clipboard.writeText(url).catch(() => {
202
202
  // Fallback for environments where clipboard API is unavailable
@@ -207,7 +207,7 @@ export class LinkTooltip {
207
207
  document.body.appendChild(ta);
208
208
  ta.select();
209
209
  document.execCommand('copy');
210
- document.body.removeChild(ta);
210
+ ta.remove();
211
211
  });
212
212
  }
213
213
  // Brief visual feedback
@@ -223,7 +223,7 @@ export class LinkTooltip {
223
223
  this._hide();
224
224
 
225
225
  // Place cursor inside the anchor, then open the link dialog (which pre-fills from selection)
226
- const sel = window.getSelection();
226
+ const sel = globalThis.getSelection();
227
227
  const range = document.createRange();
228
228
  range.selectNodeContents(anchor);
229
229
  sel.removeAllRanges();
@@ -238,7 +238,7 @@ export class LinkTooltip {
238
238
  this._hide();
239
239
 
240
240
  // Select the anchor text, then remove the link
241
- const sel = window.getSelection();
241
+ const sel = globalThis.getSelection();
242
242
  const range = document.createRange();
243
243
  range.selectNode(anchor);
244
244
  sel.removeAllRanges();
@@ -61,8 +61,8 @@ export class MarkdownShortcuts {
61
61
  * @returns {{ text: string, range: Range, lineNode: Node } | null}
62
62
  */
63
63
  _getLineContext() {
64
- const sel = window.getSelection();
65
- if (!sel || !sel.rangeCount) return null;
64
+ const sel = globalThis.getSelection();
65
+ if (!sel?.rangeCount) return null;
66
66
  const range = sel.getRangeAt(0);
67
67
  if (!range.collapsed) return null;
68
68
 
@@ -87,7 +87,7 @@ export class MarkdownShortcuts {
87
87
 
88
88
  _isBlock(node) {
89
89
  if (node.nodeType !== Node.ELEMENT_NODE) return false;
90
- const display = window.getComputedStyle(node).display;
90
+ const display = globalThis.getComputedStyle(node).display;
91
91
  return display === 'block' || display === 'list-item' || display === 'table-cell';
92
92
  }
93
93
 
@@ -106,7 +106,7 @@ export class MarkdownShortcuts {
106
106
  ];
107
107
 
108
108
  for (const { re, handler } of blockPatterns) {
109
- const m = text.match(re);
109
+ const m = re.exec(text);
110
110
  if (m) {
111
111
  handler(m);
112
112
  return true;
@@ -137,8 +137,8 @@ export class MarkdownShortcuts {
137
137
  // ---------------------------------------------------------------------------
138
138
 
139
139
  _selectLineAndDelete() {
140
- const sel = window.getSelection();
141
- if (!sel || !sel.rangeCount) return;
140
+ const sel = globalThis.getSelection();
141
+ if (!sel?.rangeCount) return;
142
142
  const range = sel.getRangeAt(0);
143
143
  // Select from the start of the block to the cursor and delete
144
144
  const startRange = document.createRange();
@@ -188,8 +188,8 @@ export class MarkdownShortcuts {
188
188
  // ---------------------------------------------------------------------------
189
189
 
190
190
  _onInput() {
191
- const sel = window.getSelection();
192
- if (!sel || !sel.rangeCount) return;
191
+ const sel = globalThis.getSelection();
192
+ if (!sel?.rangeCount) return;
193
193
  const range = sel.getRangeAt(0);
194
194
  if (!range.collapsed) return;
195
195
 
@@ -215,7 +215,7 @@ export class MarkdownShortcuts {
215
215
 
216
216
  const upToCursor = text.slice(0, offset);
217
217
  for (const { re, tag } of inlineRules) {
218
- const m = upToCursor.match(re);
218
+ const m = re.exec(upToCursor);
219
219
  if (!m) continue;
220
220
 
221
221
  const matchStart = upToCursor.length - m[0].length;
@@ -233,11 +233,8 @@ export class MarkdownShortcuts {
233
233
  const beforeNode = document.createTextNode(before);
234
234
  const afterNode = document.createTextNode('​' + after);
235
235
 
236
- const parent = node.parentNode;
237
- parent.insertBefore(beforeNode, node);
238
- parent.insertBefore(el, node);
239
- parent.insertBefore(afterNode, node);
240
- parent.removeChild(node);
236
+ /** @type {ChildNode} */ (node).before(beforeNode, el, afterNode);
237
+ /** @type {ChildNode} */ (node).remove();
241
238
 
242
239
  // Place cursor after the element (after the ZWS)
243
240
  const newRange = document.createRange();
@@ -76,9 +76,7 @@ export class Mention {
76
76
 
77
77
  destroy() {
78
78
  clearTimeout(this._debounceTimer);
79
- if (this._dropdown && this._dropdown.parentNode) {
80
- this._dropdown.parentNode.removeChild(this._dropdown);
81
- }
79
+ this._dropdown?.remove();
82
80
  this._dropdown = null;
83
81
  this._disposers.forEach((d) => d());
84
82
  this._disposers = [];
@@ -163,10 +161,10 @@ export class Mention {
163
161
  let top = rect.bottom + 4;
164
162
  let left = rect.left;
165
163
 
166
- if (rect.bottom + ddh + 8 > window.innerHeight) {
164
+ if (rect.bottom + ddh + 8 > globalThis.innerHeight) {
167
165
  top = rect.top - ddh - 4;
168
166
  }
169
- left = Math.max(8, Math.min(left, window.innerWidth - ddw - 8));
167
+ left = Math.max(8, Math.min(left, globalThis.innerWidth - ddw - 8));
170
168
 
171
169
  dd.style.top = `${top}px`;
172
170
  dd.style.left = `${left}px`;
@@ -196,7 +194,7 @@ export class Mention {
196
194
  _captureCaretRect() {
197
195
  // Prefer a range over the trigger character — it has non-zero width and
198
196
  // getBoundingClientRect() reliably returns a valid rect.
199
- if (this._triggerNode && this._triggerNode.isConnected) {
197
+ if (this._triggerNode?.isConnected) {
200
198
  try {
201
199
  const r = document.createRange();
202
200
  const end = Math.min(this._triggerOffset + 1, this._triggerNode.textContent.length);
@@ -211,7 +209,7 @@ export class Mention {
211
209
  }
212
210
 
213
211
  // Fallback: use getClientRects() on the current collapsed selection
214
- const sel = window.getSelection();
212
+ const sel = globalThis.getSelection();
215
213
  if (!sel || !sel.rangeCount) return;
216
214
  const rects = sel.getRangeAt(0).getClientRects();
217
215
  if (rects.length > 0) {
@@ -224,7 +222,7 @@ export class Mention {
224
222
  // ---------------------------------------------------------------------------
225
223
 
226
224
  _getQueryAtCursor() {
227
- const sel = window.getSelection();
225
+ const sel = globalThis.getSelection();
228
226
  if (!sel || !sel.rangeCount) return null;
229
227
  const range = sel.getRangeAt(0);
230
228
  if (!range.collapsed) return null;
@@ -305,7 +303,7 @@ export class Mention {
305
303
 
306
304
  _onDocClick(e) {
307
305
  if (!this._open) return;
308
- if (this._dropdown && this._dropdown.contains(e.target)) return;
306
+ if (this._dropdown?.contains(e.target)) return;
309
307
  this._hideDropdown();
310
308
  }
311
309
 
@@ -325,7 +323,7 @@ export class Mention {
325
323
  node.textContent = before + after;
326
324
 
327
325
  // Move cursor to where the trigger was
328
- const sel = window.getSelection();
326
+ const sel = globalThis.getSelection();
329
327
  const range = document.createRange();
330
328
  range.setStart(node, this._triggerOffset);
331
329
  range.collapse(true);
@@ -43,7 +43,7 @@ export class Placeholder {
43
43
  // addition to regular whitespace before deciding if the editor is empty.
44
44
  // Without this, a freshly-created checklist item or icon leaves a ZWS in
45
45
  // the DOM that causes the placeholder to overlap real content (A-1).
46
- const hasText = editable.textContent.replace(/\u200B/g, '').trim().length > 0;
46
+ const hasText = editable.textContent.replaceAll('\u200B', '').trim().length > 0;
47
47
  const isEmpty = !hasText &&
48
48
  !editable.querySelector('img, table, hr, .an-video-wrapper');
49
49
  editable.classList.toggle('an-placeholder', isEmpty && !isFocused);
@@ -69,9 +69,7 @@ export class ShortcutsDialog {
69
69
  destroy() {
70
70
  this._disposers.forEach((d) => d());
71
71
  this._disposers = [];
72
- if (this._dialog && this._dialog.parentNode) {
73
- this._dialog.parentNode.removeChild(this._dialog);
74
- }
72
+ this._dialog?.remove();
75
73
  this._dialog = null;
76
74
  }
77
75
 
@@ -79,7 +77,7 @@ export class ShortcutsDialog {
79
77
  if (this._dialog) {
80
78
  this._dialog.style.display = 'flex';
81
79
  this._removeTrap = trapFocus(this._dialog, () => this._close());
82
- setTimeout(() => this._closeBtn && this._closeBtn.focus(), 50);
80
+ setTimeout(() => this._closeBtn?.focus(), 50);
83
81
  }
84
82
  }
85
83