autumnnote 1.0.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +874 -0
  3. package/dist/autumnnote.css +1 -0
  4. package/dist/autumnnote.es.js +5888 -0
  5. package/dist/autumnnote.es.js.map +1 -0
  6. package/dist/autumnnote.umd.js +74 -0
  7. package/dist/autumnnote.umd.js.map +1 -0
  8. package/package.json +55 -0
  9. package/src/js/Context.js +497 -0
  10. package/src/js/core/dom.js +315 -0
  11. package/src/js/core/env.js +25 -0
  12. package/src/js/core/func.js +153 -0
  13. package/src/js/core/key.js +66 -0
  14. package/src/js/core/lists.js +121 -0
  15. package/src/js/core/markdown.js +294 -0
  16. package/src/js/core/range.js +194 -0
  17. package/src/js/core/sanitise.js +78 -0
  18. package/src/js/editing/History.js +205 -0
  19. package/src/js/editing/Style.js +329 -0
  20. package/src/js/editing/Table.js +59 -0
  21. package/src/js/editing/Typing.js +142 -0
  22. package/src/js/index.js +126 -0
  23. package/src/js/module/Buttons.js +300 -0
  24. package/src/js/module/Clipboard.js +460 -0
  25. package/src/js/module/CodeTooltip.js +428 -0
  26. package/src/js/module/Codeview.js +122 -0
  27. package/src/js/module/ContextMenu.js +470 -0
  28. package/src/js/module/Editor.js +528 -0
  29. package/src/js/module/EmojiDialog.js +726 -0
  30. package/src/js/module/FindReplace.js +440 -0
  31. package/src/js/module/Fullscreen.js +80 -0
  32. package/src/js/module/IconDialog.js +620 -0
  33. package/src/js/module/ImageDialog.js +208 -0
  34. package/src/js/module/ImageResizer.js +216 -0
  35. package/src/js/module/ImageTooltip.js +286 -0
  36. package/src/js/module/LinkDialog.js +204 -0
  37. package/src/js/module/LinkTooltip.js +242 -0
  38. package/src/js/module/Placeholder.js +44 -0
  39. package/src/js/module/ShortcutsDialog.js +141 -0
  40. package/src/js/module/Statusbar.js +238 -0
  41. package/src/js/module/TableTooltip.js +568 -0
  42. package/src/js/module/Toolbar.js +562 -0
  43. package/src/js/module/VideoDialog.js +263 -0
  44. package/src/js/module/VideoResizer.js +227 -0
  45. package/src/js/module/VideoTooltip.js +252 -0
  46. package/src/js/renderer.js +107 -0
  47. package/src/js/settings.js +134 -0
  48. package/src/styles/_variables.scss +48 -0
  49. package/src/styles/autumnnote.scss +1740 -0
  50. package/types/index.d.ts +324 -0
