autumnnote 2.0.0 → 2.2.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 (64) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.css +1 -1
  4. package/dist/autumnnote.es.js +661 -798
  5. package/dist/autumnnote.es.js.map +1 -1
  6. package/dist/autumnnote.min.js +20 -20
  7. package/dist/autumnnote.umd.js +20 -20
  8. package/dist/autumnnote.umd.js.map +1 -1
  9. package/dist/icon-data-V0Xqv-wX.js +255 -0
  10. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  11. package/package.json +13 -4
  12. package/types/index.d.ts +8 -0
  13. package/src/js/Context.js +0 -854
  14. package/src/js/core/detectLang.js +0 -98
  15. package/src/js/core/dom.js +0 -372
  16. package/src/js/core/env.js +0 -25
  17. package/src/js/core/key.js +0 -66
  18. package/src/js/core/lists.js +0 -121
  19. package/src/js/core/markdown.js +0 -695
  20. package/src/js/core/range.js +0 -194
  21. package/src/js/core/sanitise.js +0 -231
  22. package/src/js/editing/History.js +0 -266
  23. package/src/js/editing/Style.js +0 -812
  24. package/src/js/editing/Table.js +0 -105
  25. package/src/js/editing/Typing.js +0 -397
  26. package/src/js/index.js +0 -193
  27. package/src/js/index.umd.js +0 -17
  28. package/src/js/module/AutoSaveRestore.js +0 -125
  29. package/src/js/module/BaseDialog.js +0 -133
  30. package/src/js/module/BaseMediaTooltip.js +0 -142
  31. package/src/js/module/BaseResizer.js +0 -312
  32. package/src/js/module/BubbleToolbar.js +0 -483
  33. package/src/js/module/Buttons.js +0 -399
  34. package/src/js/module/Clipboard.js +0 -579
  35. package/src/js/module/CodeTooltip.js +0 -493
  36. package/src/js/module/Codeview.js +0 -125
  37. package/src/js/module/ContextMenu.js +0 -621
  38. package/src/js/module/Editor.js +0 -747
  39. package/src/js/module/EmojiDialog.js +0 -254
  40. package/src/js/module/FindReplace.js +0 -512
  41. package/src/js/module/Fullscreen.js +0 -80
  42. package/src/js/module/IconDialog.js +0 -618
  43. package/src/js/module/ImageCropOverlay.js +0 -586
  44. package/src/js/module/ImageDialog.js +0 -193
  45. package/src/js/module/ImageResizer.js +0 -42
  46. package/src/js/module/ImageTooltip.js +0 -285
  47. package/src/js/module/LinkDialog.js +0 -145
  48. package/src/js/module/LinkTooltip.js +0 -250
  49. package/src/js/module/MarkdownShortcuts.js +0 -250
  50. package/src/js/module/Mention.js +0 -365
  51. package/src/js/module/Placeholder.js +0 -51
  52. package/src/js/module/ShortcutsDialog.js +0 -111
  53. package/src/js/module/SlashMenu.js +0 -376
  54. package/src/js/module/Statusbar.js +0 -246
  55. package/src/js/module/TableTooltip.js +0 -1521
  56. package/src/js/module/Toolbar.js +0 -750
  57. package/src/js/module/VideoDialog.js +0 -193
  58. package/src/js/module/VideoResizer.js +0 -66
  59. package/src/js/module/VideoTooltip.js +0 -248
  60. package/src/js/module/emoji-data.js +0 -496
  61. package/src/js/renderer.js +0 -120
  62. package/src/js/settings.js +0 -214
  63. package/src/styles/_variables.scss +0 -48
  64. package/src/styles/autumnnote.scss +0 -2866
