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
@@ -7,7 +7,7 @@
7
7
  * single / replace-all replacement.
8
8
  */
9
9
 
10
- import { createElement, on, trapFocus } from '../core/dom.js';
10
+ import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
11
11
 
12
12
  export class FindReplace {
13
13
  /** @param {import('../Context.js').Context} context */
@@ -61,7 +61,7 @@ export class FindReplace {
61
61
  this._disposers.forEach((d) => d());
62
62
  this._disposers = [];
63
63
  if (this._dialog && this._dialog.parentNode) {
64
- this._dialog.parentNode.removeChild(this._dialog);
64
+ this._dialog.remove();
65
65
  }
66
66
  this._dialog = null;
67
67
  }
@@ -120,8 +120,8 @@ export class FindReplace {
120
120
  const title = this._dialog.querySelector('.an-dialog-title');
121
121
 
122
122
  const isReplace = this._mode === 'replace';
123
- if (replaceRow) replaceRow.style.display = isReplace ? '' : 'none';
124
- if (replaceActions) replaceActions.style.display = isReplace ? '' : 'none';
123
+ if (replaceRow) /** @type {HTMLElement} */ (replaceRow).style.display = isReplace ? '' : 'none';
124
+ if (replaceActions) /** @type {HTMLElement} */ (replaceActions).style.display = isReplace ? '' : 'none';
125
125
  if (title) title.textContent = isReplace ? this.context.locale.findReplace.findReplaceTitle : this.context.locale.findReplace.findTitle;
126
126
  }
127
127
 
@@ -137,107 +137,133 @@ export class FindReplace {
137
137
  'aria-modal': 'true',
138
138
  'aria-label': L.findReplaceTitle,
139
139
  });
140
- const box = createElement('div', { class: 'an-dialog-box' });
140
+ const box = createElement('div', { class: 'an-dialog-box an-fr-box' });
141
141
 
142
- // ---- Title row ----
143
- const titleRow = createElement('div', { class: 'an-icon-title-row' });
142
+ // ---- Header: [icon][title] [×] ----
143
+ const header = createElement('div', { class: 'an-fr-header' });
144
+ const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
145
+ const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
146
+ iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`;
144
147
  const title = createElement('h3', { class: 'an-dialog-title' });
145
148
  title.textContent = L.findTitle;
149
+ titleGroup.append(iconEl, title);
146
150
  const closeBtn = createElement('button', {
147
151
  type: 'button',
148
152
  class: 'an-icon-close',
149
- 'aria-label': 'Close',
153
+ title: L.close,
154
+ 'aria-label': L.close,
150
155
  });
151
- closeBtn.textContent = L.close;
156
+ closeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
152
157
  this._closeBtn = closeBtn;
153
- titleRow.append(title, closeBtn);
154
- box.appendChild(titleRow);
158
+ header.append(titleGroup, closeBtn);
159
+ box.appendChild(header);
155
160
 
156
- // ---- Find row ----
157
- const findRow = createElement('div', { class: 'an-fr-find-row' });
158
- const findInput = createElement('input', {
161
+ // ---- Search bar: [input] [Aa] [↑] [↓] counter ----
162
+ const searchBar = createElement('div', { class: 'an-fr-search-bar' });
163
+ const findInput = /** @type {HTMLInputElement} */ (createElement('input', {
159
164
  type: 'text',
160
- class: 'an-input',
165
+ class: 'an-input an-fr-input',
161
166
  placeholder: L.findPlaceholder,
162
167
  'aria-label': L.searchAriaLabel,
163
- });
168
+ }));
164
169
  this._findInput = findInput;
165
- findRow.appendChild(findInput);
166
- box.appendChild(findRow);
167
170
 
168
- // ---- Options row (case-sensitive toggle + match counter) ----
169
- const optRow = createElement('div', { class: 'an-fr-options-row' });
170
- const caseLabel = createElement('label', { class: 'an-label an-label-inline' });
171
- const caseCheckbox = createElement('input', {
171
+ // Case-sensitive toggle (visual button; hidden checkbox kept for state)
172
+ const caseCheckbox = /** @type {HTMLInputElement} */ (createElement('input', {
172
173
  type: 'checkbox',
174
+ style: 'display:none',
175
+ 'aria-hidden': 'true',
176
+ }));
177
+ this._caseCheckbox = caseCheckbox;
178
+ const caseBtn = createElement('button', {
179
+ type: 'button',
180
+ class: 'an-fr-icon-btn',
181
+ title: 'Case sensitive',
173
182
  'aria-label': 'Case sensitive',
174
183
  });
175
- this._caseCheckbox = caseCheckbox;
176
- caseLabel.append(caseCheckbox, document.createTextNode(L.caseSensitive));
184
+ caseBtn.textContent = 'Aa';
185
+
186
+ const prevBtn = createElement('button', {
187
+ type: 'button',
188
+ class: 'an-fr-icon-btn',
189
+ title: 'Previous (Shift+Enter)',
190
+ 'aria-label': 'Previous',
191
+ });
192
+ prevBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>`;
193
+
194
+ const nextBtn = createElement('button', {
195
+ type: 'button',
196
+ class: 'an-fr-icon-btn',
197
+ title: 'Next (Enter)',
198
+ 'aria-label': 'Next',
199
+ });
200
+ nextBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;
201
+
177
202
  const counter = createElement('span', { class: 'an-fr-counter' });
