autumnnote 2.1.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 (65) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.es.js +385 -619
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.min.js +20 -20
  6. package/dist/autumnnote.umd.js +20 -20
  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,855 +0,0 @@
1
- /**
2
- * Toolbar.js - Builds and manages the editor toolbar UI
3
- * Inspired by Summernote's Toolbar module — rewritten without jQuery
4
- */
5
-
6
- import { createElement, on } from '../core/dom.js';
7
- import { getButton } from './Buttons.js';
8
-
9
- /** Resolve a toolbar item: string → registry lookup, object → pass-through. */
10
- const _resolveBtn = (item) => (typeof item === 'string') ? getButton(item) : item;
11
-
12
- // Module-level cache for FontAwesome detection.
13
- // Evaluated once per page load so all Toolbar instances on the same page agree
14
- // on whether the HOST PAGE included FA — regardless of whether IconDialog later
15
- // auto-injects its own FA <link> for the icon-picker glyph rendering.
16
- let _faPageLevelReady = null;
17
-
18
- // ---------------------------------------------------------------------------
19
- // Module-level icon lookup tables — built once, shared across all instances.
20
- // Previously these were re-created inside _createButton() on every button
21
- // render, producing O(buttons × map-size) allocations per toolbar init.
22
- // ---------------------------------------------------------------------------
23
- const _S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
24
- const _svgWrap = (paths) =>
25
- `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${_S} style="display:block">${paths}</svg>`;
26
-
27
- const _SVG_MAP = new Map([
28
- // Format
29
- ['bold', _svgWrap('<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/>')],
30
- ['italic', _svgWrap('<line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/>')],
31
- ['underline', _svgWrap('<path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/><line x1="4" y1="21" x2="20" y2="21"/>')],
32
- ['strikethrough', _svgWrap('<path d="M17.3 12H6.7"/><path d="M10 6.5C10 5.1 11.1 4 12.5 4c1.4 0 2.5 1.1 2.5 2.5 0 .8-.4 1.5-1 2"/><path d="M14 17.5C14 19 12.9 20 11.5 20 10.1 20 9 18.9 9 17.5c0-.8.4-1.5 1-2"/>')],
33
- ['superscript', _svgWrap('<path d="m4 19 8-8"/><path d="m12 19-8-8"/><path d="M20 12h-4c0-1.5.44-2 1.5-2.5S20 8.33 20 7.25C20 6 19 5 17.5 5S15 6 15 7"/>')],
34
- ['subscript', _svgWrap('<path d="m4 5 8 8"/><path d="m12 5-8 8"/><path d="M20 21h-4c0-1.5.44-2 1.5-2.5S20 17.33 20 16.25C20 15 19 14 17.5 14S15 15 15 16"/>')],
35
- // Alignment
36
- ['align-left', _svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="15" y1="12" x2="3" y2="12"/><line x1="17" y1="18" x2="3" y2="18"/>')],
37
- ['align-center', _svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="18" y1="12" x2="6" y2="12"/><line x1="21" y1="18" x2="3" y2="18"/>')],
38
- ['align-right', _svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="12" x2="9" y2="12"/><line x1="21" y1="18" x2="7" y2="18"/>')],
39
- ['align-justify', _svgWrap('<line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="12" x2="3" y2="12"/><line x1="21" y1="18" x2="3" y2="18"/>')],
40
- // Lists
41
- ['list-ul', _svgWrap('<line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4" cy="6" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="4" cy="18" r="1" fill="currentColor" stroke="none"/>')],
42
- ['list-ol', _svgWrap('<line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1V3"/><path d="M4 10h2l-2 2h2"/><path d="M4 16.5A1.5 1.5 0 0 1 5.5 15a1.5 1.5 0 0 1 0 3H4"/>')],
43
- ['indent', _svgWrap('<polyline points="3 8 7 12 3 16"/><line x1="21" y1="12" x2="11" y2="12"/><line x1="21" y1="6" x2="11" y2="6"/><line x1="21" y1="18" x2="11" y2="18"/>')],
44
- ['outdent', _svgWrap('<polyline points="7 8 3 12 7 16"/><line x1="21" y1="12" x2="11" y2="12"/><line x1="21" y1="6" x2="11" y2="6"/><line x1="21" y1="18" x2="11" y2="18"/>')],
45
- // History
46
- ['undo', _svgWrap('<path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/>')],
47
- ['redo', _svgWrap('<path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"/>')],
48
- // Insert
49
- ['minus', _svgWrap('<line x1="5" y1="12" x2="19" y2="12"/>')],
50
- ['link', _svgWrap('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>')],
51
- ['image', _svgWrap('<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"/>')],
52
- ['video', _svgWrap('<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/>')],
53
- ['table', _svgWrap('<rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/>')],
54
- ['emoji', _svgWrap('<circle cx="12" cy="12" r="10"/><path d="M8.5 14.5s1.5 2.5 3.5 2.5 3.5-2.5 3.5-2.5"/><circle cx="9" cy="9" r="1.5" fill="currentColor" stroke="none"/><circle cx="15" cy="9" r="1.5" fill="currentColor" stroke="none"/>')],
55
- ['icon', _svgWrap('<circle cx="8" cy="8" r="3"/><circle cx="16" cy="8" r="3"/><rect x="5" y="13" width="6" height="6" rx="1"/><rect x="13" y="13" width="6" height="6" rx="1"/>')],
56
- // View
57
- ['code', _svgWrap('<polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>')],
58
- ['expand', _svgWrap('<polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>')],
59
- // Color pickers
60
- ['foreColor', _svgWrap('<path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/>')],
61
- ['backColor', _svgWrap('<path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/>')],
62
- ['keyboard', _svgWrap('<rect x="2" y="6" width="20" height="12" rx="2"/><line x1="6" y1="10" x2="6" y2="10" stroke-width="2.5"/><line x1="10" y1="10" x2="10" y2="10" stroke-width="2.5"/><line x1="14" y1="10" x2="14" y2="10" stroke-width="2.5"/><line x1="18" y1="10" x2="18" y2="10" stroke-width="2.5"/><line x1="8" y1="14" x2="16" y2="14" stroke-width="2"/>')],
63
- ['caption', _svgWrap('<rect x="3" y="3" width="18" height="11" rx="2"/><line x1="6" y1="18" x2="18" y2="18"/><line x1="9" y1="21" x2="15" y2="21"/>')],
64
- ['remove-format', _svgWrap('<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/>')],
65
- ['direction', _svgWrap('<path d="M12 20V4"/><path d="m9 7-3 3 3 3"/><path d="M4 10h8"/><path d="m15 7 3 3-3 3"/><path d="M20 10h-8"/>')],
66
- ['search', _svgWrap('<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>')],
67
- ['find-replace', _svgWrap('<circle cx="10" cy="10" r="6"/><line x1="18" y1="18" x2="14.35" y2="14.35"/><path d="M16 19h6"/><path d="M19 16v6"/>')],
68
- ['inline-code', _svgWrap('<path d="M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1"/><path d="M16 3h1a2 2 0 0 1 2 2v5c0 1.1.9 2 2 2a2 2 0 0 1-2 2v5a2 2 0 0 1-2 2h-1"/>')],
69
- ['checklist', _svgWrap('<rect x="3" y="4" width="5" height="5" rx="1"/><path d="m4 6.5 1 1 2-2"/><rect x="3" y="13" width="5" height="5" rx="1"/><line x1="10" y1="6.5" x2="21" y2="6.5"/><line x1="10" y1="15.5" x2="21" y2="15.5"/>')],
70
- ['print', _svgWrap('<path d="M6 9V2h12v7"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/>')],
71
- ]);
72
-
73
- const _FA_MAP = new Map([
74
- ['bold', 'fa-bold'],
75
- ['italic', 'fa-italic'],
76
- ['underline', 'fa-underline'],
77
- ['strikethrough', 'fa-strikethrough'],
78
- ['superscript', 'fa-superscript'],
79
- ['subscript', 'fa-subscript'],
80
- ['align-left', 'fa-align-left'],
81
- ['align-center', 'fa-align-center'],
82
- ['align-right', 'fa-align-right'],
83
- ['align-justify', 'fa-align-justify'],
84
- ['list-ul', 'fa-list-ul'],
85
- ['list-ol', 'fa-list-ol'],
86
- ['indent', 'fa-indent'],
87
- ['outdent', 'fa-outdent'],
88
- ['undo', 'fa-rotate-left'],
89
- ['redo', 'fa-rotate-right'],
90
- ['minus', 'fa-minus'],
91
- ['link', 'fa-link'],
92
- ['image', 'fa-image'],
93
- ['code', 'fa-code'],
94
- ['expand', 'fa-expand'],
95
- ['emoji', 'fa-face-smile'],
96
- ['icon', 'fa-icons'],
97
- ['foreColor', 'fa-font'],
98
- ['backColor', 'fa-highlighter'],
99
- ['keyboard', 'fa-keyboard'],
100
- ['remove-format', 'fa-remove-format'],
101
- ['direction', 'fa-arrow-right-arrow-left'],
102
- ['search', 'fa-magnifying-glass'],
103
- ['find-replace', 'fa-magnifying-glass-plus'],
104
- ['inline-code', 'fa-code'],
105
- ['checklist', 'fa-list-check'],
106
- ['print', 'fa-print'],
107
- ]);
108
-
109
- export class Toolbar {
110
- /**
111
- * @param {import('../Context.js').Context} context
112
- */
113
- constructor(context) {
114
- this.context = context;
115
- this.options = context.options;
116
- /** @type {HTMLElement|null} */
117
- this.el = null;
118
- /** @type {Array<() => void>} disposers */
119
- this._disposers = [];
120
- /** @type {Array<() => void>} closers for all open color picker popups */
121
- this._colorPickerClosers = [];
122
- /** @type {number|null} rAF handle for debounced refresh */
123
- this._refreshRaf = null;
124
- }
125
-
126
- // ---------------------------------------------------------------------------
127
- // Lifecycle
128
- // ---------------------------------------------------------------------------
129
-
130
- initialize() {
131
- this.el = createElement('div', {
132
- class: 'an-toolbar',
133
- role: 'toolbar',
134
- 'aria-orientation': 'horizontal',
135
- // Matches the hardcoded English label renderer.js gives the editable.
136
- 'aria-label': 'Editor toolbar',
137
- });
138
- // Detect FontAwesome once at toolbar build time to avoid re-querying the DOM
139
- // for every button rendered.
140
- this._faReady = this._detectFontAwesome();
141
- this._buildButtons();
142
- this._initRovingFocus();
143
- this._btnMap = new Map(
144
- (this.options.toolbar || []).flat()
145
- .map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]),
146
- );
147
- return this;
148
- }
149
-
150
- destroy() {
151
- if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
152
- this._refreshRaf = null;
153
- this._disposers.forEach((d) => d());
154
- this._disposers = [];
155
- if (this.el?.parentNode) {
156
- this.el.remove();
157
- }
158
- this.el = null;
159
- }
160
-
161
- // ---------------------------------------------------------------------------
162
- // Build
163
- // ---------------------------------------------------------------------------
164
-
165
- _buildButtons() {
166
- const toolbar = this.options.toolbar || [];
167
- // Build into a DocumentFragment so all groups are appended in a single
168
- // DOM operation, avoiding one reflow per group.
169
- const fragment = document.createDocumentFragment();
170
- toolbar.forEach((group) => {
171
- const groupEl = createElement('div', { class: 'an-btn-group' });
172
- group.forEach((item) => {
173
- const btnDef = _resolveBtn(item);
174
- if (!btnDef) {
175
- console.warn(`[AutumnNote] Toolbar: button "${item}" not found in registry. Skipped.`);
176
- return;
177
- }
178
- let el;
179
- if (btnDef.type === 'select') el = this._createSelect(btnDef);
180
- else if (btnDef.type === 'grid') el = this._createGridPicker(btnDef);
181
- else if (btnDef.type === 'colorpicker') el = this._createColorPicker(btnDef);
182
- else el = this._createButton(btnDef);
183
- groupEl.appendChild(el);
184
- });
185
- fragment.appendChild(groupEl);
186
- });
187
- this.el.appendChild(fragment);
188
- }
189
-
190
- /**
191
- * Creates a table-grid picker button with a hoverable row/col selector popup.
192
- * @param {import('./Buttons.js').ButtonDef} def
193
- * @returns {HTMLDivElement}
194
- */
195
- _createGridPicker(def) {
196
- const ROWS = 10;
197
- const COLS = 10;
198
-
199
- const wrap = createElement('div', { class: 'an-table-picker-wrap' });
200
-
201
- const useBootstrap = !!this.options.useBootstrap;
202
- const baseClass = useBootstrap
203
- ? (this.options.toolbarButtonClass || 'btn btn-sm btn-light')
204
- : 'an-btn';
205
- const btn = createElement('button', {
206
- type: 'button',
207
- class: baseClass,
208
- title: this.context.locale.toolbar[def.name] || def.tooltip || '',
209
- 'data-btn': def.name,
210
- 'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
211
- 'aria-haspopup': 'true',
212
- 'aria-expanded': 'false',
213
- });
214
-
215
- // Set icon — inline SVG (table) with optional FontAwesome fallback
216
- if (this._faReady) {
217
- const faPrefix = this.options.fontAwesomeClass || 'fas';
218
- btn.innerHTML = `<i class="${faPrefix} fa-table" aria-hidden="true"></i>`;
219
- } else {
220
- const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
221
- btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${S} style="display:block"><rect x="3" y="3" width="18" height="18" rx="1"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/></svg>`;
222
- }
223
-
224
- // Popup
225
- const popup = createElement('div', {
226
- class: 'an-table-picker-popup',
227
- role: 'dialog',
228
- 'aria-label': 'Select table size',
229
- });
230
- const grid = createElement('div', { class: 'an-table-grid' });
231
- const label = createElement('div', { class: 'an-table-label' });
232
- label.textContent = this.context.locale.toolbar.insertTableLabel || 'Insert Table';
233
-
234
- const cells = [];
235
- for (let r = 1; r <= ROWS; r++) {
236
- for (let c = 1; c <= COLS; c++) {
237
- const cell = createElement('div', {
238
- class: 'an-table-cell',
239
- 'data-row': String(r),
240
- 'data-col': String(c),
241
- });
242
- cells.push(cell);
243
- grid.appendChild(cell);
244
- }
245
- }
246
-
247
- popup.appendChild(grid);
248
- popup.appendChild(label);
249
-
250
- let isOpen = false;
251
-
252
- const setHighlight = (rows, cols) => {
253
- cells.forEach((cell) => {
254
- const r = +cell.dataset.row;
255
- const c = +cell.dataset.col;
256
- cell.classList.toggle('active', r <= rows && c <= cols);
257
- });
258
- label.textContent = (rows && cols) ? `${rows} × ${cols}` : (this.context.locale.toolbar.insertTableLabel || 'Insert Table');
259
- };
260
-
261
- const openPopup = () => {
262
- isOpen = true;
263
- const rect = btn.getBoundingClientRect();
264
-
265
- // Measure popup dimensions while invisible so we can set the correct
266
- // position before the browser paints (matches the color-picker pattern).
267
- popup.style.visibility = 'hidden';
268
- popup.style.display = 'block';
269
- const pw = popup.offsetWidth;
270
- const ph = popup.offsetHeight;
271
-
272
- let left = rect.left;
273
- let top = rect.bottom + 4;
274
- if (left + pw > globalThis.innerWidth - 8) left = Math.max(8, globalThis.innerWidth - pw - 8);
275
- if (top + ph > globalThis.innerHeight - 8) top = rect.top - ph - 4;
276
-
277
- popup.style.left = `${left}px`;
278
- popup.style.top = `${top}px`;
279
- popup.style.visibility = '';
280
- btn.setAttribute('aria-expanded', 'true');
281
- };
282
-
283
- const closePopup = () => {
284
- isOpen = false;
285
- popup.style.display = 'none';
286
- btn.setAttribute('aria-expanded', 'false');
287
- setHighlight(0, 0);
288
- };
289
-
290
- const d1 = on(btn, 'click', (e) => {
291
- e.stopPropagation();
292
- if (isOpen) closePopup(); else openPopup();
293
- });
294
-
295
- const d2 = on(grid, 'mouseover', (e) => {
296
- const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
297
- if (!cell) return;
298
- setHighlight(+cell.dataset.row, +cell.dataset.col);
299
- });
300
-
301
- const d3 = on(grid, 'mouseleave', () => setHighlight(0, 0));
302
-
303
- const d4 = on(grid, 'click', (e) => {
304
- const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
305
- if (!cell) return;
306
- const rows = +cell.dataset.row;
307
- const cols = +cell.dataset.col;
308
- closePopup();
309
- this.context.invoke('editor.focus');
310
- def.action(this.context, rows, cols);
311
- });
312
-
313
- const d5 = on(document, 'click', () => { if (isOpen) closePopup(); });
314
-
315
- // Append popup to body so position:fixed is truly viewport-relative,
316
- // unaffected by any ancestor transform / filter (same pattern as color picker).
317
- this._disposers.push(d1, d2, d3, d4, d5, () => {
318
- if (popup.parentNode) popup.remove();
319
- });
320
-
321
- wrap.appendChild(btn);
322
- document.body.appendChild(popup);
323
- return /** @type {HTMLDivElement} */ (wrap);
324
- }
325
-
326
- /**
327
- * Creates a split color-picker widget:
328
- * [icon + strip | ▾] — left applies current color, right opens swatch popup.
329
- * @param {{ name: string, type: 'colorpicker', tooltip: string, defaultColor: string, action: Function }} def
330
- * @returns {HTMLDivElement}
331
- */
332
- _createColorPicker(def) {
333
- const PRESETS = [
334
- // Grayscale
335
- '#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#efefef', '#ffffff',
336
- // Saturated
337
- '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#9900ff', '#ff00ff',
338
- // Pastel
339
- '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#d9d2e9', '#ead1dc',
340
- ];
341
-
342
- let currentColor = def.defaultColor || '#000000';
343
-
344
- const wrap = createElement('div', { class: 'an-color-picker-wrap' });
345
-
346
- const useBootstrap = !!this.options.useBootstrap;
347
- const baseClass = useBootstrap ? (this.options.toolbarButtonClass || 'btn btn-sm btn-light') : 'an-btn';
348
-
349
- // ---- Apply button (icon + color strip) ----
350
- const applyBtn = createElement('button', {
351
- type: 'button',
352
- class: `${baseClass} an-color-btn`,
353
- title: this.context.locale.toolbar[def.name] || def.tooltip || '',
354
- 'data-btn': def.name,
355
- 'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
356
- });
357
-
358
- const S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
359
- const iconSvg = def.name === 'foreColor'
360
- ? `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${S} style="display:block"><path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/></svg>`
361
- : `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${S} style="display:block"><path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/></svg>`;
362
-
363
- applyBtn.innerHTML = iconSvg;
364
- const strip = createElement('span', { class: 'an-color-strip' });
365
- strip.style.background = currentColor;
366
- applyBtn.appendChild(strip);
367
-
368
- // ---- Arrow button (open popup) ----
369
- const arrowBtn = createElement('button', {
370
- type: 'button',
371
- class: `${baseClass} an-color-arrow`,
372
- title: def.name === 'foreColor'
373
- ? (this.context.locale.toolbar.chooseTextColor || 'Choose text color')
374
- : (this.context.locale.toolbar.chooseHighlightColor || 'Choose highlight color'),
375
- 'aria-haspopup': 'true',
376
- 'aria-expanded': 'false',
377
- });
378
- arrowBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="display:block"><path d="M7 10l5 5 5-5H7z"/></svg>`;
379
-
380
- // ---- Popup ----
381
- const popup = createElement('div', { class: 'an-color-popup' });
382
- popup.style.display = 'none';
383
-
384
- const swatches = createElement('div', { class: 'an-color-swatches' });
385
- const userSwatches = Array.isArray(this.options.colorSwatches) ? this.options.colorSwatches : [];
386
- const allColors = [...new Set([...userSwatches, ...PRESETS])];
387
- allColors.forEach((color) => {
388
- const sw = createElement('div', { class: 'an-color-swatch', title: color, 'data-color': color });
389
- sw.style.background = color;
390
- swatches.appendChild(sw);
391
- });
392
-
393
- const customRow = createElement('div', { class: 'an-color-custom' });
394
- const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', value: currentColor, title: this.context.locale.toolbar.customColor || 'Custom color' }));
395
- const customLabel = createElement('span', {}, [this.context.locale.toolbar.customColor || 'Custom color']);
396
- customRow.appendChild(colorInput);
397
- customRow.appendChild(customLabel);
398
-
399
- popup.appendChild(swatches);
400
- popup.appendChild(customRow);
401
-
402
- // ---- State ----
403
- let isOpen = false;
404
- /** @type {Range|null} saved selection range before popup opens */
405
- let savedRange = null;
406
-
407
- const saveSelection = () => {
408
- const sel = globalThis.getSelection();
409
- savedRange = sel?.rangeCount ? sel.getRangeAt(0).cloneRange() : null;
410
- };
411
-
412
- const restoreSelection = () => {
413
- if (!savedRange) return;
414
- try {
415
- const sel = globalThis.getSelection();
416
- if (!sel) return;
417
- sel.removeAllRanges();
418
- sel.addRange(savedRange);
419
- } catch (_) {
420
- void _; // range may be stale if DOM changed while popup was open
421
- }
422
- };
423
-
424
- const openPopup = () => {
425
- // Close any other open color picker before opening this one
426
- this._colorPickerClosers.forEach((fn) => { if (fn !== closePopup) fn(); });
427
- saveSelection();
428
- isOpen = true;
429
- // Use fixed positioning so the popup escapes any overflow-clipping ancestor
430
- // (notably toolbar scroll mode, where overflow-x:auto coerces overflow-y)
431
- const rect = arrowBtn.getBoundingClientRect();
432
- const popupMinW = 184;
433
- let left = rect.left;
434
- if (left + popupMinW > globalThis.innerWidth) left = rect.right - popupMinW;
435
- popup.style.top = `${rect.bottom + 4}px`;
436
- popup.style.left = `${Math.max(4, left)}px`;
437
- popup.style.display = 'block';
438
- arrowBtn.setAttribute('aria-expanded', 'true');
439
- };
440
-
441
- const closePopup = () => {
442
- isOpen = false;
443
- popup.style.display = 'none';
444
- popup.style.top = '';
445
- popup.style.left = '';
446
- arrowBtn.setAttribute('aria-expanded', 'false');
447
- };
448
-
449
- const applyColor = (color) => {
450
- currentColor = color;
451
- strip.style.background = color;
452
- colorInput.value = color;
453
- restoreSelection();
454
- def.action(this.context, color);
455
- this.context.invoke('editor.afterCommand');
456
- closePopup();
457
- };
458
-
459
- const d1 = on(applyBtn, 'click', (e) => {
460
- e.preventDefault();
461
- restoreSelection();
462
- def.action(this.context, currentColor);
463
- this.context.invoke('editor.afterCommand');
464
- });
465
-
466
- const d2 = on(arrowBtn, 'mousedown', (e) => {
467
- // Prevent editor blur so selection is preserved when the popup opens
468
- e.preventDefault();
469
- });
470
-
471
- const d2b = on(arrowBtn, 'click', (e) => {
472
- e.stopPropagation();
473
- if (isOpen) closePopup(); else openPopup();
474
- });
475
-
476
- const d3 = on(swatches, 'mousedown', (e) => {
477
- // Prevent blur before the click handler fires
478
- e.preventDefault();
479
- });
480
-
481
- const d3b = on(swatches, 'click', (e) => {
482
- const sw = /** @type {Element} */ (e.target)?.closest('.an-color-swatch');
483
- if (sw) applyColor(/** @type {HTMLElement} */ (sw).dataset.color);
484
- });
485
-
486
- const d4 = on(colorInput, 'change', (e) => {
487
- applyColor(/** @type {HTMLInputElement} */ (e.target).value);
488
- });
489
-
490
- const d5 = on(document, 'click', (e) => {
491
- // popup is in document.body, not inside wrap — check both
492
- if (isOpen && !wrap.contains(/** @type {Node} */ (e.target)) && !popup.contains(/** @type {Node} */ (e.target))) closePopup();
493
- });
494
-
495
- const d6 = on(popup, 'click', (e) => e.stopPropagation());
496
-
497
- // Close the popup when the viewport scrolls or resizes so the fixed-position
498
- // popup doesn't drift away from the button it belongs to.
499
- const onScrollResize = () => { if (isOpen) closePopup(); };
500
- document.addEventListener('scroll', onScrollResize, { passive: true, capture: true });
501
- globalThis.addEventListener('resize', onScrollResize, { passive: true });
502
-
503
- this._disposers.push(d1, d2, d2b, d3, d3b, d4, d5, d6,
504
- () => document.removeEventListener('scroll', onScrollResize, { capture: true }),
505
- () => globalThis.removeEventListener('resize', onScrollResize),
506
- // Remove popup from body on editor destroy
507
- () => { if (popup.parentNode) popup.remove(); },
508
- );
509
-
510
- // Register this popup's closer so other color pickers can close it
511
- this._colorPickerClosers.push(closePopup);
512
- this._disposers.push(() => {
513
- const idx = this._colorPickerClosers.indexOf(closePopup);
514
- if (idx !== -1) this._colorPickerClosers.splice(idx, 1);
515
- });
516
-
517
- // Append popup to document.body so it escapes all overflow-clipping and
518
- // contain:layout ancestors (contain:layout makes the container a fixed-pos
519
- // containing block per the CSS Contain spec, breaking viewport coordinates).
520
- wrap.appendChild(applyBtn);
521
- wrap.appendChild(arrowBtn);
522
- document.body.appendChild(popup);
523
- return /** @type {HTMLDivElement} */ (wrap);
524
- }
525
-
526
- /**
527
- * Creates a <select> dropdown for font-family (or similar) options.
528
- * @param {import('./Buttons.js').DropdownDef} def
529
- * @returns {HTMLSelectElement}
530
- */
531
- _createSelect(def) {
532
- const items = (def.name === 'fontFamily')
533
- ? (this.options.fontFamilies || [])
534
- : (def.items || []);
535
-
536
- const cls = def.selectClass ? `an-select ${def.selectClass}` : 'an-select';
537
- const select = createElement('select', {
538
- class: cls,
539
- title: this.context.locale.toolbar[def.name] || def.tooltip || '',
540
- 'data-btn': def.name,
541
- 'aria-label': this.context.locale.toolbar[def.name] || def.tooltip || def.name,
542
- });
543
-
544
- // Blank "placeholder" option (non-selectable header)
545
- const placeholderText = this.context.locale.toolbar[def.name + 'Placeholder'] || def.placeholder || 'Font';
546
- const placeholder = createElement('option', { value: '', disabled: '', hidden: '' }, [placeholderText]);
547
- select.appendChild(placeholder);
548
-
549
- items.forEach((item) => {
550
- const value = (typeof item === 'object') ? item.value : item;
551
- let label;
552
- if (typeof item !== 'object') {
553
- label = item;
554
- } else if (def.name === 'paragraphStyle') {
555
- label = this.context.locale.toolbar.paragraphItems?.[item.value] || item.label;
556
- } else {
557
- label = item.label;
558
- }
559
- const isHeader = (typeof item === 'object') && !!item.disabled;
560
- const attrs = { value };
561
- if (isHeader) attrs.disabled = '';
562
- const opt = createElement('option', attrs, [label]);
563
- // Only apply fontFamily face preview on real (non-header) entries
564
- if (def.name === 'fontFamily' && !isHeader) opt.style.fontFamily = value;
565
- select.appendChild(opt);
566
- });
567
-
568
- // Save the editor selection when the user starts interacting with the
569
- // dropdown (mousedown fires before the editor loses focus). When the
570
- // change handler runs, focus has moved to the <select>; we restore the
571
- // saved range so execCommand / fontSize() act on the intended text.
572
- /** @type {Range|null} */
573
- let _savedRange = null;
574
- const dMousedown = on(select, 'mousedown', () => {
575
- const sel = globalThis.getSelection();
576
- _savedRange = sel?.rangeCount ? sel.getRangeAt(0).cloneRange() : null;
577
- });
578
-
579
- const disposer = on(select, 'change', (e) => {
580
- const value = /** @type {HTMLSelectElement} */ (e.target).value;
581
- const selectedOpt = /** @type {HTMLSelectElement} */ (e.target).options[/** @type {HTMLSelectElement} */ (e.target).selectedIndex];
582
- if (!value || selectedOpt.disabled) return;
583
- this.context.invoke('editor.focus');
584
- // Restore selection saved on mousedown so the action targets the correct text.
585
- if (_savedRange) {
586
- try {
587
- const sel = globalThis.getSelection();
588
- if (sel) { sel.removeAllRanges(); sel.addRange(_savedRange); }
589
- } catch (_) { void _; /* range may be stale if DOM changed */ }
590
- }
591
- def.action(this.context, value);
592
- this.context.invoke('editor.afterCommand');
593
- });
594
-
595
- this._disposers.push(dMousedown, disposer);
596
- return /** @type {HTMLSelectElement} */ (select);
597
- }
598
-
599
- /**
600
- * @param {import('./Buttons.js').ButtonDef} btnDef
601
- * @returns {HTMLButtonElement}
602
- */
603
- _createButton(btnDef) {
604
- // Determine classes based on whether the consumer wants Bootstrap styling
605
- const useBootstrap = !!this.options.useBootstrap;
606
- const baseClass = useBootstrap ? (this.options.toolbarButtonClass || 'btn btn-sm btn-light') : `an-btn`;
607
- const extra = btnDef.className ? ` ${btnDef.className}` : '';
608
- const classAttr = `${baseClass}${extra}`;
609
-
610
- const btn = createElement('button', {
611
- type: 'button',
612
- class: classAttr,
613
- title: this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || '',
614
- 'data-btn': btnDef.name,
615
- 'aria-label': this.context.locale.toolbar[btnDef.name] || btnDef.tooltip || btnDef.name,
616
- // A button that reports an active state is a toggle, so its state has to
617
- // be exposed to assistive tech and not only through the `.active` class.
618
- ...(typeof btnDef.isActive === 'function' ? { 'aria-pressed': 'false' } : {}),
619
- });
620
-
621
- // Render icon: prefer FontAwesome if enabled; otherwise fall back to SVG or text.
622
- const faPrefix = this.options.fontAwesomeClass || 'fas';
623
- const useFaNow = this._faReady;
624
- if (useFaNow) {
625
- const faName = _FA_MAP.get(btnDef.icon) || _FA_MAP.get(btnDef.name) || null;
626
- if (faName) {
627
- btn.innerHTML = `<i class="${faPrefix} ${faName}" aria-hidden="true"></i>`;
628
- } else if (_SVG_MAP.has(btnDef.icon)) {
629
- btn.innerHTML = _SVG_MAP.get(btnDef.icon);
630
- } else {
631
- btn.textContent = btnDef.icon || btnDef.name;
632
- }
633
- } else if (_SVG_MAP.has(btnDef.icon)) {
634
- // FontAwesome absent: use SVG fallback when available
635
- btn.innerHTML = _SVG_MAP.get(btnDef.icon);
636
- } else if (_SVG_MAP.has(btnDef.name)) {
637
- btn.innerHTML = _SVG_MAP.get(btnDef.name);
638
- } else {
639
- btn.textContent = btnDef.icon || btnDef.name;
640
- }
641
-
642
- const disposer = on(btn, 'click', (event) => {
643
- event.preventDefault();
644
- // Restore focus to the editor before executing the action
645
- this.context.invoke('editor.focus');
646
- btnDef.action(this.context);
647
- this.context.invoke('editor.afterCommand');
648
- this.refresh();
649
- });
650
-
651
- this._disposers.push(disposer);
652
- return /** @type {HTMLButtonElement} */ (btn);
653
- }
654
-
655
- // ---------------------------------------------------------------------------
656
- // Keyboard navigation — WAI-ARIA toolbar pattern
657
- // ---------------------------------------------------------------------------
658
-
659
- /**
660
- * Every control the toolbar contains, in visual order. Disabled ones are
661
- * included so their tabindex can be cleared; `_navigable()` filters them out.
662
- * @returns {HTMLElement[]}
663
- */
664
- _controls() {
665
- if (!this.el) return [];
666
- return /** @type {HTMLElement[]} */ (Array.from(this.el.querySelectorAll('button, select')));
667
- }
668
-
669
- /** @returns {HTMLElement[]} controls that can actually receive focus */
670
- _navigable() {
671
- return this._controls().filter(
672
- (el) => !(/** @type {HTMLButtonElement} */ (el).disabled),
673
- );
674
- }
675
-
676
- /**
677
- * Enforces the roving-tabindex invariant: exactly one control sits in the tab
678
- * order and the rest are reached with arrow keys. The default toolbar renders
679
- * 39 controls, so without this a keyboard user pressed Tab 39 times to get
680
- * past the toolbar and into the editable area.
681
- *
682
- * @param {HTMLElement} [focused] - control that should own the tab stop;
683
- * omitted on refresh, where the user's current position is preserved.
684
- */
685
- _syncRovingTabindex(focused) {
686
- const all = this._controls();
687
- const navigable = this._navigable();
688
- if (!navigable.length) return;
689
- const active = (focused && navigable.includes(focused))
690
- ? focused
691
- : navigable.find((el) => el.getAttribute('tabindex') === '0') || navigable[0];
692
- all.forEach((el) => el.setAttribute('tabindex', el === active ? '0' : '-1'));
693
- }
694
-
695
- /** Wires arrow-key navigation. Called on build and again after rebuild(). */
696
- _initRovingFocus() {
697
- if (!this.el) return;
698
- this._syncRovingTabindex();
699
- this._disposers.push(
700
- on(this.el, 'keydown', (e) => this._onToolbarKeydown(/** @type {KeyboardEvent} */ (e))),
701
- // Clicking or programmatically focusing a control moves the tab stop with
702
- // it, so Tab always leaves from wherever the user actually is.
703
- on(this.el, 'focusin', (e) => {
704
- const el = /** @type {Element} */ (e.target)?.closest?.('button, select');
705
- if (el) this._syncRovingTabindex(/** @type {HTMLElement} */ (el));
706
- }),
707
- );
708
- }
709
-
710
- /**
711
- * Home/End jump to the ends; Left/Right step between controls and wrap.
712
- * Up/Down are deliberately left alone so `<select>` controls keep their
713
- * native value-changing behaviour.
714
- * @param {KeyboardEvent} event
715
- */
716
- _onToolbarKeydown(event) {
717
- if (!['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(event.key)) return;
718
- const controls = this._navigable();
719
- const current = /** @type {HTMLElement|null} */ (
720
- /** @type {Element} */ (event.target)?.closest?.('button, select')
721
- );
722
- const idx = current ? controls.indexOf(current) : -1;
723
- if (idx === -1) return;
724
-
725
- let next;
726
- if (event.key === 'Home') {
727
- next = controls[0];
728
- } else if (event.key === 'End') {
729
- next = controls.at(-1);
730
- } else {
731
- // In RTL the arrow that points at the next control is the left one.
732
- const rtl = this.options.direction === 'rtl';
733
- const forward = (event.key === 'ArrowRight') !== rtl;
734
- next = controls[(idx + (forward ? 1 : -1) + controls.length) % controls.length];
735
- }
736
- if (!next) return;
737
- event.preventDefault();
738
- this._syncRovingTabindex(next);
739
- next.focus();
740
- }
741
-
742
- // ---------------------------------------------------------------------------
743
- // FontAwesome detection (run once at initialize time)
744
- // ---------------------------------------------------------------------------
745
-
746
- _detectFontAwesome() {
747
- if (!this.options.useFontAwesome) return false;
748
- // Return cached result when available. This ensures that a later-initialised
749
- // toolbar sees the same detection state as the first one — even if IconDialog
750
- // has since injected its own FA <link> into <head> for the icon-picker UI.
751
- if (_faPageLevelReady !== null) return _faPageLevelReady;
752
- if (document.querySelector('.fa, .fas, .far, .fal, .fab, .fa-solid')) {
753
- _faPageLevelReady = true;
754
- return true;
755
- }
756
- // Exclude the editor-self-injected link (id='an-fontawesome-css') so it doesn't
757
- // count as "the host page loaded FA" for toolbar icon rendering purposes.
758
- const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
759
- .filter((l) => l.id !== 'an-fontawesome-css')
760
- .map((l) => /** @type {HTMLLinkElement} */ (l).href || '').join(' ');
761
- _faPageLevelReady = /fontawesome|font-awesome|use\.fontawesome|all\.css/.test(links);
762
- return _faPageLevelReady;
763
- }
764
-
765
- // ---------------------------------------------------------------------------
766
- // State refresh (active / disabled states)
767
- // ---------------------------------------------------------------------------
768
-
769
- refresh() {
770
- // Debounce via rAF — multiple rapid calls (e.g. afterCommand + button click)
771
- // collapse into a single update per animation frame.
772
- if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
773
- this._refreshRaf = requestAnimationFrame(() => {
774
- this._refreshRaf = null;
775
- this._doRefresh();
776
- });
777
- }
778
-
779
- _doRefresh() {
780
- if (!this.el) return;
781
- const btnMap = this._btnMap || new Map();
782
-
783
- // Sync button active states
784
- this.el.querySelectorAll('button[data-btn]').forEach((btn) => {
785
- const def = btnMap.get(/** @type {HTMLElement} */ (btn).dataset.btn);
786
- if (def && typeof def.isActive === 'function') {
787
- const active = !!def.isActive(this.context);
788
- btn.classList.toggle('active', active);
789
- btn.setAttribute('aria-pressed', String(active));
790
- }
791
- if (def && typeof def.isDisabled === 'function') {
792
- /** @type {HTMLButtonElement} */ (btn).disabled = !!def.isDisabled(this.context);
793
- }
794
- });
795
-
796
- // A button disabled just now must not be the one holding the tab stop.
797
- this._syncRovingTabindex();
798
-
799
- // Sync select dropdowns (e.g. font family) with current cursor position
800
- this.el.querySelectorAll('select[data-btn]').forEach((select) => {
801
- const def = btnMap.get(/** @type {HTMLElement} */ (select).dataset.btn);
802
- if (!def || typeof def.getValue !== 'function') return;
803
- // queryCommandValue returns the font name, possibly quoted — strip quotes
804
- let raw = (def.getValue(this.context) || '').replace(/["']/g, '').trim();
805
- // Fallback: when no selection/font set, use the configured default font
806
- if (!raw) {
807
- raw = this.options.defaultFontFamily
808
- || this.options.fontFamilies?.[0]
809
- || '';
810
- }
811
- // Try to match against available options (case-insensitive)
812
- const sel = /** @type {HTMLSelectElement} */ (select);
813
- const matched = Array.from(sel.options).find(
814
- (opt) => opt.value?.toLowerCase() === raw.toLowerCase()
815
- );
816
- sel.value = matched ? matched.value : '';
817
- });
818
- }
819
-
820
- /**
821
- * Shows the toolbar.
822
- */
823
- show() {
824
- if (this.el) this.el.style.display = '';
825
- }
826
-
827
- /**
828
- * Hides the toolbar.
829
- */
830
- hide() {
831
- if (this.el) this.el.style.display = 'none';
832
- }
833
-
834
- /**
835
- * Tears down and re-renders the toolbar in-place.
836
- * Call after registering new buttons post-create via context.use(plugin)
837
- * or AutumnNote.registerButton() to make them appear in the toolbar.
838
- */
839
- rebuild() {
840
- if (this._refreshRaf) { cancelAnimationFrame(this._refreshRaf); this._refreshRaf = null; }
841
- this._disposers.forEach((d) => d());
842
- this._disposers = [];
843
- if (this.el) this.el.innerHTML = '';
844
- this._faReady = this._detectFontAwesome();
845
- this._buildButtons();
846
- // rebuild() cleared the disposers, taking the keydown/focusin listeners
847
- // with them — re-arm navigation over the new controls.
848
- this._initRovingFocus();
849
- this._btnMap = new Map(
850
- (this.options.toolbar || []).flat()
851
- .map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]),
852
- );
853
- this.refresh();
854
- }
855
- }