autumnnote 2.1.0 → 2.3.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 (65) hide show
  1. package/README.md +74 -6
  2. package/dist/autumnnote.cjs +27 -22
  3. package/dist/autumnnote.es.js +474 -591
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.min.js +27 -22
  6. package/dist/autumnnote.umd.js +27 -22
  7. package/dist/autumnnote.umd.js.map +1 -1
  8. package/dist/icon-data-V0Xqv-wX.js +255 -0
  9. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  10. package/package.json +12 -3
  11. package/types/index.d.ts +8 -0
  12. package/src/js/Context.js +0 -854
  13. package/src/js/core/detectLang.js +0 -98
  14. package/src/js/core/dom.js +0 -372
  15. package/src/js/core/env.js +0 -38
  16. package/src/js/core/key.js +0 -66
  17. package/src/js/core/lists.js +0 -121
  18. package/src/js/core/markdown.js +0 -695
  19. package/src/js/core/range.js +0 -194
  20. package/src/js/core/sanitise.js +0 -304
  21. package/src/js/editing/History.js +0 -266
  22. package/src/js/editing/Style.js +0 -812
  23. package/src/js/editing/Table.js +0 -105
  24. package/src/js/editing/Typing.js +0 -397
  25. package/src/js/index.js +0 -193
  26. package/src/js/index.umd.js +0 -17
  27. package/src/js/module/AutoSaveRestore.js +0 -125
  28. package/src/js/module/BaseDialog.js +0 -133
  29. package/src/js/module/BaseMediaTooltip.js +0 -142
  30. package/src/js/module/BaseResizer.js +0 -322
  31. package/src/js/module/BubbleToolbar.js +0 -483
  32. package/src/js/module/Buttons.js +0 -399
  33. package/src/js/module/Clipboard.js +0 -579
  34. package/src/js/module/CodeTooltip.js +0 -493
  35. package/src/js/module/Codeview.js +0 -125
  36. package/src/js/module/ContextMenu.js +0 -621
  37. package/src/js/module/Editor.js +0 -747
  38. package/src/js/module/EmojiDialog.js +0 -254
  39. package/src/js/module/FindReplace.js +0 -512
  40. package/src/js/module/Fullscreen.js +0 -80
  41. package/src/js/module/IconDialog.js +0 -618
  42. package/src/js/module/ImageCropOverlay.js +0 -586
  43. package/src/js/module/ImageDialog.js +0 -193
  44. package/src/js/module/ImageResizer.js +0 -42
  45. package/src/js/module/ImageTooltip.js +0 -285
  46. package/src/js/module/LinkDialog.js +0 -145
  47. package/src/js/module/LinkTooltip.js +0 -250
  48. package/src/js/module/MarkdownShortcuts.js +0 -250
  49. package/src/js/module/Mention.js +0 -365
  50. package/src/js/module/Placeholder.js +0 -51
  51. package/src/js/module/ShortcutsDialog.js +0 -111
  52. package/src/js/module/SlashMenu.js +0 -376
  53. package/src/js/module/Statusbar.js +0 -246
  54. package/src/js/module/TableTooltip.js +0 -1392
  55. package/src/js/module/Toolbar.js +0 -855
  56. package/src/js/module/VideoDialog.js +0 -193
  57. package/src/js/module/VideoResizer.js +0 -66
  58. package/src/js/module/VideoTooltip.js +0 -248
  59. package/src/js/module/emoji-data.js +0 -496
  60. package/src/js/module/table-grid.js +0 -102
  61. package/src/js/module/table-icons.js +0 -33
  62. package/src/js/renderer.js +0 -120
  63. package/src/js/settings.js +0 -214
  64. package/src/styles/_variables.scss +0 -48
  65. package/src/styles/autumnnote.scss +0 -2877
@@ -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
- }