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
@@ -3,7 +3,7 @@
3
3
  * Inspired by Summernote's ImageDialog — rewritten without jQuery
4
4
  */
5
5
 
6
- import { createElement, on, trapFocus } from '../core/dom.js';
6
+ import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
7
7
  import { withSavedRange } from '../core/range.js';
8
8
 
9
9
  export class ImageDialog {
@@ -33,7 +33,7 @@ export class ImageDialog {
33
33
  this._disposers.forEach((d) => d());
34
34
  this._disposers = [];
35
35
  if (this._dialog && this._dialog.parentNode) {
36
- this._dialog.parentNode.removeChild(this._dialog);
36
+ this._dialog.remove();
37
37
  }
38
38
  this._dialog = null;
39
39
  }
@@ -66,32 +66,37 @@ export class ImageDialog {
66
66
  });
67
67
  const box = createElement('div', { class: 'an-dialog-box' });
68
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>`;
69
72
  const title = createElement('h3', { class: 'an-dialog-title' });
70
73
  title.textContent = L.title;
74
+ header.appendChild(iconEl);
75
+ header.appendChild(title);
71
76
 
72
77
  // URL tab
73
78
  const urlLabel = createElement('label', { class: 'an-label' });
74
79
  urlLabel.textContent = L.imageUrl;
75
- const urlInput = createElement('input', {
80
+ const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
76
81
  type: 'url',
77
82
  class: 'an-input',
78
83
  placeholder: L.urlPlaceholder,
79
84
  autocomplete: 'off',
80
- });
85
+ }));
81
86
  this._urlInput = urlInput;
82
87
 
83
88
  // Alt text
84
89
  const altLabel = createElement('label', { class: 'an-label' });
85
90
  altLabel.textContent = L.altText;
86
- const altInput = createElement('input', {
91
+ const altInput = /** @type {HTMLInputElement} */ (createElement('input', {
87
92
  type: 'text',
88
93
  class: 'an-input',
89
94
  placeholder: L.altPlaceholder,
90
95
  autocomplete: 'off',
91
- });
96
+ }));
92
97
  this._altInput = altInput;
93
98
 
94
- box.append(title, urlLabel, urlInput, altLabel, altInput);
99
+ box.append(header, urlLabel, urlInput, altLabel, altInput);
95
100
 
96
101
  // Alignment
97
102
  const alignLabel = createElement('label', { class: 'an-label' });
@@ -105,7 +110,7 @@ export class ImageDialog {
105
110
  ];
106
111
  alignOptions.forEach(({ value, label }) => {
107
112
  const radioId = `an-align-${value || 'none'}`;
108
- const radio = createElement('input', { type: 'radio', name: 'an-img-align', id: radioId, value });
113
+ const radio = /** @type {HTMLInputElement} */ (createElement('input', { type: 'radio', name: 'an-img-align', id: radioId, value }));
109
114
  if (value === '') radio.checked = true;
110
115
  const lbl = createElement('label', { for: radioId, class: 'an-align-label' });
111
116
  lbl.textContent = label;
@@ -116,11 +121,11 @@ export class ImageDialog {
116
121
  if (this.options.allowImageUpload !== false) {
117
122
  const fileLabel = createElement('label', { class: 'an-label' });
118
123
  fileLabel.textContent = L.uploadLabel;
119
- const fileInput = createElement('input', {
124
+ const fileInput = /** @type {HTMLInputElement} */ (createElement('input', {
120
125
  type: 'file',
121
126
  class: 'an-input',
122
127
  accept: 'image/jpeg,image/png,image/gif,image/webp,image/svg+xml,image/avif',
123
- });
128
+ }));
124
129
  this._fileInput = fileInput;
125
130
  // Hint line shown below the file input for format errors
126
131
  const fileHint = createElement('p', { class: 'an-dialog-hint' });
@@ -141,12 +146,13 @@ export class ImageDialog {
141
146
 
142
147
  box.append(btnRow);
143
148
  overlay.appendChild(box);
149
+ makeDraggable(header, box);
144
150
 
145
151
  const d1 = on(insertBtn, 'click', () => this._onInsert());
146
152
  const d2 = on(cancelBtn, 'click', () => this._close());
147
153
  const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
148
- const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
149
- const d5 = on(altInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
154
+ const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
155
+ const d5 = on(altInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
150
156
  this._disposers.push(d1, d2, d3, d4, d5);
151
157
 
152
158
  return overlay;
@@ -197,7 +203,7 @@ export class ImageDialog {
197
203
  const src = this._urlInput.value.trim();
198
204
  const alt = this._altInput.value.trim();
199
205
  const alignRadio = this._alignRow && this._alignRow.querySelector('input[name="an-img-align"]:checked');
200
- const align = alignRadio ? alignRadio.value : '';
206
+ const align = alignRadio ? /** @type {HTMLInputElement} */ (alignRadio).value : '';
201
207
 
202
208
  if (!src) {
203
209
  this._urlInput.focus();
@@ -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 = () => {
@@ -51,12 +51,12 @@ export class ImageResizer {
51
51
  // Also select on right-click so the highlight shows before the context menu
52
52
  on(editable, 'contextmenu', (e) => {
53
53
  if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
54
- const img = e.target.closest('img');
54
+ const img = /** @type {Element} */ (e.target)?.closest('img');
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';
@@ -39,24 +39,28 @@ export class ImageTooltip {
39
39
  this._disposers.push(
40
40
  on(editable, 'mouseover', (e) => {
41
41
  if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
42
- const img = e.target.closest('img');
42
+ const img = /** @type {Element} */ (e.target)?.closest('img');
43
43
  // Skip when the image is inside a link — LinkTooltip takes priority there
44
44
  if (img && editable.contains(img) && !img.closest('a[href]')) {
45
45
  this._scheduleShow(img);
46
46
  }
47
47
  }, { passive: true }),
48
48
  on(editable, 'mouseout', (e) => {
49
- const to = e.relatedTarget;
49
+ const to = /** @type {Node|null} */ (/** @type {MouseEvent} */ (e).relatedTarget);
50
50
  if (!to || (!editable.contains(to) && !this._el.contains(to))) {
51
51
  this._scheduleHide();
52
52
  }
53
53
  }, { passive: true }),
54
54
  // Hide when image is deselected by clicking elsewhere
55
55
  on(document, 'click', (e) => {
56
- if (this._activeImg && !this._activeImg.contains(e.target) && !this._el.contains(e.target)) {
56
+ const et = /** @type {Node} */ (e.target);
57
+ if (this._activeImg && !this._activeImg.contains(et) && !this._el.contains(et)) {
57
58
  this._hide();
58
59
  }
59
60
  }),
61
+ // Hide when the page scrolls or resizes — the tooltip position becomes stale
62
+ on(globalThis, 'scroll', () => this._hide(), { passive: true }),
63
+ on(globalThis, 'resize', () => this._hide(), { passive: true }),
60
64
  );
61
65
 
62
66
  return this;
@@ -66,7 +70,7 @@ export class ImageTooltip {
66
70
  this._clearTimers();
67
71
  this._disposers.forEach((d) => d());
68
72
  this._disposers = [];
69
- if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
73
+ this._el?.remove();
70
74
  this._el = null;
71
75
  }
72
76
 
@@ -181,7 +185,7 @@ export class ImageTooltip {
181
185
  this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
182
186
  }
183
187
 
184
- _show(img) {
188
+ _show(_img) {
185
189
  this._el.style.display = 'flex';
186
190
  // Defer positioning: offsetWidth on a newly-visible element forces layout
187
191
  requestAnimationFrame(() => {
@@ -211,8 +215,8 @@ export class ImageTooltip {
211
215
  let top = rect.bottom + margin;
212
216
  let left = rect.left + (rect.width - tipW) / 2;
213
217
 
214
- if (top + tipH > window.innerHeight - margin) top = rect.top - tipH - margin;
215
- 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;
216
220
  if (left < margin) left = margin;
217
221
 
218
222
  this._el.style.top = `${top}px`;
@@ -275,8 +279,8 @@ export class ImageTooltip {
275
279
  if (!img) return;
276
280
  // Parse current rotation angle from inline transform
277
281
  const current = img.style.transform || '';
278
- const match = current.match(/rotate\((-?[\d.]+)deg\)/);
279
- const prev = match ? parseFloat(match[1]) : 0;
282
+ const match = /rotate\((-?[\d.]+)deg\)/.exec(current);
283
+ const prev = match ? Number.parseFloat(match[1]) : 0;
280
284
  const next = (prev + delta + 360) % 360; // normalise to [0, 360)
281
285
  // Preserve any other transform functions (e.g. scale), replace only rotate()
282
286
  const cleaned = current.replace(/rotate\(-?[\d.]+deg\)/, '').trim();
@@ -294,10 +298,10 @@ export class ImageTooltip {
294
298
  this._hide();
295
299
  this.context.invoke('imageResizer.deselect');
296
300
  const figure = img.closest('figure.an-figure');
297
- if (figure && figure.parentNode) {
298
- figure.parentNode.removeChild(figure);
299
- } else if (img.parentNode) {
300
- img.parentNode.removeChild(img);
301
+ if (figure) {
302
+ figure.remove();
303
+ } else {
304
+ img.remove();
301
305
  }
302
306
  this.context.invoke('editor.afterCommand');
303
307
  }
@@ -323,7 +327,7 @@ export class ImageTooltip {
323
327
  this._hide();
324
328
  const range = document.createRange();
325
329
  range.selectNodeContents(cap);
326
- const sel = window.getSelection();
330
+ const sel = globalThis.getSelection();
327
331
  if (sel) { sel.removeAllRanges(); sel.addRange(range); }
328
332
  }
329
333
  return;
@@ -363,7 +367,7 @@ export class ImageTooltip {
363
367
  // Select caption text immediately
364
368
  const range = document.createRange();
365
369
  range.selectNodeContents(figcaption);
366
- const sel = window.getSelection();
370
+ const sel = globalThis.getSelection();
367
371
  if (sel) { sel.removeAllRanges(); sel.addRange(range); }
368
372
 
369
373
  this.context.invoke('editor.afterCommand');
@@ -3,7 +3,7 @@
3
3
  * Inspired by Summernote's LinkDialog — rewritten without jQuery
4
4
  */
5
5
 
6
- import { createElement, on, trapFocus } from '../core/dom.js';
6
+ import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
7
7
  import { withSavedRange } from '../core/range.js';
8
8
 
9
9
  export class LinkDialog {
@@ -33,7 +33,7 @@ export class LinkDialog {
33
33
  this._disposers.forEach((d) => d());
34
34
  this._disposers = [];
35
35
  if (this._dialog && this._dialog.parentNode) {
36
- this._dialog.parentNode.removeChild(this._dialog);
36
+ this._dialog.remove();
37
37
  }
38
38
  this._dialog = null;
39
39
  }
@@ -63,42 +63,47 @@ export class LinkDialog {
63
63
  const overlay = createElement('div', { class: 'an-dialog-overlay', role: 'dialog', 'aria-modal': 'true', 'aria-label': L.ariaLabel });
64
64
  const box = createElement('div', { class: 'an-dialog-box' });
65
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>`;
66
69
  const title = createElement('h3', { class: 'an-dialog-title' });