@@ -1,618 +0,0 @@
1
- /**
2
- * IconDialog.js - Browse and insert FontAwesome Free icons
3
- */
4
-
5
- import { createElement, on, makeDraggable } from '../core/dom.js';
6
- import { BaseDialog } from './BaseDialog.js';
7
-
8
- // ---------------------------------------------------------------------------
9
- // Icon catalogue — FA 6 Free Solid slug names, grouped by category
10
- // ---------------------------------------------------------------------------
11
-
12
- const ICON_CATEGORIES = [
13
- { id: 'popular', label: 'Popular' },
14
- { id: 'interface', label: 'Interface' },
15
- { id: 'navigation', label: 'Navigation' },
16
- { id: 'media', label: 'Media' },
17
- { id: 'communication', label: 'Communication' },
18
- { id: 'files', label: 'Files' },
19
- { id: 'people', label: 'People' },
20
- { id: 'objects', label: 'Objects' },
21
- ];
22
-
23
- // Each entry: [slug, category]
24
- const ICON_LIST = [
25
- // Popular
26
- ['house', 'popular'],
27
- ['star', 'popular'],
28
- ['heart', 'popular'],
29
- ['check', 'popular'],
30
- ['xmark', 'popular'],
31
- ['plus', 'popular'],
32
- ['minus', 'popular'],
33
- ['magnifying-glass', 'popular'],
34
- ['gear', 'popular'],
35
- ['bell', 'popular'],
36
- ['user', 'popular'],
37
- ['envelope', 'popular'],
38
- ['phone', 'popular'],
39
- ['calendar', 'popular'],
40
- ['clock', 'popular'],
41
- ['lock', 'popular'],
42
- ['eye', 'popular'],
43
- ['eye-slash', 'popular'],
44
- ['trash', 'popular'],
45
- ['pen', 'popular'],
46
- ['bookmark', 'popular'],
47
- ['flag', 'popular'],
48
- ['thumbs-up', 'popular'],
49
- ['thumbs-down', 'popular'],
50
- ['circle-info', 'popular'],
51
- ['triangle-exclamation', 'popular'],
52
- ['circle-check', 'popular'],
53
- ['circle-xmark', 'popular'],
54
- ['share', 'popular'],
55
- ['download', 'popular'],
56
- ['upload', 'popular'],
57
- ['tag', 'popular'],
58
- // Interface
59
- ['bars', 'interface'],
60
- ['ellipsis', 'interface'],
61
- ['ellipsis-vertical', 'interface'],
62
- ['list', 'interface'],
63
- ['table-cells', 'interface'],
64
- ['table-list', 'interface'],
65
- ['grip', 'interface'],
66
- ['filter', 'interface'],
67
- ['sort', 'interface'],
68
- ['arrows-rotate', 'interface'],
69
- ['rotate', 'interface'],
70
- ['rotate-left', 'interface'],
71
- ['rotate-right', 'interface'],
72
- ['copy', 'interface'],
73
- ['floppy-disk', 'interface'],
74
- ['print', 'interface'],
75
- ['link', 'interface'],
76
- ['code', 'interface'],
77
- ['expand', 'interface'],
78
- ['compress', 'interface'],
79
- ['cloud', 'interface'],
80
- ['lock-open', 'interface'],
81
- ['key', 'interface'],
82
- ['shield', 'interface'],
83
- ['shield-halved', 'interface'],
84
- ['sliders', 'interface'],
85
- ['toggle-on', 'interface'],
86
- ['square-check', 'interface'],
87
- ['circle-plus', 'interface'],
88
- ['circle-minus', 'interface'],
89
- // Navigation
90
- ['arrow-right', 'navigation'],
91
- ['arrow-left', 'navigation'],
92
- ['arrow-up', 'navigation'],
93
- ['arrow-down', 'navigation'],
94
- ['chevron-right', 'navigation'],
95
- ['chevron-left', 'navigation'],
96
- ['chevron-up', 'navigation'],
97
- ['chevron-down', 'navigation'],
98
- ['angle-right', 'navigation'],
99
- ['angle-left', 'navigation'],
100
- ['angle-up', 'navigation'],
101
- ['angle-down', 'navigation'],
102
- ['angles-right', 'navigation'],
103
- ['angles-left', 'navigation'],
104
- ['location-dot', 'navigation'],
105
- ['compass', 'navigation'],
106
- ['map', 'navigation'],
107
- ['map-pin', 'navigation'],
108
- ['route', 'navigation'],
109
- ['circle-arrow-right', 'navigation'],
110
- ['circle-arrow-left', 'navigation'],
111
- ['arrow-trend-up', 'navigation'],
112
- ['arrow-trend-down', 'navigation'],
113
- ['right-from-bracket', 'navigation'],
114
- ['right-to-bracket', 'navigation'],
115
- // Media
116
- ['play', 'media'],
117
- ['pause', 'media'],
118
- ['stop', 'media'],
119
- ['forward', 'media'],
120
- ['backward', 'media'],
121
- ['forward-step', 'media'],
122
- ['backward-step', 'media'],
123
- ['volume-high', 'media'],
124
- ['volume-low', 'media'],
125
- ['volume-xmark', 'media'],
126
- ['volume-off', 'media'],
127
- ['music', 'media'],
128
- ['headphones', 'media'],
129
- ['microphone', 'media'],
130
- ['microphone-slash', 'media'],
131
- ['film', 'media'],
132
- ['camera', 'media'],
133
- ['camera-rotate', 'media'],
134
- ['video', 'media'],
135
- ['image', 'media'],
136
- ['images', 'media'],
137
- ['photo-film', 'media'],
138
- ['podcast', 'media'],
139
- ['radio', 'media'],
140
- // Communication
141
- ['comment', 'communication'],
142
- ['comments', 'communication'],
143
- ['comment-dots', 'communication'],
144
- ['message', 'communication'],
145
- ['paper-plane', 'communication'],
146
- ['reply', 'communication'],
147
- ['reply-all', 'communication'],
148
- ['at', 'communication'],
149
- ['hashtag', 'communication'],
150
- ['wifi', 'communication'],
151
- ['signal', 'communication'],
152
- ['rss', 'communication'],
153
- ['share-nodes', 'communication'],
154
- ['inbox', 'communication'],
155
- ['phone-volume', 'communication'],
156
- ['mobile', 'communication'],
157
- ['mobile-screen', 'communication'],
158
- ['laptop', 'communication'],
159
- ['desktop', 'communication'],
160
- ['tower-broadcast', 'communication'],
161
- // Files
162
- ['file', 'files'],
163
- ['file-lines', 'files'],
164
- ['folder', 'files'],
165
- ['folder-open', 'files'],
166
- ['folder-plus', 'files'],
167
- ['folder-minus', 'files'],
168
- ['file-image', 'files'],
169
- ['file-pdf', 'files'],
170
- ['file-code', 'files'],
171
- ['file-zipper', 'files'],
172
- ['file-audio', 'files'],
173
- ['file-video', 'files'],
174
- ['file-arrow-up', 'files'],
175
- ['file-arrow-down', 'files'],
176
- ['database', 'files'],
177
- ['box', 'files'],
178
- ['box-open', 'files'],
179
- ['hard-drive', 'files'],
180
- ['server', 'files'],
181
- // People
182
- ['user', 'people'],
183
- ['users', 'people'],
184
- ['user-group', 'people'],
185
- ['user-plus', 'people'],
186
- ['user-minus', 'people'],
187
- ['user-check', 'people'],
188
- ['user-xmark', 'people'],
189
- ['user-tie', 'people'],
190
- ['user-shield', 'people'],
191
- ['user-secret', 'people'],
192
- ['person', 'people'],
193
- ['person-running', 'people'],
194
- ['person-walking', 'people'],
195
- ['child', 'people'],
196
- ['handshake', 'people'],
197
- ['hand', 'people'],
198
- ['hands-holding', 'people'],
199
- ['people-group', 'people'],
200
- // Objects
201
- ['pencil', 'objects'],
202
- ['paintbrush', 'objects'],
203
- ['eraser', 'objects'],
204
- ['scissors', 'objects'],
205
- ['wrench', 'objects'],
206
- ['screwdriver', 'objects'],
207
- ['hammer', 'objects'],
208
- ['toolbox', 'objects'],
209
- ['graduation-cap', 'objects'],
210
- ['book', 'objects'],
211
- ['book-open', 'objects'],
212
- ['glasses', 'objects'],
213
- ['microscope', 'objects'],
214
- ['flask', 'objects'],
215
- ['stethoscope', 'objects'],
216
- ['hospital', 'objects'],
217
- ['building', 'objects'],
218
- ['city', 'objects'],
219
- ['car', 'objects'],
220
- ['truck', 'objects'],
221
- ['plane', 'objects'],
222
- ['train', 'objects'],
223
- ['bicycle', 'objects'],
224
- ['bus', 'objects'],
225
- ['rocket', 'objects'],
226
- ['briefcase', 'objects'],
227
- ['cart-shopping', 'objects'],
228
- ['bag-shopping', 'objects'],
229
- ['credit-card', 'objects'],
230
- ['money-bill', 'objects'],
231
- ['chart-bar', 'objects'],
232
- ['chart-line', 'objects'],
233
- ['chart-pie', 'objects'],
234
- ['bolt', 'objects'],
235
- ['sun', 'objects'],
236
- ['moon', 'objects'],
237
- ['snowflake', 'objects'],
238
- ['fire', 'objects'],
239
- ['droplet', 'objects'],
240
- ['leaf', 'objects'],
241
- ['tree', 'objects'],
242
- ['earth-americas', 'objects'],
243
- ['globe', 'objects'],
244
- ['award', 'objects'],
245
- ['trophy', 'objects'],
246
- ['gift', 'objects'],
247
- ['puzzle-piece', 'objects'],
248
- ];
249
-
250
- export class IconDialog extends BaseDialog {
251
- _selectedIcon = null;
252
- _activeCat = 'all';
253
-
254
- // ---------------------------------------------------------------------------
255
- // Lifecycle
256
- // ---------------------------------------------------------------------------
257
-
258
- initialize() {
259
- this._ensureFontAwesome();
260
- // Grid is built lazily in show() to avoid ~250 icon cells at load time.
261
- return this;
262
- }
263
-
264
- /**
265
- * Injects FA 6 Free CSS from CDN if it is not already on the page.
266
- * This guarantees icon glyphs render in the grid, preview, and in the
267
- * editor content after insertion — regardless of whether the host page
268
- * loaded FA itself.
269
- */
270
- _ensureFontAwesome() {
271
- // Already present (icon element or stylesheet link)?
272
- if (document.getElementById('an-fontawesome-css')) return;
273
- const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
274
- .map((l) => /** @type {HTMLLinkElement} */ (l).href || '').join(' ');
275
- if (/fontawesome|font-awesome/.test(links)) return;
276
- if (document.querySelector('.fa-solid, .fas, .far, .fab')) return;
277
-
278
- const link = document.createElement('link');
279
- link.id = 'an-fontawesome-css';
280
- link.rel = 'stylesheet';
281
- link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css';
282
- link.crossOrigin = 'anonymous';
283
- link.referrerPolicy = 'no-referrer';
284
- document.head.appendChild(link);
285
- }
286
-
287
- // ---------------------------------------------------------------------------
288
- // Public API
289
- // ---------------------------------------------------------------------------
290
-
291
- show() {
292
- if (!this._dialog) {
293
- this._dialog = this._buildDialog();
294
- document.body.appendChild(this._dialog);
295
- }
296
- this._saveRange();
297
- this._selectedIcon = null;
298
- this._activeCat = 'all';
299
- this._searchInput.value = '';
300
- // Clear previously selected icon highlight and disable Insert button
301
- this._grid.querySelectorAll('.an-icon-cell.active').forEach((c) => c.classList.remove('active'));
302
- this._insertBtn.setAttribute('disabled', '');
303
- this._updateCatTabs();
304
- this._filterIcons('', 'all');
305
- this._updatePreview(null);
306
- this._open();
307
- }
308
-
309
- // ---------------------------------------------------------------------------
310
- // Build dialog
311
- // ---------------------------------------------------------------------------
312
-
313
- _buildDialog() {
314
- const L = this.context.locale.iconDialog;
315
- const overlay = createElement('div', {
316
- class: 'an-dialog-overlay',
317
- role: 'dialog',
318
- 'aria-modal': 'true',
319
- 'aria-label': L.ariaLabel,
320
- });
321
-
322
- const box = createElement('div', { class: 'an-dialog-box an-icon-box' });
323
-
324
- // Title row
325
- const titleRow = createElement('div', { class: 'an-icon-title-row' });
326
- const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
327
- const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
328
- 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>`;
329
- const title = createElement('h3', { class: 'an-dialog-title' });
330
- title.textContent = L.title;
331
- titleGroup.append(iconEl, title);
332
- const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
333
- closeBtn.innerHTML = '&times;';
334
- titleRow.append(titleGroup, closeBtn);
335
-
336
- // Search
337
- const searchInput = /** @type {HTMLInputElement} */ (createElement('input', {
338
- type: 'search',
339
- class: 'an-input an-icon-search',
340
- placeholder: L.searchPlaceholder,
341
- autocomplete: 'off',
342
- }));
343
- this._searchInput = searchInput;
344
- this._firstInput = searchInput;
345
-
346
- // Category tabs
347
- const catBar = createElement('div', { class: 'an-icon-cats' });
348
- const allTab = createElement('button', { type: 'button', class: 'an-icon-cat active', 'data-cat': 'all' });
349
- allTab.textContent = L.all;
350
- catBar.appendChild(allTab);
351
- ICON_CATEGORIES.forEach(({ id, label }) => {
352
- const tab = createElement('button', { type: 'button', class: 'an-icon-cat', 'data-cat': id });
353
- tab.textContent = L.categories?.[id] || label;
354
- catBar.appendChild(tab);
355
- });
356
- this._catBar = catBar;
357
-
358
- // Icon grid
359
- const grid = createElement('div', { class: 'an-icon-grid' });
360
- ICON_LIST.forEach(([name, cat]) => {
361
- const cell = createElement('button', { type: 'button', class: 'an-icon-cell', 'data-name': name, 'data-cat': cat, title: name });
362
- const icon = createElement('i', { class: 'fa-solid fa-' + name, 'aria-hidden': 'true' });
363
- const label = createElement('span');
364
- label.textContent = name;
365
- cell.append(icon, label);
366
- grid.appendChild(cell);
367
- });
368
- this._grid = grid;
369
-
370
- // Options row: style + size + color
371
- const optRow = createElement('div', { class: 'an-icon-options' });
372
-
373
- const styleLabel = createElement('label', { class: 'an-label' });
374
- styleLabel.textContent = L.style;
375
- const styleSelect = /** @type {HTMLSelectElement} */ (createElement('select', { class: 'an-input an-icon-option-select' }));
376
- [['fa-solid', 'Solid'], ['fa-regular', 'Regular'], ['fa-light', 'Light (Pro)']].forEach(([v, t]) => {
377
- const opt = createElement('option', { value: v });
378
- opt.textContent = t;
379
- styleSelect.appendChild(opt);
380
- });
381
- styleSelect.value = 'fa-solid';
382
- this._styleSelect = styleSelect;
383
-
384
- const sizeLabel = createElement('label', { class: 'an-label' });
385
- sizeLabel.textContent = L.size;
386
- const sizeSelect = /** @type {HTMLSelectElement} */ (createElement('select', { class: 'an-input an-icon-option-select' }));
387
- [['', 'Inherit'], ['0.75em', '0.75em'], ['1em', '1em'], ['1.25em', '1.25em'], ['1.5em', '1.5em'], ['2em', '2em'], ['3em', '3em']].forEach(([v, t]) => {
388
- const opt = /** @type {HTMLOptionElement} */ (createElement('option', { value: v }));
389
- if (v === '1em') opt.selected = true;
390
- opt.textContent = t;
391
- sizeSelect.appendChild(opt);
392
- });
393
- this._sizeSelect = sizeSelect;
394
-
395
- const colorLabel = createElement('label', { class: 'an-label' });
396
- colorLabel.textContent = L.color;
397
- const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' }));
398
- this._colorInput = colorInput;
399
-
400
- const useColorLabel = createElement('label', { class: 'an-label an-label-inline an-icon-use-color' });
401
- const useColorCb = /** @type {HTMLInputElement} */ (createElement('input', { type: 'checkbox', checked: '' }));
402
- this._useColorCb = useColorCb;
403
- useColorLabel.append(useColorCb, document.createTextNode(L.useColor));
404
-
405
- optRow.append(styleLabel, styleSelect, sizeLabel, sizeSelect, colorLabel, colorInput, useColorLabel);
406
-
407
- // Preview
408
- const preview = createElement('div', { class: 'an-icon-preview' });
409
- const previewHint = createElement('span', { class: 'an-icon-preview-hint' });
410
- previewHint.textContent = L.selectHint;
411
- preview.appendChild(previewHint);
412
- this._preview = preview;
413
-
414
- // Actions
415
- const btnRow = createElement('div', { class: 'an-dialog-actions' });
416
- const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary', disabled: '' });
417
- insertBtn.textContent = L.insertBtn;
418
- const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
419
- cancelBtn.textContent = L.cancelBtn;
420
- btnRow.append(insertBtn, cancelBtn);
421
- this._insertBtn = insertBtn;
422
-
423
- box.append(titleRow, searchInput, catBar, grid, optRow, preview, btnRow);
424
- overlay.appendChild(box);
425
- makeDraggable(titleRow, box);
426
-
427
- // -------------------------------------------------------------------------
428
- // Events
429
- // -------------------------------------------------------------------------
430
-
431
- const d1 = on(closeBtn, 'click', () => this._close());
432
- const d2 = on(cancelBtn, 'click', () => this._close());
433
- const d3 = on(insertBtn, 'click', () => this._onInsert());
434
- const d4 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
435
- const d5 = on(searchInput, 'input', () => this._filterIcons(searchInput.value, this._activeCat));
436
- const d6 = on(catBar, 'click', (e) => {
437
- const tab = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-cat]'));
438
- if (tab) {
439
- this._activeCat = tab.dataset.cat;
440
- this._updateCatTabs();
441
- this._filterIcons(this._searchInput.value, this._activeCat);
442
- }
443
- });
444
- const d7 = on(grid, 'click', (e) => {
445
- const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-icon-cell'));
446
- if (cell) this._selectIcon(cell.dataset.name);
447
- });
448
- const d8 = on(styleSelect, 'change', () => this._updatePreview(this._selectedIcon));
449
- const d9 = on(sizeSelect, 'change', () => this._updatePreview(this._selectedIcon));
450
- const d10 = on(colorInput, 'input', () => this._updatePreview(this._selectedIcon));
451
- const d11 = on(useColorCb, 'change', () => this._updatePreview(this._selectedIcon));
452
-
453
- this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11);
454
-
455
- return overlay;
456
- }
457
-
458
- // ---------------------------------------------------------------------------
459
- // Filter / select
460
- // ---------------------------------------------------------------------------
461
-
462
- _updateCatTabs() {
463
- this._catBar.querySelectorAll('.an-icon-cat').forEach((tab) => {
464
- tab.classList.toggle('active', /** @type {HTMLElement} */ (tab).dataset.cat === this._activeCat);
465
- });
466
- }
467
-
468
- _filterIcons(query, cat) {
469
- const q = (query || '').trim().toLowerCase();
470
- let visibleCount = 0;
471
- this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
472
- const hCell = /** @type {HTMLElement} */ (cell);
473
- const name = hCell.dataset.name;
474
- const cellCat = hCell.dataset.cat;
475
- const matchesCat = !cat || cat === 'all' || cellCat === cat;
476
- const matchesQuery = !q || name.includes(q);
477
- const visible = matchesCat && matchesQuery;
478
- hCell.style.display = visible ? '' : 'none';
479
- if (visible) visibleCount++;
480
- });
481
- // Show empty state if needed
482
- let empty = this._grid.querySelector('.an-icon-empty');
483
- if (!empty) {
484
- empty = createElement('div', { class: 'an-icon-empty' });
485
- empty.textContent = 'No icons found';
486
- this._grid.appendChild(empty);
487
- }
488
- /** @type {HTMLElement} */ (empty).style.display = visibleCount > 0 ? 'none' : '';
489
- }
490
-
491
- _selectIcon(name) {
492
- this._selectedIcon = name;
493
- // Highlight selected cell
494
- this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
495
- cell.classList.toggle('active', /** @type {HTMLElement} */ (cell).dataset.name === name);
496
- });
497
- // Enable insert button
498
- this._insertBtn.removeAttribute('disabled');
499
- // Update preview
500
- this._updatePreview(name);
501
- }
502
-
503
- _updatePreview(name) {
504
- if (!this._preview) return;
505
- if (!name) {
506
- this._preview.innerHTML = '<span class="an-icon-preview-hint">Select an icon</span>';
507
- return;
508
- }
509
- const cls = this._styleSelect?.value || 'fa-solid';
510
- const size = this._sizeSelect?.value || '1em';
511
- const useColor = this._useColorCb?.checked ?? false;
512
- const color = useColor ? (this._colorInput?.value ?? '') : '';
513
- const styleAttr = [
514
- size ? `font-size:${size}` : '',
515
- color ? `color:${color}` : '',
516
- ].filter(Boolean).join(';');
517
- // Use innerHTML — atomic, no partial-update risk
518
- this._preview.innerHTML =
519
- `<i class="${cls} fa-${name}" aria-hidden="true"${
520
- styleAttr ? ` style="${styleAttr}"` : ''
521
- }></i>` +
522
- `<div class="an-icon-preview-name">${cls} fa-${name}</div>`;
523
- }
524
-
525
- // ---------------------------------------------------------------------------
526
- // Insert
527
- // ---------------------------------------------------------------------------
528
-
529
- _onInsert() {
530
- if (!this._selectedIcon) return;
531
-
532
- const cls = this._styleSelect?.value || 'fa-solid';
533
- const size = this._sizeSelect?.value || '';
534
- const useColor = this._useColorCb?.checked ?? false;
535
- const color = useColor ? (this._colorInput?.value ?? '') : '';
536
- const styleParts = [
537
- size ? `font-size:${size}` : '',
538
- color ? `color:${color}` : '',
539
- ].filter(Boolean);
540
-
541
- const iconEl = document.createElement('i');
542
- iconEl.className = `${cls} fa-${this._selectedIcon}`;
543
- iconEl.setAttribute('aria-hidden', 'true');
544
- iconEl.setAttribute('contenteditable', 'false');
545
- if (styleParts.length) iconEl.setAttribute('style', styleParts.join(';'));
546
-
547
- const savedRange = this._savedRange;
548
- const editable = this.context.layoutInfo.editable;
549
-
550
- // ---- Same ordering as ImageDialog (the proven pattern) ----
551
- // 1. Restore the selection while the dialog is still mounted
552
- if (savedRange) savedRange.select();
553
-
554
- // 2. Get the now-live range from the selection
555
- const sel = globalThis.getSelection();
556
- let range = (sel?.rangeCount ?? 0) > 0 ? sel.getRangeAt(0) : null;
557
- if (!range) {
558
- range = document.createRange();
559
- range.selectNodeContents(editable);
560
- range.collapse(false);
561
- }
562
-
563
- // 3. Insert the <i> node directly — never via execCommand/insertHTML
564
- // (execCommand leaves the caret inside the inserted element)
565
- //
566
- // E-1: When the range covers the entire content of a <td>/<th>,
567
- // deleteContents() can drift the range endpoint outside the cell in some
568
- // browsers. Save the cell reference first and re-anchor after deletion.
569
- const _sc = range.startContainer;
570
- const _tdAnchor = /** @type {Element|null} */ (_sc.nodeType === 1 ? _sc : _sc.parentElement)
571
- ?.closest('td, th');
572
- range.deleteContents();
573
- if (_tdAnchor?.isConnected && !_tdAnchor.contains(range.startContainer)) {
574
- range.setStart(_tdAnchor, 0);
575
- range.collapse(true);
576
- }
577
- range.insertNode(iconEl);
578
-
579
- // 4. Place cursor after the icon.
580
- // Keep a text anchor (\u200B) after the icon so ArrowLeft/ArrowRight and
581
- // caret placement at end-of-line stay stable across browsers.
582
- let caretTextNode = iconEl.nextSibling;
583
- if (caretTextNode?.nodeType !== Node.TEXT_NODE) {
584
- caretTextNode = document.createTextNode('\u200B');
585
- iconEl.parentNode.insertBefore(caretTextNode, iconEl.nextSibling);
586
- } else if (!caretTextNode.textContent) {
587
- // Split text-node insertions can leave an empty text node after icon.
588
- // Fill it with ZWS so caret-after-icon remains stable at end-of-line.
589
- caretTextNode.textContent = '\u200B';
590
- }
591
-
592
- const caretOffset = ((caretTextNode.textContent || '').startsWith('\u200B')) ? 1 : 0;
593
- const caretRange = document.createRange();
594
- caretRange.setStart(caretTextNode, Math.min(caretOffset, caretTextNode.textContent.length));
595
- caretRange.collapse(true);
596
- if (sel) {
597
- sel.removeAllRanges();
598
- sel.addRange(caretRange);
599
- }
600
-
601
- // 5. Close dialog last — same as ImageDialog
602
- this._close();
603
-
604
- // 6. Restore editor focus (needed for toolbar refresh via afterCommand)
605
- editable.focus();
606
- if (sel) {
607
- // Some browsers reset selection when refocusing editable after closing dialog.
608
- sel.removeAllRanges();
609
- sel.addRange(caretRange);
610
- }
611
- this.context.invoke('editor.afterCommand');
612
- }
613
-
614
- _close() {
615
- this._selectedIcon = null;
616
- super._close();
617
- }
618
- }