autumnnote 1.4.2 → 1.6.0

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 (46) hide show
  1. package/README.md +102 -21
  2. package/dist/autumnnote.css +324 -2
  3. package/dist/autumnnote.es.js +700 -228
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +691 -235
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +8 -1
  8. package/src/js/Context.js +8 -3
  9. package/src/js/core/detectLang.js +98 -0
  10. package/src/js/core/dom.js +62 -6
  11. package/src/js/core/markdown.js +14 -13
  12. package/src/js/core/range.js +2 -2
  13. package/src/js/editing/History.js +6 -6
  14. package/src/js/editing/Style.js +19 -19
  15. package/src/js/editing/Table.js +4 -6
  16. package/src/js/editing/Typing.js +6 -6
  17. package/src/js/i18n/en.js +2 -0
  18. package/src/js/i18n/vi.js +2 -0
  19. package/src/js/index.js +4 -3
  20. package/src/js/module/BubbleToolbar.js +30 -13
  21. package/src/js/module/Buttons.js +16 -14
  22. package/src/js/module/Clipboard.js +5 -5
  23. package/src/js/module/CodeTooltip.js +42 -16
  24. package/src/js/module/Codeview.js +2 -2
  25. package/src/js/module/ContextMenu.js +12 -12
  26. package/src/js/module/Editor.js +64 -12
  27. package/src/js/module/EmojiDialog.js +19 -13
  28. package/src/js/module/FindReplace.js +85 -59
  29. package/src/js/module/Fullscreen.js +1 -1
  30. package/src/js/module/IconDialog.js +26 -20
  31. package/src/js/module/ImageCropOverlay.js +6 -6
  32. package/src/js/module/ImageDialog.js +18 -12
  33. package/src/js/module/ImageResizer.js +1 -1
  34. package/src/js/module/ImageTooltip.js +8 -4
  35. package/src/js/module/LinkDialog.js +18 -12
  36. package/src/js/module/LinkTooltip.js +5 -2
  37. package/src/js/module/Mention.js +3 -3
  38. package/src/js/module/Statusbar.js +2 -2
  39. package/src/js/module/TableTooltip.js +180 -18
  40. package/src/js/module/Toolbar.js +16 -15
  41. package/src/js/module/VideoDialog.js +8 -2
  42. package/src/js/module/VideoResizer.js +3 -3
  43. package/src/js/module/VideoTooltip.js +11 -6
  44. package/src/js/renderer.js +4 -2
  45. package/src/js/settings.js +53 -36
  46. package/src/styles/autumnnote.scss +332 -4
@@ -3,7 +3,7 @@
3
3
  * Click an emoji to insert it directly at the caret — no extra "Insert" step.
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
  // ---------------------------------------------------------------------------