178
203
  this._counterEl = counter;
179
- optRow.append(caseLabel, counter);
180
- box.appendChild(optRow);
181
-
182
- // ---- Find actions ----
183
- const findActions = createElement('div', { class: 'an-dialog-actions an-fr-find-actions' });
184
- const prevBtn = createElement('button', { type: 'button', class: 'an-btn' });
185
- prevBtn.textContent = L.prevBtn;
186
- const nextBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
187
- nextBtn.textContent = L.nextBtn;
188
- findActions.append(prevBtn, nextBtn);
189
- box.appendChild(findActions);
190
-
191
- // ---- Replace row (hidden by default) ----
204
+
205
+ searchBar.append(findInput, caseCheckbox, caseBtn, prevBtn, nextBtn, counter);
206
+ box.appendChild(searchBar);
207
+
208
+ // ---- Replace row: [input] [Replace] [All] (hidden by default) ----
192
209
  const replaceRow = createElement('div', { class: 'an-fr-replace-row' });
193
210
  replaceRow.style.display = 'none';
194
- const replaceInput = createElement('input', {
211
+ const replaceInput = /** @type {HTMLInputElement} */ (createElement('input', {
195
212
  type: 'text',
196
- class: 'an-input',
213
+ class: 'an-input an-fr-input',
197
214
  placeholder: L.replacePlaceholder,
198
215
  'aria-label': L.replaceAriaLabel,
199
- });
216
+ }));
200
217
  this._replaceInput = replaceInput;
201
- replaceRow.appendChild(replaceInput);
218
+ const replaceBtn = createElement('button', { type: 'button', class: 'an-btn an-fr-replace-btn' });
219
+ replaceBtn.textContent = L.replaceBtn;
220
+ const replaceAllBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary an-fr-replace-btn' });
221
+ replaceAllBtn.textContent = L.replaceAllBtn;
222
+ replaceRow.append(replaceInput, replaceBtn, replaceAllBtn);
202
223
  box.appendChild(replaceRow);
203
224
 
204
- // ---- Replace actions (hidden by default) ----
205
- const replaceActions = createElement('div', { class: 'an-dialog-actions an-fr-replace-actions' });
225
+ // Compat div — _updateMode toggles this; kept empty so existing logic works
226
+ const replaceActions = createElement('div', { class: 'an-fr-replace-actions' });
206
227
  replaceActions.style.display = 'none';