67
70
  title.textContent = L.title;
71
+ header.appendChild(iconEl);
72
+ header.appendChild(title);
68
73
 
69
74
  // URL field
70
75
  const urlLabel = createElement('label', { class: 'an-label' });
71
76
  urlLabel.textContent = L.url;
72
- const urlInput = createElement('input', {
77
+ const urlInput = /** @type {HTMLInputElement} */ (createElement('input', {
73
78
  type: 'url',
74
79
  class: 'an-input',
75
80
  placeholder: L.urlPlaceholder,
76
81
  id: 'an-link-url',
77
82
  name: 'url',
78
83
  autocomplete: 'off',
79
- });
84
+ }));
80
85
  this._urlInput = urlInput;
81
86
 
82
87
  // Text field
83
88
  const textLabel = createElement('label', { class: 'an-label' });
84
89
  textLabel.textContent = L.displayText;
85
- const textInput = createElement('input', {
90
+ const textInput = /** @type {HTMLInputElement} */ (createElement('input', {
86
91
  type: 'text',
87
92
  class: 'an-input',
88
93
  placeholder: L.textPlaceholder,
89
94
  id: 'an-link-text',
90
95
  name: 'linkText',
91
96
  autocomplete: 'off',
92
- });
97
+ }));
93
98
  this._textInput = textInput;