@@ -566,19 +566,23 @@ export class EmojiDialog {
566
566
 
567
567
  // Title row
568
568
  const titleRow = createElement('div', { class: 'an-icon-title-row' });
569
+ const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
570
+ const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
571
+ 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"><circle cx="12" cy="12" r="10"/><path d="M8 13s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>`;
569
572
  const title = createElement('h3', { class: 'an-dialog-title' });
570
573
  title.textContent = L.title;
574
+ titleGroup.append(iconEl, title);
571
575
  const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
572
576
  closeBtn.innerHTML = '&times;';
573
- titleRow.append(title, closeBtn);
577
+ titleRow.append(titleGroup, closeBtn);
574
578
 
575
579
  // Search
576
- const searchInput = createElement('input', {
580
+ const searchInput = /** @type {HTMLInputElement} */ (createElement('input', {
577
581
  type: 'search',
578
582
  class: 'an-input an-icon-search',
579
583
  placeholder: L.searchPlaceholder,
580
584
  autocomplete: 'off',
581
- });
585
+ }));
582
586
  this._searchInput = searchInput;
583
587
 
584
588
  // Category tabs
@@ -617,6 +621,7 @@ export class EmojiDialog {
617
621
 
618
622
  box.append(titleRow, searchInput, catBar, grid, btnRow);
619
623
  overlay.appendChild(box);
624
+ makeDraggable(titleRow, box);
620
625
 
621
626
  // Events
622
627
  const d1 = on(closeBtn, 'click', () => this._close());
@@ -624,7 +629,7 @@ export class EmojiDialog {
624
629
  const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
625
630
  const d4 = on(searchInput, 'input', () => this._filterEmojis(searchInput.value, this._activeCat));
626
631
  const d5 = on(catBar, 'click', (e) => {
627
- const tab = e.target.closest('[data-cat]');
632
+ const tab = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-cat]'));
628
633
  if (tab) {
629
634
  this._activeCat = tab.dataset.cat;
630
635
  this._updateCatTabs();
@@ -632,7 +637,7 @@ export class EmojiDialog {
632
637
  }
633
638
  });
634
639
  const d6 = on(grid, 'click', (e) => {
635
- const cell = e.target.closest('.an-emoji-cell');
640
+ const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-emoji-cell'));
636
641
  if (cell) this._onEmojiClick(cell.dataset.char);
637
642
  });
638
643
  this._disposers.push(d1, d2, d3, d4, d5, d6);
@@ -645,7 +650,7 @@ export class EmojiDialog {
645
650
 
646
651
  _updateCatTabs() {
647
652
  this._catBar.querySelectorAll('.an-icon-cat').forEach((tab) => {
648
- tab.classList.toggle('active', tab.dataset.cat === this._activeCat);
653
+ tab.classList.toggle('active', /** @type {HTMLElement} */ (tab).dataset.cat === this._activeCat);
649
654
  });
650
655
  }
651
656
 
@@ -653,10 +658,11 @@ export class EmojiDialog {
653
658
  const q = (query || '').trim().toLowerCase();
654
659
  let count = 0;
655
660
  this._grid.querySelectorAll('.an-emoji-cell').forEach((cell) => {
656
- const matchCat = !cat || cat === 'all' || cell.dataset.cat === cat;
657
- const matchQuery = !q || cell.dataset.keywords.includes(q) || cell.dataset.char === q;
661
+ const hCell = /** @type {HTMLElement} */ (cell);
662
+ const matchCat = !cat || cat === 'all' || hCell.dataset.cat === cat;
663
+ const matchQuery = !q || hCell.dataset.keywords.includes(q) || hCell.dataset.char === q;
658
664
  const visible = matchCat && matchQuery;
659
- cell.style.display = visible ? '' : 'none';
665
+ hCell.style.display = visible ? '' : 'none';
660
666
  if (visible) count++;
661
667
  });
662
668
  let empty = this._grid.querySelector('.an-icon-empty');
@@ -665,7 +671,7 @@ export class EmojiDialog {
665
671
  empty.textContent = 'No emojis found';
666
672
  this._grid.appendChild(empty);
667
673
  }
668
- empty.style.display = count > 0 ? 'none' : '';
674
+ /** @type {HTMLElement} */ (empty).style.display = count > 0 ? 'none' : '';
669
675
  }
670
676
 
671
677
  // ---------------------------------------------------------------------------
@@ -694,8 +700,8 @@ export class EmojiDialog {
694
700
  // deleteContents() can drift the range endpoint outside the cell in some
695
701
  // browsers. Save the cell reference first and re-anchor after deletion.
696
702
  const _sc = range.startContainer;
697
- const _tdAnchor = (_sc.nodeType === 1 ? _sc : _sc.parentElement)
698
- ?.closest?.('td, th');
703
+ const _tdAnchor = /** @type {Element|null} */ (_sc.nodeType === 1 ? _sc : _sc.parentElement)
704
+ ?.closest('td, th');
699
705
  range.deleteContents();
700
706
  if (_tdAnchor && _tdAnchor.isConnected && !_tdAnchor.contains(range.startContainer)) {
701
707
  range.setStart(_tdAnchor, 0);
@@ -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 */
@@ -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
  }
@@ -331,7 +357,7 @@ export class FindReplace {
331
357
  re.lastIndex = 0;
332
358
  let m;
333
359
  while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) {
334
- results.push({ node, start: m.index, end: m.index + m[0].length });
360
+ results.push({ node: /** @type {Text} */ (node), start: m.index, end: m.index + m[0].length });
335
361
  }
336
362
  }
337
363
  return results;
@@ -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
 
@@ -344,19 +344,23 @@ export class IconDialog {
344
344
 
345
345
  // Title row
346
346
  const titleRow = createElement('div', { class: 'an-icon-title-row' });
347
+ const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
348
+ const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
349
+ 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
350
  const title = createElement('h3', { class: 'an-dialog-title' });
348
351
  title.textContent = L.title;
352
+ titleGroup.append(iconEl, title);
349
353
  const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
350
354
  closeBtn.innerHTML = '&times;';
351
- titleRow.append(title, closeBtn);
355
+ titleRow.append(titleGroup, closeBtn);
352
356
 
353
357
  // Search
354
- const searchInput = createElement('input', {
358
+ const searchInput = /** @type {HTMLInputElement} */ (createElement('input', {
355
359
  type: 'search',
356
360
  class: 'an-input an-icon-search',
357
361
  placeholder: L.searchPlaceholder,
358
362
  autocomplete: 'off',
359
- });
363
+ }));
360
364
  this._searchInput = searchInput;
361
365
 
362
366
  // Category tabs
@@ -388,7 +392,7 @@ export class IconDialog {
388
392
 
389
393
  const styleLabel = createElement('label', { class: 'an-label' });
390
394
  styleLabel.textContent = L.style;
391
- const styleSelect = createElement('select', { class: 'an-input an-icon-option-select' });
395
+ const styleSelect = /** @type {HTMLSelectElement} */ (createElement('select', { class: 'an-input an-icon-option-select' }));
392
396
  [['fa-solid', 'Solid'], ['fa-regular', 'Regular'], ['fa-light', 'Light (Pro)']].forEach(([v, t]) => {
393
397
  const opt = createElement('option', { value: v });
394
398
  opt.textContent = t;
@@ -399,9 +403,9 @@ export class IconDialog {
399
403
 
400
404
  const sizeLabel = createElement('label', { class: 'an-label' });
401
405
  sizeLabel.textContent = L.size;
402
- const sizeSelect = createElement('select', { class: 'an-input an-icon-option-select' });
406
+ const sizeSelect = /** @type {HTMLSelectElement} */ (createElement('select', { class: 'an-input an-icon-option-select' }));
403
407
  [['', '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 });
408
+ const opt = /** @type {HTMLOptionElement} */ (createElement('option', { value: v }));
405
409
  if (v === '1em') opt.selected = true;
406
410
  opt.textContent = t;
407
411
  sizeSelect.appendChild(opt);
@@ -410,11 +414,11 @@ export class IconDialog {
410
414
 
411
415
  const colorLabel = createElement('label', { class: 'an-label' });
412
416
  colorLabel.textContent = L.color;
413
- const colorInput = createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' });
417
+ const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' }));
414
418
  this._colorInput = colorInput;
415
419
 
416
420
  const useColorLabel = createElement('label', { class: 'an-label an-label-inline an-icon-use-color' });
417
- const useColorCb = createElement('input', { type: 'checkbox', checked: '' });
421
+ const useColorCb = /** @type {HTMLInputElement} */ (createElement('input', { type: 'checkbox', checked: '' }));
418
422
  this._useColorCb = useColorCb;
419
423
  useColorLabel.append(useColorCb, document.createTextNode(L.useColor));
420
424
 
@@ -438,6 +442,7 @@ export class IconDialog {
438
442
 
439
443
  box.append(titleRow, searchInput, catBar, grid, optRow, preview, btnRow);
440
444
  overlay.appendChild(box);
445
+ makeDraggable(titleRow, box);
441
446
 
442
447
  // -------------------------------------------------------------------------
443
448
  // Events
@@ -449,7 +454,7 @@ export class IconDialog {
449
454
  const d4 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
450
455
  const d5 = on(searchInput, 'input', () => this._filterIcons(searchInput.value, this._activeCat));
451
456
  const d6 = on(catBar, 'click', (e) => {
452
- const tab = e.target.closest('[data-cat]');
457
+ const tab = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-cat]'));
453
458
  if (tab) {
454
459
  this._activeCat = tab.dataset.cat;
455
460
  this._updateCatTabs();
@@ -457,7 +462,7 @@ export class IconDialog {
457
462
  }
458
463
  });
459
464
  const d7 = on(grid, 'click', (e) => {
460
- const cell = e.target.closest('.an-icon-cell');
465
+ const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-icon-cell'));
461
466
  if (cell) this._selectIcon(cell.dataset.name);
462
467
  });
463
468
  const d8 = on(styleSelect, 'change', () => this._updatePreview(this._selectedIcon));
@@ -476,7 +481,7 @@ export class IconDialog {
476
481
 
477
482
  _updateCatTabs() {
478
483
  this._catBar.querySelectorAll('.an-icon-cat').forEach((tab) => {
479
- tab.classList.toggle('active', tab.dataset.cat === this._activeCat);
484
+ tab.classList.toggle('active', /** @type {HTMLElement} */ (tab).dataset.cat === this._activeCat);
480
485
  });
481
486
  }
482
487
 
@@ -484,12 +489,13 @@ export class IconDialog {
484
489
  const q = (query || '').trim().toLowerCase();
485
490
  let visibleCount = 0;
486
491
  this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
487
- const name = cell.dataset.name;
488
- const cellCat = cell.dataset.cat;
492
+ const hCell = /** @type {HTMLElement} */ (cell);
493
+ const name = hCell.dataset.name;
494
+ const cellCat = hCell.dataset.cat;
489
495
  const matchesCat = !cat || cat === 'all' || cellCat === cat;
490
496
  const matchesQuery = !q || name.includes(q);
491
497
  const visible = matchesCat && matchesQuery;
492
- cell.style.display = visible ? '' : 'none';
498
+ hCell.style.display = visible ? '' : 'none';
493
499
  if (visible) visibleCount++;
494
500
  });
495
501
  // Show empty state if needed
@@ -499,14 +505,14 @@ export class IconDialog {
499
505
  empty.textContent = 'No icons found';
500
506
  this._grid.appendChild(empty);
501
507
  }
502
- empty.style.display = visibleCount > 0 ? 'none' : '';
508
+ /** @type {HTMLElement} */ (empty).style.display = visibleCount > 0 ? 'none' : '';
503
509
  }
504
510
 
505
511
  _selectIcon(name) {
506
512
  this._selectedIcon = name;
507
513
  // Highlight selected cell
508
514
  this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
509
- cell.classList.toggle('active', cell.dataset.name === name);
515
+ cell.classList.toggle('active', /** @type {HTMLElement} */ (cell).dataset.name === name);
510
516
  });
511
517
  // Enable insert button
512
518
  this._insertBtn.removeAttribute('disabled');
@@ -581,8 +587,8 @@ export class IconDialog {
581
587
  // deleteContents() can drift the range endpoint outside the cell in some
582
588
  // browsers. Save the cell reference first and re-anchor after deletion.
583
589
  const _sc = range.startContainer;
584
- const _tdAnchor = (_sc.nodeType === 1 ? _sc : _sc.parentElement)
585
- ?.closest?.('td, th');
590
+ const _tdAnchor = /** @type {Element|null} */ (_sc.nodeType === 1 ? _sc : _sc.parentElement)
591
+ ?.closest('td, th');
586
592
  range.deleteContents();
587
593
  if (_tdAnchor && _tdAnchor.isConnected && !_tdAnchor.contains(range.startContainer)) {
588
594
  range.setStart(_tdAnchor, 0);
@@ -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
 
@@ -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) {
@@ -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 {
@@ -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();
@@ -51,7 +51,7 @@ 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)),