@@ -0,0 +1,620 @@
1
+ /**
2
+ * IconDialog.js - Browse and insert FontAwesome Free icons
3
+ */
4
+
5
+ import { createElement, on, trapFocus } from '../core/dom.js';
6
+ import { withSavedRange } from '../core/range.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 {
251
+ /**
252
+ * @param {import('../Context.js').Context} context
253
+ */
254
+ constructor(context) {
255
+ this.context = context;
256
+ /** @type {HTMLElement|null} */
257
+ this._dialog = null;
258
+ this._disposers = [];
259
+ this._savedRange = null;
260
+ this._selectedIcon = null;
261
+ this._activeCat = 'all';
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Lifecycle
266
+ // ---------------------------------------------------------------------------
267
+
268
+ initialize() {
269
+ this._ensureFontAwesome();
270
+ // Dialog grid is built lazily on first show() to avoid rendering ~250 icon cells at load time.
271
+ return this;
272
+ }
273
+
274
+ /**
275
+ * Injects FA 6 Free CSS from CDN if it is not already on the page.
276
+ * This guarantees icon glyphs render in the grid, preview, and in the
277
+ * editor content after insertion — regardless of whether the host page
278
+ * loaded FA itself.
279
+ */
280
+ _ensureFontAwesome() {
281
+ // Already present (icon element or stylesheet link)?
282
+ if (document.getElementById('an-fontawesome-css')) return;
283
+ const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
284
+ .map((l) => l.href || '').join(' ');
285
+ if (/fontawesome|font-awesome/.test(links)) return;
286
+ if (document.querySelector('.fa-solid, .fas, .far, .fab')) return;
287
+
288
+ const link = document.createElement('link');
289
+ link.id = 'an-fontawesome-css';
290
+ link.rel = 'stylesheet';
291
+ link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css';
292
+ link.crossOrigin = 'anonymous';
293
+ link.referrerPolicy = 'no-referrer';
294
+ document.head.appendChild(link);
295
+ }
296
+
297
+ destroy() {
298
+ this._disposers.forEach((d) => d());
299
+ this._disposers = [];
300
+ if (this._dialog && this._dialog.parentNode) {
301
+ this._dialog.parentNode.removeChild(this._dialog);
302
+ }
303
+ this._dialog = null;
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // Public API
308
+ // ---------------------------------------------------------------------------
309
+
310
+ show() {
311
+ if (!this._dialog) {
312
+ this._dialog = this._buildDialog();
313
+ document.body.appendChild(this._dialog);
314
+ }
315
+ withSavedRange((range) => {
316
+ this._savedRange = range;
317
+ });
318
+ this._selectedIcon = null;
319
+ this._activeCat = 'all';
320
+ this._searchInput.value = '';
321
+ this._updateCatTabs();
322
+ this._filterIcons('', 'all');
323
+ this._updatePreview(null);
324
+ this._open();
325
+ }
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // Build dialog
329
+ // ---------------------------------------------------------------------------
330
+
331
+ _buildDialog() {
332
+ const overlay = createElement('div', {
333
+ class: 'an-dialog-overlay',
334
+ role: 'dialog',
335
+ 'aria-modal': 'true',
336
+ 'aria-label': 'Insert FA icon',
337
+ });
338
+
339
+ const box = createElement('div', { class: 'an-dialog-box an-icon-box' });
340
+
341
+ // Title row
342
+ const titleRow = createElement('div', { class: 'an-icon-title-row' });
343
+ const title = createElement('h3', { class: 'an-dialog-title' });
344
+ title.textContent = 'Insert FA Icon';
345
+ const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': 'Close' });
346
+ closeBtn.innerHTML = '×';
347
+ titleRow.append(title, closeBtn);
348
+
349
+ // Search
350
+ const searchInput = createElement('input', {
351
+ type: 'search',
352
+ class: 'an-input an-icon-search',
353
+ placeholder: 'Search icons…',
354
+ autocomplete: 'off',
355
+ });
356
+ this._searchInput = searchInput;
357
+
358
+ // Category tabs
359
+ const catBar = createElement('div', { class: 'an-icon-cats' });
360
+ const allTab = createElement('button', { type: 'button', class: 'an-icon-cat active', 'data-cat': 'all' });
361
+ allTab.textContent = 'All';
362
+ catBar.appendChild(allTab);
363
+ ICON_CATEGORIES.forEach(({ id, label }) => {
364
+ const tab = createElement('button', { type: 'button', class: 'an-icon-cat', 'data-cat': id });
365
+ tab.textContent = label;
366
+ catBar.appendChild(tab);
367
+ });
368
+ this._catBar = catBar;
369
+
370
+ // Icon grid
371
+ const grid = createElement('div', { class: 'an-icon-grid' });
372
+ ICON_LIST.forEach(([name, cat]) => {
373
+ const cell = createElement('button', { type: 'button', class: 'an-icon-cell', 'data-name': name, 'data-cat': cat, title: name });
374
+ const icon = createElement('i', { class: 'fa-solid fa-' + name, 'aria-hidden': 'true' });
375
+ const label = createElement('span');
376
+ label.textContent = name;
377
+ cell.append(icon, label);
378
+ grid.appendChild(cell);
379
+ });
380
+ this._grid = grid;
381
+
382
+ // Options row: style + size + color
383
+ const optRow = createElement('div', { class: 'an-icon-options' });
384
+
385
+ const styleLabel = createElement('label', { class: 'an-label' });
386
+ styleLabel.textContent = 'Style';
387
+ const styleSelect = createElement('select', { class: 'an-input an-icon-option-select' });
388
+ [['fa-solid', 'Solid'], ['fa-regular', 'Regular'], ['fa-light', 'Light (Pro)']].forEach(([v, t]) => {
389
+ const opt = createElement('option', { value: v });
390
+ opt.textContent = t;
391
+ styleSelect.appendChild(opt);
392
+ });
393
+ styleSelect.value = 'fa-solid';
394
+ this._styleSelect = styleSelect;
395
+
396
+ const sizeLabel = createElement('label', { class: 'an-label' });
397
+ sizeLabel.textContent = 'Size';
398
+ const sizeSelect = createElement('select', { class: 'an-input an-icon-option-select' });
399
+ [['', 'Inherit'], ['0.75em', '0.75em'], ['1em', '1em'], ['1.25em', '1.25em'], ['1.5em', '1.5em'], ['2em', '2em'], ['3em', '3em']].forEach(([v, t]) => {
400
+ const opt = createElement('option', { value: v });
401
+ if (v === '1em') opt.selected = true;
402
+ opt.textContent = t;
403
+ sizeSelect.appendChild(opt);
404
+ });
405
+ this._sizeSelect = sizeSelect;
406
+
407
+ const colorLabel = createElement('label', { class: 'an-label' });
408
+ colorLabel.textContent = 'Color';
409
+ const colorInput = createElement('input', { type: 'color', class: 'an-icon-color', value: '#000000' });
410
+ this._colorInput = colorInput;
411
+
412
+ const useColorLabel = createElement('label', { class: 'an-label an-label-inline an-icon-use-color' });
413
+ const useColorCb = createElement('input', { type: 'checkbox', checked: '' });
414
+ this._useColorCb = useColorCb;
415
+ useColorLabel.append(useColorCb, document.createTextNode(' Use color'));
416
+
417
+ optRow.append(styleLabel, styleSelect, sizeLabel, sizeSelect, colorLabel, colorInput, useColorLabel);
418
+
419
+ // Preview
420
+ const preview = createElement('div', { class: 'an-icon-preview' });
421
+ const previewHint = createElement('span', { class: 'an-icon-preview-hint' });
422
+ previewHint.textContent = 'Select an icon';
423
+ preview.appendChild(previewHint);
424
+ this._preview = preview;
425
+
426
+ // Actions
427
+ const btnRow = createElement('div', { class: 'an-dialog-actions' });
428
+ const insertBtn = createElement('button', { type: 'button', class: 'an-btn an-btn-primary', disabled: '' });
429
+ insertBtn.textContent = 'Insert FA Icon';
430
+ const cancelBtn = createElement('button', { type: 'button', class: 'an-btn' });
431
+ cancelBtn.textContent = 'Cancel';
432
+ btnRow.append(insertBtn, cancelBtn);
433
+ this._insertBtn = insertBtn;
434
+
435
+ box.append(titleRow, searchInput, catBar, grid, optRow, preview, btnRow);
436
+ overlay.appendChild(box);
437
+
438
+ // -------------------------------------------------------------------------
439
+ // Events
440
+ // -------------------------------------------------------------------------
441
+
442
+ const d1 = on(closeBtn, 'click', () => this._close());
443
+ const d2 = on(cancelBtn, 'click', () => this._close());
444
+ const d3 = on(insertBtn, 'click', () => this._onInsert());
445
+ const d4 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
446
+ const d5 = on(searchInput, 'input', () => this._filterIcons(searchInput.value, this._activeCat));
447
+ const d6 = on(catBar, 'click', (e) => {
448
+ const tab = e.target.closest('[data-cat]');
449
+ if (tab) {
450
+ this._activeCat = tab.dataset.cat;
451
+ this._updateCatTabs();
452
+ this._filterIcons(this._searchInput.value, this._activeCat);
453
+ }
454
+ });
455
+ const d7 = on(grid, 'click', (e) => {
456
+ const cell = e.target.closest('.an-icon-cell');
457
+ if (cell) this._selectIcon(cell.dataset.name);
458
+ });
459
+ const d8 = on(styleSelect, 'change', () => this._updatePreview(this._selectedIcon));
460
+ const d9 = on(sizeSelect, 'change', () => this._updatePreview(this._selectedIcon));
461
+ const d10 = on(colorInput, 'input', () => this._updatePreview(this._selectedIcon));
462
+ const d11 = on(useColorCb, 'change', () => this._updatePreview(this._selectedIcon));
463
+
464
+ this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11);
465
+
466
+ return overlay;
467
+ }
468
+
469
+ // ---------------------------------------------------------------------------
470
+ // Filter / select
471
+ // ---------------------------------------------------------------------------
472
+
473
+ _updateCatTabs() {
474
+ this._catBar.querySelectorAll('.an-icon-cat').forEach((tab) => {
475
+ tab.classList.toggle('active', tab.dataset.cat === this._activeCat);
476
+ });
477
+ }
478
+
479
+ _filterIcons(query, cat) {
480
+ const q = (query || '').trim().toLowerCase();
481
+ let visibleCount = 0;
482
+ this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
483
+ const name = cell.dataset.name;
484
+ const cellCat = cell.dataset.cat;
485
+ const matchesCat = !cat || cat === 'all' || cellCat === cat;
486
+ const matchesQuery = !q || name.includes(q);
487
+ const visible = matchesCat && matchesQuery;
488
+ cell.style.display = visible ? '' : 'none';
489
+ if (visible) visibleCount++;
490
+ });
491
+ // Show empty state if needed
492
+ let empty = this._grid.querySelector('.an-icon-empty');
493
+ if (!empty) {
494
+ empty = createElement('div', { class: 'an-icon-empty' });
495
+ empty.textContent = 'No icons found';
496
+ this._grid.appendChild(empty);
497
+ }
498
+ empty.style.display = visibleCount > 0 ? 'none' : '';
499
+ }
500
+
501
+ _selectIcon(name) {
502
+ this._selectedIcon = name;
503
+ // Highlight selected cell
504
+ this._grid.querySelectorAll('.an-icon-cell').forEach((cell) => {
505
+ cell.classList.toggle('active', cell.dataset.name === name);
506
+ });
507
+ // Enable insert button
508
+ this._insertBtn.removeAttribute('disabled');
509
+ // Update preview
510
+ this._updatePreview(name);
511
+ }
512
+
513
+ _updatePreview(name) {
514
+ if (!this._preview) return;
515
+ if (!name) {
516
+ this._preview.innerHTML = '<span class="an-icon-preview-hint">Select an icon</span>';
517
+ return;
518
+ }
519
+ const cls = (this._styleSelect && this._styleSelect.value) || 'fa-solid';
520
+ const size = (this._sizeSelect && this._sizeSelect.value) || '1em';
521
+ const useColor = this._useColorCb ? this._useColorCb.checked : false;
522
+ const color = (useColor && this._colorInput) ? this._colorInput.value : '';
523
+ const styleAttr = [
524
+ size ? `font-size:${size}` : '',
525
+ color ? `color:${color}` : '',
526
+ ].filter(Boolean).join(';');
527
+ // Use innerHTML — atomic, no partial-update risk
528
+ this._preview.innerHTML =
529
+ `<i class="${cls} fa-${name}" aria-hidden="true"${
530
+ styleAttr ? ` style="${styleAttr}"` : ''
531
+ }></i>` +
532
+ `<div class="an-icon-preview-name">${cls} fa-${name}</div>`;
533
+ }
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // Insert
537
+ // ---------------------------------------------------------------------------
538
+
539
+ _onInsert() {
540
+ if (!this._selectedIcon) return;
541
+
542
+ const cls = (this._styleSelect && this._styleSelect.value) || 'fa-solid';
543
+ const size = (this._sizeSelect && this._sizeSelect.value) || '';
544
+ const useColor = this._useColorCb ? this._useColorCb.checked : false;
545
+ const color = (useColor && this._colorInput) ? this._colorInput.value : '';
546
+ const styleParts = [
547
+ size ? `font-size:${size}` : '',
548
+ color ? `color:${color}` : '',
549
+ ].filter(Boolean);
550
+
551
+ const iconEl = document.createElement('i');
552
+ iconEl.className = `${cls} fa-${this._selectedIcon}`;
553
+ iconEl.setAttribute('aria-hidden', 'true');
554
+ if (styleParts.length) iconEl.setAttribute('style', styleParts.join(';'));
555
+
556
+ const savedRange = this._savedRange;
557
+ const editable = this.context.layoutInfo.editable;
558
+
559
+ // ---- Same ordering as ImageDialog (the proven pattern) ----
560
+ // 1. Restore the selection while the dialog is still mounted
561
+ if (savedRange) savedRange.select();
562
+
563
+ // 2. Get the now-live range from the selection
564
+ const sel = window.getSelection();
565
+ let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
566
+ if (!range) {
567
+ range = document.createRange();
568
+ range.selectNodeContents(editable);
569
+ range.collapse(false);
570
+ }
571
+
572
+ // 3. Insert the <i> node directly — never via execCommand/insertHTML
573
+ // (execCommand leaves the caret inside the inserted element)
574
+ range.deleteContents();
575
+ range.insertNode(iconEl);
576
+
577
+ // 4. Insert a zero-width space text node immediately after the icon.
578
+ // This is essential when the icon lands at the END of a paragraph:
579
+ // browsers cannot place the caret after an inline element that is the
580
+ // last child of a block — there is no text node to anchor into.
581
+ // The ZWS gives the caret a real text node to sit in, so typing after
582
+ // the icon works correctly. It is invisible to the reader.
583
+ const zwsNode = document.createTextNode('\u200B');
584
+ iconEl.parentNode.insertBefore(zwsNode, iconEl.nextSibling);
585
+
586
+ // 5. Place caret inside the ZWS text node (offset 1 = after the ZWS char).
587
+ range.setStart(zwsNode, 1);
588
+ range.collapse(true);
589
+ if (sel) {
590
+ sel.removeAllRanges();
591
+ sel.addRange(range);
592
+ }
593
+
594
+ // 5. Close dialog last — same as ImageDialog
595
+ this._close();
596
+
597
+ // 6. Restore editor focus (needed for toolbar refresh via afterCommand)
598
+ editable.focus();
599
+ this.context.invoke('editor.afterCommand');
600
+ }
601
+
602
+ // ---------------------------------------------------------------------------
603
+ // Open / Close
604
+ // ---------------------------------------------------------------------------
605
+
606
+ _open() {
607
+ if (this._dialog) {
608
+ this._dialog.style.display = 'flex';
609
+ this._removeTrap = trapFocus(this._dialog, () => this._close());
610
+ setTimeout(() => this._searchInput && this._searchInput.focus(), 50);
611
+ }
612
+ }
613
+
614
+ _close() {
615
+ if (this._dialog) this._dialog.style.display = 'none';
616
+ if (this._removeTrap) { this._removeTrap(); this._removeTrap = null; }
617
+ this._savedRange = null;
618
+ this._selectedIcon = null;
619
+ }
620
+ }