94
99
 
95
100
  // Open in new tab
96
101
  const tabLabel = createElement('label', { class: 'an-label an-label-inline' });
97
- const tabCheckbox = createElement('input', {
102
+ const tabCheckbox = /** @type {HTMLInputElement} */ (createElement('input', {
98
103
  type: 'checkbox',
99
104
  id: 'an-link-newtab',
100
105
  name: 'openInNewTab',
101
- });
106
+ }));
102
107
  this._tabCheckbox = tabCheckbox;
103
108
  tabLabel.appendChild(tabCheckbox);
104
109
  tabLabel.appendChild(document.createTextNode(' ' + L.openInNewTab));
@@ -112,15 +117,16 @@ export class LinkDialog {
112
117
  btnRow.appendChild(insertBtn);
113
118
  btnRow.appendChild(cancelBtn);
114
119
 
115
- box.append(title, urlLabel, urlInput, textLabel, textInput, tabLabel, btnRow);
120
+ box.append(header, urlLabel, urlInput, textLabel, textInput, tabLabel, btnRow);
116
121
  overlay.appendChild(box);
122
+ makeDraggable(header, box);
117
123
 
118
124
  // Events
119
125
  const d1 = on(insertBtn, 'click', () => this._onInsert());
120
126
  const d2 = on(cancelBtn, 'click', () => this._close());
121
127
  const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
122
- const d4 = on(urlInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
123
- const d5 = on(textInput, 'keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this._onInsert(); } });
128
+ const d4 = on(urlInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
129
+ const d5 = on(textInput, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Enter') { e.preventDefault(); this._onInsert(); } });
124
130
  this._disposers.push(d1, d2, d3, d4, d5);
125
131
 
126
132
  return overlay;
@@ -132,7 +138,7 @@ export class LinkDialog {
132
138
 
133
139
  _prefill() {
134
140
  // Try to find currently selected anchor
135
- const sel = window.getSelection();
141
+ const sel = globalThis.getSelection();
136
142
  let anchor = null;
137
143
  if (sel && sel.rangeCount > 0) {
138
144
  let node = sel.getRangeAt(0).startContainer;
@@ -142,9 +148,9 @@ export class LinkDialog {
142
148
  }
143
149
  }
144
150
  if (anchor) {
145
- this._urlInput.value = anchor.getAttribute('href') || '';
151
+ this._urlInput.value = /** @type {Element} */ (anchor).getAttribute('href') || '';
146
152
  this._textInput.value = anchor.textContent || '';
147
- this._tabCheckbox.checked = anchor.getAttribute('target') === '_blank';
153
+ this._tabCheckbox.checked = /** @type {Element} */ (anchor).getAttribute('target') === '_blank';
148
154
  } else {
149
155
  this._urlInput.value = '';
150
156
  this._textInput.value = sel ? sel.toString() : '';
@@ -33,18 +33,21 @@ export class LinkTooltip {
33
33
  this._disposers.push(
34
34
  // Detect when pointer enters a link
35
35
  on(editable, 'mouseover', (e) => {
36
- const anchor = e.target.closest('a[href]');
36
+ const anchor = /** @type {Element} */ (e.target)?.closest('a[href]');
37
37
  if (anchor && editable.contains(anchor)) {
38
38
  this._scheduleShow(anchor);
39
39
  }
40
40
  }),
41
41
  // Detect when pointer leaves the editable area entirely
42
42
  on(editable, 'mouseout', (e) => {
43
- const to = e.relatedTarget;
43
+ const to = /** @type {Node|null} */ (/** @type {MouseEvent} */ (e).relatedTarget);
44
44
  if (!to || (!editable.contains(to) && !this._el.contains(to))) {
45
45
  this._scheduleHide();
46
46
  }
47
47
  }),
48
+ // Hide when the page scrolls or resizes — the tooltip position becomes stale
49
+ on(globalThis, 'scroll', () => this._hide(), { passive: true }),
50
+ on(globalThis, 'resize', () => this._hide(), { passive: true }),
48
51
  );
49
52
 
50
53
  return this;
@@ -54,7 +57,7 @@ export class LinkTooltip {
54
57
  this._clearTimers();
55
58
  this._disposers.forEach((d) => d());
56
59
  this._disposers = [];
57
- if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
60
+ if (this._el && this._el.parentNode) this._el.remove();
58
61
  this._el = null;
59
62
  }
60
63
 
@@ -160,11 +163,11 @@ export class LinkTooltip {
160
163
  let top = rect.bottom + margin;
161
164
  let left = rect.left;
162
165
 
163
- if (top + tipH > window.innerHeight - margin) {
166
+ if (top + tipH > globalThis.innerHeight - margin) {
164
167
  top = rect.top - tipH - margin;
165
168
  }
166
- if (left + tipW > window.innerWidth - margin) {
167
- left = window.innerWidth - tipW - margin;
169
+ if (left + tipW > globalThis.innerWidth - margin) {
170
+ left = globalThis.innerWidth - tipW - margin;
168
171
  }
169
172
  if (left < margin) left = margin;
170
173
 
@@ -187,13 +190,13 @@ export class LinkTooltip {
187
190
  // ---------------------------------------------------------------------------
188
191
 
189
192
  _openLink() {
190
- const url = this._activeAnchor && this._activeAnchor.getAttribute('href');
191
- if (url) window.open(url, '_blank', 'noopener,noreferrer');
193
+ const url = this._activeAnchor?.getAttribute('href');
194
+ if (url) globalThis.open(url, '_blank', 'noopener,noreferrer');
192
195
  this._hide();
193
196
  }
194
197
 
195
198
  _copyLink() {
196
- const url = this._activeAnchor && this._activeAnchor.getAttribute('href');
199
+ const url = this._activeAnchor?.getAttribute('href');
197
200
  if (url) {
198
201
  navigator.clipboard.writeText(url).catch(() => {
199
202
  // Fallback for environments where clipboard API is unavailable
@@ -204,7 +207,7 @@ export class LinkTooltip {
204
207
  document.body.appendChild(ta);
205
208
  ta.select();
206
209
  document.execCommand('copy');
207
- document.body.removeChild(ta);
210
+ ta.remove();
208
211
  });
209
212
  }
210
213
  // Brief visual feedback
@@ -220,7 +223,7 @@ export class LinkTooltip {
220
223
  this._hide();
221
224
 
222
225
  // Place cursor inside the anchor, then open the link dialog (which pre-fills from selection)
223
- const sel = window.getSelection();
226
+ const sel = globalThis.getSelection();
224
227
  const range = document.createRange();
225
228
  range.selectNodeContents(anchor);
226
229
  sel.removeAllRanges();
@@ -235,7 +238,7 @@ export class LinkTooltip {
235
238
  this._hide();
236
239
 
237
240
  // Select the anchor text, then remove the link
238
- const sel = window.getSelection();
241
+ const sel = globalThis.getSelection();
239
242
  const range = document.createRange();
240
243
  range.selectNode(anchor);
241
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 = [];
@@ -96,11 +94,11 @@ export class Mention {
96
94
  // Event delegation: single listeners on the container instead of per-item
97
95
  el.addEventListener('mousedown', (e) => e.preventDefault());
98
96
  el.addEventListener('click', (e) => {
99
- const item = e.target.closest('.an-mention-item');
97
+ const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-mention-item'));
100
98
  if (item) this._select(+item.dataset.index);
101
99
  });
102
100
  el.addEventListener('mousemove', (e) => {
103
- const item = e.target.closest('.an-mention-item');
101
+ const item = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-mention-item'));
104
102
  if (item) this._highlightItem(+item.dataset.index);
105
103
  });
106
104
 
@@ -119,7 +117,7 @@ export class Mention {
119
117
  const li = document.createElement('div');
120
118
  li.className = 'an-mention-item';
121
119
  li.setAttribute('role', 'option');
122
- li.dataset.index = i;
120
+ li.dataset.index = String(i);
123
121
  if (item.avatar) {
124
122
  const img = document.createElement('img');
125
123
  img.src = item.avatar;
@@ -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