207
- const replaceBtn = createElement('button', { type: 'button', class: 'an-btn' });
208
- replaceBtn.textContent = L.replaceBtn;
209
- const replaceAllBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary' });
210
- replaceAllBtn.textContent = L.replaceAllBtn;
211
- replaceActions.append(replaceBtn, replaceAllBtn);
212
228
  box.appendChild(replaceActions);
213
229
 
214
230
  overlay.appendChild(box);
231
+ makeDraggable(header, box);
215
232
 
216
233
  // ---- Event bindings ----
217
234
  const d1 = on(closeBtn, 'click', () => this._close());
218
235
  const d2 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
219
236
  const d3 = on(findInput, 'input', () => this._onSearch());
220
- const d4 = on(caseCheckbox, 'change', () => {
237
+ const d4 = on(caseBtn, 'click', () => {
238
+ this._caseSensitive = !this._caseSensitive;
239
+ caseCheckbox.checked = this._caseSensitive;
240
+ caseBtn.classList.toggle('an-fr-icon-btn--active', this._caseSensitive);
241
+ this._onSearch();
242
+ });
243
+ // Hidden checkbox change handler keeps backward-compat with programmatic toggles
244
+ const d5 = on(caseCheckbox, 'change', () => {
221
245
  this._caseSensitive = caseCheckbox.checked;
246
+ caseBtn.classList.toggle('an-fr-icon-btn--active', this._caseSensitive);
222
247
  this._onSearch();
223
248
  });
224
- const d5 = on(nextBtn, 'click', () => this._next());
225
- const d6 = on(prevBtn, 'click', () => this._prev());
226
- const d7 = on(replaceBtn, 'click', () => this._replace());
227
- const d8 = on(replaceAllBtn, 'click', () => this._replaceAll());
228
- const d9 = on(findInput, 'keydown', (e) => {
229
- if (e.key === 'Enter') {
249
+ const d6 = on(nextBtn, 'click', () => this._next());
250
+ const d7 = on(prevBtn, 'click', () => this._prev());
251
+ const d8 = on(replaceBtn, 'click', () => this._replace());
252
+ const d9 = on(replaceAllBtn, 'click', () => this._replaceAll());
253
+ const d10 = on(findInput, 'keydown', (e) => {
254
+ const ke = /** @type {KeyboardEvent} */ (e);
255
+ if (ke.key === 'Enter') {
230
256
  e.preventDefault();
231
- e.shiftKey ? this._prev() : this._next();
257
+ ke.shiftKey ? this._prev() : this._next();
232
258
  }
233
259
  });
234
- const d10 = on(replaceInput, 'keydown', (e) => {
235
- if (e.key === 'Enter') {
260
+ const d11 = on(replaceInput, 'keydown', (e) => {
261
+ if (/** @type {KeyboardEvent} */ (e).key === 'Enter') {
236
262
  e.preventDefault();
237
263
  this._replace();
238
264
  }
239
265
  });
240
- this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10);
266
+ this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11);
241
267
 
242
268
  return overlay;
243
269
  }
@@ -326,13 +352,14 @@ export class FindReplace {
326
352
  // Cap results to prevent blocking the main thread on very large documents
327
353
  const MAX_RESULTS = 500;
328
354
  const walker = document.createTreeWalker(root, 0x4 /* NodeFilter.SHOW_TEXT */);
329
- let node;
330
- while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
355
+ let node = walker.nextNode();
356
+ while (node && results.length < MAX_RESULTS) {
331
357
  re.lastIndex = 0;
332
358
  let m;
333
- while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) {
334
- results.push({ node, start: m.index, end: m.index + m[0].length });
359
+ while ((m = re.exec(/** @type {Text} */ (node).textContent)) !== null && results.length < MAX_RESULTS) {
360
+ results.push({ node: /** @type {Text} */ (node), start: m.index, end: m.index + m[0].length });
335
361
  }
362
+ node = walker.nextNode();
336
363
  }
337
364
  return results;
338
365
  }
@@ -382,7 +409,7 @@ export class FindReplace {
382
409
  const parent = match.mark.parentNode;
383
410
  const textNode = document.createTextNode(replacement);
384
411
  parent.insertBefore(textNode, match.mark);
385
- parent.removeChild(match.mark);
412
+ match.mark.remove();
386
413
  parent.normalize();
387
414
  this.context.invoke('editor.afterCommand');
388
415
  const savedIndex = this._currentIndex;
@@ -402,7 +429,7 @@ export class FindReplace {
402
429
  if (!mark || !mark.parentNode) return;
403
430
  const textNode = document.createTextNode(replacement);
404
431
  mark.parentNode.insertBefore(textNode, mark);
405
- mark.parentNode.removeChild(mark);
432
+ mark.remove();
406
433
  });
407
434
 
408
435
  if (this.context.layoutInfo.editable) {
@@ -430,7 +457,7 @@ export class FindReplace {
430
457
  const parent = mark.parentNode;
431
458
  if (!parent) return;
432
459
  while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
433
- parent.removeChild(mark);
460
+ mark.remove();
434
461
  });
435
462
  // Merge adjacent text nodes after unwrapping
436
463
  editable.normalize();
@@ -21,7 +21,7 @@ export class Fullscreen {
21
21
  initialize() {
22
22
  // Press Escape to exit fullscreen
23
23
  const d = on(document, 'keydown', (event) => {
24
- if (this._active && isKey(event, key.ESCAPE)) {
24
+ if (this._active && isKey(/** @type {KeyboardEvent} */ (event), key.ESCAPE)) {
25
25
  this.deactivate();
26
26
  }
27
27
  });
@@ -2,7 +2,7 @@
2
2
  * IconDialog.js - Browse and insert FontAwesome Free icons
3
3
  */
4
4
 
5
- import { createElement, on, trapFocus } from '../core/dom.js';
5
+ import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
6
6
  import { withSavedRange } from '../core/range.js';
7
7
 
8
8
  // ---------------------------------------------------------------------------
@@ -281,7 +281,7 @@ export class IconDialog {
281
281
  // Already present (icon element or stylesheet link)?
282
282
  if (document.getElementById('an-fontawesome-css')) return;
283
283
  const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
284
- .map((l) => l.href || '').join(' ');
284
+ .map((l) => /** @type {HTMLLinkElement} */ (l).href || '').join(' ');
285
285
  if (/fontawesome|font-awesome/.test(links)) return;
286
286
  if (document.querySelector('.fa-solid, .fas, .far, .fab')) return;
287
287
 
@@ -297,9 +297,7 @@ export class IconDialog {
297
297
  destroy() {
298
298
  this._disposers.forEach((d) => d());
299
299
  this._disposers = [];
300
- if (this._dialog && this._dialog.parentNode) {
301
- this._dialog.parentNode.removeChild(this._dialog);
302
- }
300
+ this._dialog?.remove();
303
301
  this._dialog = null;
304
302
  }
305
303
 
@@ -344,19 +342,23 @@ export class IconDialog {
344
342
 
345
343
  // Title row
346
344
  const titleRow = createElement('div', { class: 'an-icon-title-row' });
345
+ const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
346
+ const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
347
+ iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>`;
347
348
  const title = createElement('h3', { class: 'an-dialog-title' });
348
349
  title.textContent = L.title;
350
+ titleGroup.append(iconEl, title);
349
351
  const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
350
352
  closeBtn.innerHTML = '&times;';
351
- titleRow.append(title, closeBtn);
353
+ titleRow.append(titleGroup, closeBtn);
352
354
 
353
355
  // Search
354
- const searchInput = createElement('input', {
356
+ const searchInput = /** @type {HTMLInputElement} */ (createElement('input', {
355
357
  type: 'search',
356
358
  class: 'an-input an-icon-search',
357
359
  placeholder: L.searchPlaceholder,
358
360
  autocomplete: 'off',
359
- });
361
+ }));
360
362
  this._searchInput = searchInput;
361
363
 
362
364
  // Category tabs
@@ -366,7 +368,7 @@ export class IconDialog {
366
368
  catBar.appendChild(allTab);
367
369
  ICON_CATEGORIES.forEach(({ id, label }) => {
368
370
  const tab = createElement('button', { type: 'button', class: 'an-icon-cat', 'data-cat': id });
369
- tab.textContent = (L.categories && L.categories[id]) || label;
371
+ tab.textContent = L.categories?.[id] || label;
370
372
  catBar.appendChild(tab);
371
373
  });
372
374
  this._catBar = catBar;
@@ -388,7 +390,7 @@ export class IconDialog {
388
390
 
389
391
  const styleLabel = createElement('label', { class: 'an-label' });
390
392
  styleLabel.textContent = L.style;
391
- const styleSelect = createElement('select', { class: 'an-input an-icon-option-select' });
393
+ const styleSelect = /** @type {HTMLSelectElement} */ (createElement('select', { class: 'an-input an-icon-option-select' }));
392
394
  [['fa-solid', 'Solid'], ['fa-regular', 'Regular'], ['fa-light', 'Light (Pro)']].forEach(([v, t]) => {
393
395
  const opt = createElement('option', { value: v });
394
396
  opt.textContent = t;
@@ -399,9 +401,9 @@ export class IconDialog {
399
401
 
400
402
  const sizeLabel = createElement('label', { class: 'an-label' });
401
403
  sizeLabel.textContent = L.size;
402
- const sizeSelect = createElement('select', { class: 'an-input an-icon-option-select' });
404
+ const sizeSelect = /** @type {HTMLSelectElement} */ (createElement('select', { class: 'an-input an-icon-option-select' }));
403
405
  [['', 'Inherit'], ['0.75em', '0.75em'], ['1em', '1em'], ['1.25em', '1.25em'], ['1.5em', '1.5em'], ['2em', '2em'], ['3em', '3em']].forEach(([v, t]) => {
404
- const opt = createElement('option', { value: v });
406
+ const opt = /** @type {HTMLOptionElement} */ (createElement('option', { value: v }));
405
407
  if (v === '1em') opt.selected = true;
406
408
  opt.textContent = t;
407
409
  sizeSelect.appendChild(opt);
@@ -410,11 +412,11 @@ export class IconDialog {
410
412
 
411
413
  const colorLabel = createElement('label', { class: 'an-label' });
412
414
  colorLabel.textContent = L.color;
413
- const colorInput = createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' });
415
+ const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' }));
414
416
  this._colorInput = colorInput;
415
417
 
416
418
  const useColorLabel = createElement('label', { class: 'an-label an-label-inline an-icon-use-color' });
417
- const useColorCb = createElement('input', { type: 'checkbox', checked: '' });
419
+ const useColorCb = /** @type {HTMLInputElement} */ (createElement('input', { type: 'checkbox', checked: '' }));
418
420
  this._useColorCb = useColorCb;
419
421
  useColorLabel.append(useColorCb, document.createTextNode(L.useColor));
420
422
 
@@ -438,6 +440,7 @@ export class IconDialog {
438
440
 
439
441
  box.append(titleRow, searchInput, catBar, grid, optRow, preview, btnRow);
440
442
  overlay.appendChild(box);
443
+ makeDraggable(titleRow, box);
441
444
 
442
445
  // -------------------------------------------------------------------------
443
446
  // Events
@@ -449,7 +452,7 @@ export class IconDialog {
449
452
  const d4 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
450
453
  const d5 = on(searchInput, 'input', () => this._filterIcons(searchInput.value, this._activeCat));
451
454
  const d6 = on(catBar, 'click', (e) => {
452
- const tab = e.target.closest('[data-cat]');
455
+ const tab = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-cat]'));
453
456
  if (tab) {
454
457
  this._activeCat = tab.dataset.cat;
455
458
  this._updateCatTabs();
@@ -457,7 +460,7 @@ export class IconDialog {
457
460
  }
458
461
  });
459
462
  const d7 = on(grid, 'click', (e) => {
460
- const cell = e.target.closest('.an-icon-cell');
463
+ const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-icon-cell'));
461
464
  if (cell) this._selectIcon(cell.dataset.name);
462
465
  });
463
466
  const d8 = on(styleSelect, 'change', () => this._updatePreview(this._selectedIcon));
@@ -476,7 +479,7 @@ export class IconDialog {
476
479
 
477
480
  _updateCatTabs() {
478
481
  this._catBar.querySelectorAll('.an-icon-cat').forEach((tab) => {
479
- tab.classList.toggle('active', tab.dataset.cat === this._activeCat);
482
+ tab.classList.toggle('active', /** @type {HTMLElement} */ (tab).dataset.cat === this._activeCat);
480
483
  });
481
484
  }
482
485
 
@@ -484,12 +487,13 @@ export class IconDialog {
484
487
  const q = (query || '').trim().toLowerCase();
485
488
  let visibleCount = 0;
486
489
  this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
487
- const name = cell.dataset.name;
488
- const cellCat = cell.dataset.cat;
490
+ const hCell = /** @type {HTMLElement} */ (cell);
491
+ const name = hCell.dataset.name;
492
+ const cellCat = hCell.dataset.cat;
489
493
  const matchesCat = !cat || cat === 'all' || cellCat === cat;
490
494
  const matchesQuery = !q || name.includes(q);
491
495
  const visible = matchesCat && matchesQuery;
492
- cell.style.display = visible ? '' : 'none';
496
+ hCell.style.display = visible ? '' : 'none';
493
497
  if (visible) visibleCount++;
494
498
  });
495
499
  // Show empty state if needed
@@ -499,14 +503,14 @@ export class IconDialog {
499
503
  empty.textContent = 'No icons found';
500
504
  this._grid.appendChild(empty);
501
505
  }
502
- empty.style.display = visibleCount > 0 ? 'none' : '';
506
+ /** @type {HTMLElement} */ (empty).style.display = visibleCount > 0 ? 'none' : '';
503
507
  }
504
508
 
505
509
  _selectIcon(name) {
506
510
  this._selectedIcon = name;
507
511
  // Highlight selected cell
508
512
  this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
509
- cell.classList.toggle('active', cell.dataset.name === name);
513
+ cell.classList.toggle('active', /** @type {HTMLElement} */ (cell).dataset.name === name);
510
514
  });
511
515
  // Enable insert button
512
516
  this._insertBtn.removeAttribute('disabled');
@@ -520,10 +524,10 @@ export class IconDialog {
520
524
  this._preview.innerHTML = '<span class="an-icon-preview-hint">Select an icon</span>';
521
525
  return;
522
526
  }
523
- const cls = (this._styleSelect && this._styleSelect.value) || 'fa-solid';
524
- const size = (this._sizeSelect && this._sizeSelect.value) || '1em';
525
- const useColor = this._useColorCb ? this._useColorCb.checked : false;
526
- const color = (useColor && this._colorInput) ? this._colorInput.value : '';
527
+ const cls = this._styleSelect?.value || 'fa-solid';
528
+ const size = this._sizeSelect?.value || '1em';
529
+ const useColor = this._useColorCb?.checked ?? false;
530
+ const color = useColor ? (this._colorInput?.value ?? '') : '';
527
531
  const styleAttr = [
528
532
  size ? `font-size:${size}` : '',
529
533
  color ? `color:${color}` : '',
@@ -543,10 +547,10 @@ export class IconDialog {
543
547
  _onInsert() {
544
548
  if (!this._selectedIcon) return;
545
549
 
546
- const cls = (this._styleSelect && this._styleSelect.value) || 'fa-solid';
547
- const size = (this._sizeSelect && this._sizeSelect.value) || '';
548
- const useColor = this._useColorCb ? this._useColorCb.checked : false;
549
- const color = (useColor && this._colorInput) ? this._colorInput.value : '';
550
+ const cls = this._styleSelect?.value || 'fa-solid';
551
+ const size = this._sizeSelect?.value || '';
552
+ const useColor = this._useColorCb?.checked ?? false;
553
+ const color = useColor ? (this._colorInput?.value ?? '') : '';
550
554
  const styleParts = [
551
555
  size ? `font-size:${size}` : '',
552
556
  color ? `color:${color}` : '',
@@ -566,8 +570,8 @@ export class IconDialog {
566
570
  if (savedRange) savedRange.select();
567
571
 
568
572
  // 2. Get the now-live range from the selection
569
- const sel = window.getSelection();
570
- let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
573
+ const sel = globalThis.getSelection();
574
+ let range = (sel?.rangeCount ?? 0) > 0 ? sel.getRangeAt(0) : null;
571
575
  if (!range) {
572
576
  range = document.createRange();
573
577
  range.selectNodeContents(editable);
@@ -581,8 +585,8 @@ export class IconDialog {
581
585
  // deleteContents() can drift the range endpoint outside the cell in some
582
586
  // browsers. Save the cell reference first and re-anchor after deletion.
583
587
  const _sc = range.startContainer;
584
- const _tdAnchor = (_sc.nodeType === 1 ? _sc : _sc.parentElement)
585
- ?.closest?.('td, th');
588
+ const _tdAnchor = /** @type {Element|null} */ (_sc.nodeType === 1 ? _sc : _sc.parentElement)
589
+ ?.closest('td, th');
586
590
  range.deleteContents();
587
591
  if (_tdAnchor && _tdAnchor.isConnected && !_tdAnchor.contains(range.startContainer)) {
588
592
  range.setStart(_tdAnchor, 0);
@@ -633,7 +637,7 @@ export class IconDialog {
633
637
  if (this._dialog) {
634
638
  this._dialog.style.display = 'flex';
635
639
  this._removeTrap = trapFocus(this._dialog, () => this._close());
636
- setTimeout(() => this._searchInput && this._searchInput.focus(), 50);
640
+ setTimeout(() => this._searchInput?.focus(), 50);
637
641
  }
638
642
  }
639
643
 
@@ -66,7 +66,7 @@ function drawCropToCanvas(img, naturalRect, renderW, renderH) {
66
66
  if (
67
67
  img.src.startsWith('data:') ||
68
68
  img.src.startsWith('blob:') ||
69
- img.src.startsWith(window.location.origin)
69
+ img.src.startsWith(globalThis.location.origin)
70
70
  ) {
71
71
  tryDraw(img);
72
72
  return;
@@ -230,7 +230,7 @@ export class ImageCropOverlay {
230
230
  on(h, 'touchstart', (e) => {
231
231
  e.preventDefault();
232
232
  e.stopPropagation();
233
- this._startHandleDrag({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY }, id);
233
+ const te = /** @type {TouchEvent} */ (e); this._startHandleDrag({ clientX: te.touches[0].clientX, clientY: te.touches[0].clientY }, id);
234
234
  }, { passive: false }),
235
235
  );
236
236
  this._handles[id] = h;
@@ -240,18 +240,18 @@ export class ImageCropOverlay {
240
240
  /* ---- crop move drag ---- */
241
241
  this._disposers.push(
242
242
  on(cropBox, 'mousedown', (e) => {
243
- if (e.target.classList.contains('an-crop-handle')) return;
243
+ if (/** @type {Element} */ (e.target).classList.contains('an-crop-handle')) return;
244
244
  if (e.target !== cropBox && e.target !== grid) return;
245
245
  e.preventDefault();
246
246
  e.stopPropagation();
247
247
  this._startBoxMove(e);
248
248
  }),
249
249
  on(cropBox, 'touchstart', (e) => {
250
- if (e.target.classList.contains('an-crop-handle')) return;
250
+ if (/** @type {Element} */ (e.target).classList.contains('an-crop-handle')) return;
251
251
  if (e.target !== cropBox && e.target !== grid) return;
252
252
  e.preventDefault();
253
253
  e.stopPropagation();
254
- this._startBoxMove({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY });
254
+ const te2 = /** @type {TouchEvent} */ (e); this._startBoxMove({ clientX: te2.touches[0].clientX, clientY: te2.touches[0].clientY });
255
255
  }, { passive: false }),
256
256
  );
257
257
 
@@ -340,8 +340,8 @@ export class ImageCropOverlay {
340
340
  this._cropBox.style.height = `${h}px`;
341
341
 
342
342
  // Scrim cutout via clip-path polygon (punches a transparent hole)
343
- const vw = window.innerWidth;
344
- const vh = window.innerHeight;
343
+ const vw = globalThis.innerWidth;
344
+ const vh = globalThis.innerHeight;
345
345
  // Outer rect → inner rectangle (counterclockwise) = hole
346
346
  this._scrim.style.clipPath = [
347
347
  `polygon(`,
@@ -367,7 +367,7 @@ export class ImageCropOverlay {
367
367
  // Position toolbar below crop box (or above if near bottom)
368
368
  const margin = 8;
369
369
  let tbTop = y + h + margin;
370
- if (tbTop + 40 > window.innerHeight - margin) tbTop = y - 40 - margin;
370
+ if (tbTop + 40 > globalThis.innerHeight - margin) tbTop = y - 40 - margin;
371
371
  this._toolbar.style.left = `${x}px`;
372
372
  this._toolbar.style.top = `${tbTop}px`;
373
373
  }
@@ -444,7 +444,7 @@ export class ImageCropOverlay {
444
444
  _attachDocDrag(onMove) {
445
445
  const onTouchMove = (e) => {
446
446
  e.preventDefault();
447
- onMove({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY });
447
+ const te3 = /** @type {TouchEvent} */ (e); onMove(/** @type {MouseEvent} */ (/** @type {unknown} */ ({ clientX: te3.touches[0].clientX, clientY: te3.touches[0].clientY })));
448
448
  };
449
449
  const cleanup = () => {
450
450
  document.removeEventListener('mousemove', onMove);
@@ -500,7 +500,7 @@ export class ImageCropOverlay {
500
500
  const outW = w;
501
501
  const outH = h;
502
502
 
503
- const naturalRect = { x: natX, y: natY, width: natW, height: natH };
503
+ const naturalRect = /** @type {DOMRect} */ (/** @type {unknown} */ ({ x: natX, y: natY, width: natW, height: natH }));
504
504
  const canvas = await drawCropToCanvas(img, naturalRect, outW, outH);
505
505
 
506
506
  if (!canvas) {
@@ -514,7 +514,7 @@ export class ImageCropOverlay {
514
514
  }
515
515
 
516
516
  // Determine output format: preserve JPEG, fall back to PNG
517
- const fmt = img.src.match(/^data:image\/(jpe?g)/i) ? 'image/jpeg' : 'image/png';
517
+ const fmt = /^data:image\/(jpe?g)/i.exec(img.src) ? 'image/jpeg' : 'image/png';
518
518
  const quality = fmt === 'image/jpeg' ? 0.92 : undefined;
519
519
  const newSrc = canvas.toDataURL(fmt, quality);
520
520
 
@@ -551,7 +551,7 @@ export class ImageCropOverlay {
551
551
  ].join(';');
552
552
  banner.textContent = msg;
553
553
  document.body.appendChild(banner);
554
- setTimeout(() => { if (banner.parentNode) banner.parentNode.removeChild(banner); }, 4000);
554
+ setTimeout(() => { banner.remove(); }, 4000);
555
555
  }
556
556
 
557
557
  /**
@@ -563,7 +563,7 @@ export class ImageCropOverlay {
563
563
  this._disposers = [];
564
564
 
565
565
  [this._scrim, this._cropBox, this._toolbar].forEach((el) => {
566
- if (el && el.parentNode) el.parentNode.removeChild(el);
566
+ el?.remove();
567
567
  });
568
568
 
569
569
  this._scrim = null;