autumnnote 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.css +1 -1
  4. package/dist/autumnnote.es.js +661 -798
  5. package/dist/autumnnote.es.js.map +1 -1
  6. package/dist/autumnnote.min.js +20 -20
  7. package/dist/autumnnote.umd.js +20 -20
  8. package/dist/autumnnote.umd.js.map +1 -1
  9. package/dist/icon-data-V0Xqv-wX.js +255 -0
  10. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  11. package/package.json +13 -4
  12. package/types/index.d.ts +8 -0
  13. package/src/js/Context.js +0 -854
  14. package/src/js/core/detectLang.js +0 -98
  15. package/src/js/core/dom.js +0 -372
  16. package/src/js/core/env.js +0 -25
  17. package/src/js/core/key.js +0 -66
  18. package/src/js/core/lists.js +0 -121
  19. package/src/js/core/markdown.js +0 -695
  20. package/src/js/core/range.js +0 -194
  21. package/src/js/core/sanitise.js +0 -231
  22. package/src/js/editing/History.js +0 -266
  23. package/src/js/editing/Style.js +0 -812
  24. package/src/js/editing/Table.js +0 -105
  25. package/src/js/editing/Typing.js +0 -397
  26. package/src/js/index.js +0 -193
  27. package/src/js/index.umd.js +0 -17
  28. package/src/js/module/AutoSaveRestore.js +0 -125
  29. package/src/js/module/BaseDialog.js +0 -133
  30. package/src/js/module/BaseMediaTooltip.js +0 -142
  31. package/src/js/module/BaseResizer.js +0 -312
  32. package/src/js/module/BubbleToolbar.js +0 -483
  33. package/src/js/module/Buttons.js +0 -399
  34. package/src/js/module/Clipboard.js +0 -579
  35. package/src/js/module/CodeTooltip.js +0 -493
  36. package/src/js/module/Codeview.js +0 -125
  37. package/src/js/module/ContextMenu.js +0 -621
  38. package/src/js/module/Editor.js +0 -747
  39. package/src/js/module/EmojiDialog.js +0 -254
  40. package/src/js/module/FindReplace.js +0 -512
  41. package/src/js/module/Fullscreen.js +0 -80
  42. package/src/js/module/IconDialog.js +0 -618
  43. package/src/js/module/ImageCropOverlay.js +0 -586
  44. package/src/js/module/ImageDialog.js +0 -193
  45. package/src/js/module/ImageResizer.js +0 -42
  46. package/src/js/module/ImageTooltip.js +0 -285
  47. package/src/js/module/LinkDialog.js +0 -145
  48. package/src/js/module/LinkTooltip.js +0 -250
  49. package/src/js/module/MarkdownShortcuts.js +0 -250
  50. package/src/js/module/Mention.js +0 -365
  51. package/src/js/module/Placeholder.js +0 -51
  52. package/src/js/module/ShortcutsDialog.js +0 -111
  53. package/src/js/module/SlashMenu.js +0 -376
  54. package/src/js/module/Statusbar.js +0 -246
  55. package/src/js/module/TableTooltip.js +0 -1521
  56. package/src/js/module/Toolbar.js +0 -750
  57. package/src/js/module/VideoDialog.js +0 -193
  58. package/src/js/module/VideoResizer.js +0 -66
  59. package/src/js/module/VideoTooltip.js +0 -248
  60. package/src/js/module/emoji-data.js +0 -496
  61. package/src/js/renderer.js +0 -120
  62. package/src/js/settings.js +0 -214
  63. package/src/styles/_variables.scss +0 -48
  64. package/src/styles/autumnnote.scss +0 -2866
@@ -1,483 +0,0 @@
1
- /**
2
- * BubbleToolbar.js — A mini floating toolbar that appears above the current
3
- * text selection, allowing quick access to common formatting actions without
4
- * reaching for the main toolbar.
5
- *
6
- * Activated when `bubbleToolbar: true`. The set of visible buttons is
7
- * controlled by `bubbleToolbarItems` (array of button name strings).
8
- *
9
- * Built-in names: 'bold', 'italic', 'underline', 'strikethrough',
10
- * 'link', 'foreColor', 'hiliteColor', 'removeFormat', 'inlineCode'.
11
- * Any name not found in the built-in map is silently ignored.
12
- */
13
-
14
- import { on } from '../core/dom.js';
15
-
16
- const COLOR_PRESETS = [
17
- '#000000', '#434343', '#666666', '#999999', '#b7b7b7', '#cccccc', '#efefef', '#ffffff',
18
- '#ff0000', '#ff9900', '#ffff00', '#00ff00', '#00ffff', '#4a86e8', '#9900ff', '#ff00ff',
19
- '#f4cccc', '#fce5cd', '#fff2cc', '#d9ead3', '#d0e0e3', '#c9daf8', '#d9d2e9', '#ead1dc',
20
- ];
21
-
22
- // Minimal SVG icon set — only what the bubble toolbar needs.
23
- const _S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
24
- const _svg = (p) =>
25
- `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" ${_S} style="display:block">${p}</svg>`;
26
-
27
- const _ICONS = {
28
- bold: _svg('<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"/>'),
29
- italic: _svg('<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"/>'),
30
- underline: _svg('<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"/>'),
31
- strikethrough: _svg('<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"/>'),
32
- link: _svg('<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"/>'),
33
- foreColor: _svg('<path d="M4 20L12 4L20 20"/><line x1="7.5" y1="14" x2="16.5" y2="14"/>'),
34
- hiliteColor: _svg('<path d="M3 21v-4l9-9 4 4-9 9z"/><path d="M12 8l4 4"/><line x1="3" y1="21" x2="21" y2="21"/>'),
35
- removeFormat: _svg('<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"/>'),
36
- inlineCode: _svg('<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"/>'),
37
- };
38
-
39
- // Color buttons are handled via _openColorPicker; not in _ACTIONS.
40
- const _ACTIONS = {
41
- bold: (ctx) => ctx.invoke('editor.bold'),
42
- italic: (ctx) => ctx.invoke('editor.italic'),
43
- underline: (ctx) => ctx.invoke('editor.underline'),
44
- strikethrough: (ctx) => ctx.invoke('editor.strikethrough'),
45
- link: (ctx) => ctx.invoke('linkDialog.show'),
46
- removeFormat: (ctx) => {
47
- const editable = ctx.layoutInfo?.editable;
48
- if (!editable) return;
49
- editable.focus();
50
- document.execCommand('removeFormat');
51
- // Also strip inline style attributes which execCommand('removeFormat') misses
52
- const sel = globalThis.getSelection();
53
- if (sel?.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
54
- const range = sel.getRangeAt(0);
55
- const ancestor = range.commonAncestorContainer;
56
- const root = /** @type {Element|null} */ (ancestor.nodeType === 1 ? ancestor : ancestor.parentElement);
57
- if (root) {
58
- const candidates = [root, ...root.querySelectorAll('[style]')];
59
- for (const el of candidates) {
60
- if (el.hasAttribute('style') && range.intersectsNode(el)) {
61
- el.removeAttribute('style');
62
- }
63
- }
64
- }
65
- }
66
- ctx.invoke('editor.afterCommand');
67
- },
68
- inlineCode: (ctx) => ctx.invoke('editor.inlineCode'),
69
- };
70
-
71
- const _ACTIVE = {
72
- bold: () => document.queryCommandState('bold'),
73
- italic: () => document.queryCommandState('italic'),
74
- underline: () => document.queryCommandState('underline'),
75
- strikethrough: () => document.queryCommandState('strikeThrough'),
76
- };
77
-
78
- const _COLOR_TYPES = { foreColor: true, hiliteColor: true };
79
-
80
- export class BubbleToolbar {
81
- /** @param {import('../Context.js').Context} context */
82
- constructor(context) {
83
- this.context = context;
84
- this.options = context.options;
85
-
86
- /** @type {HTMLElement|null} */
87
- this._el = null;
88
- /** @type {HTMLElement|null} */
89
- this._picker = null;
90
- this._pickerType = null; // 'foreColor' | 'hiliteColor'
91
- this._savedRange = null;
92
- this._visible = false;
93
- this._rafId = null;
94
- this._contextMenuOpen = false;
95
- this._disposers = [];
96
-
97
- this._onSelectionChange = this._onSelectionChange.bind(this);
98
- this._onMousedown = this._onMousedown.bind(this);
99
- this._onKeydown = this._onKeydown.bind(this);
100
- this._onContextMenu = this._onContextMenu.bind(this);
101
- }
102
-
103
- initialize() {
104
- if (!this.options.bubbleToolbar) return this;
105
- this._build();
106
- this._buildColorPicker();
107
- const d1 = on(document, 'selectionchange', this._onSelectionChange);
108
- const d2 = on(document, 'mousedown', this._onMousedown);
109
- const d3 = on(document, 'keydown', this._onKeydown);
110
- const d4 = on(document, 'contextmenu', this._onContextMenu);
111
- const d5 = this.context.on('contextMenu:show', () => {
112
- this._contextMenuOpen = true;
113
- this._hide();
114
- });
115
- const d6 = this.context.on('contextMenu:hide', () => {
116
- this._contextMenuOpen = false;
117
- });
118
- const d7 = on(globalThis, 'scroll', () => this._hide(), { passive: true });
119
- const d8 = on(globalThis, 'resize', () => this._hide(), { passive: true });
120
- this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8);
121
- return this;
122
- }
123
-
124
- destroy() {
125
- this._el?.remove();
126
- this._el = null;
127
- this._picker?.remove();
128
- this._picker = null;
129
- this._disposers.forEach((d) => d());
130
- this._disposers = [];
131
- cancelAnimationFrame(this._rafId);
132
- }
133
-
134
- // ---------------------------------------------------------------------------
135
- // Build
136
- // ---------------------------------------------------------------------------
137
-
138
- _build() {
139
- const el = document.createElement('div');
140
- el.className = 'an-bubble-toolbar';
141
- el.setAttribute('role', 'toolbar');
142
- el.setAttribute('aria-label', 'Formatting');
143
-
144
- const items = this.options.bubbleToolbarItems || ['bold', 'italic', 'underline', 'link', 'foreColor', 'hiliteColor', 'removeFormat'];
145
- for (const name of items) {
146
- if (!_ICONS[name]) continue;
147
- const btn = document.createElement('button');
148
- btn.type = 'button';
149
- btn.dataset.name = name;
150
- btn.setAttribute('aria-label', name);
151
-
152
- if (_COLOR_TYPES[name]) {
153
- // Color button: SVG icon stacked above a colored strip, matching ContextMenu style
154
- btn.className = 'an-bubble-btn an-bubble-btn--color';
155
- const iconWrap = document.createElement('span');
156
- iconWrap.className = 'an-bubble-btn-svg';
157
- iconWrap.innerHTML = _ICONS[name];
158
- const strip = document.createElement('span');
159
- strip.className = 'an-bubble-color-strip';
160
- strip.style.background = name === 'foreColor' ? '#000000' : 'transparent';
161
- btn.appendChild(iconWrap);
162
- btn.appendChild(strip);
163
- } else {
164
- btn.className = 'an-bubble-btn';
165
- btn.innerHTML = _ICONS[name];
166
- }
167
-
168
- btn.addEventListener('mousedown', (e) => {
169
- // Prevent mousedown from collapsing selection before click fires
170
- e.preventDefault();
171
- });
172
- btn.addEventListener('click', (e) => {
173
- e.preventDefault();
174
- e.stopPropagation();
175
-
176
- // Color buttons: save selection and open the picker
177
- if (_COLOR_TYPES[name]) {
178
- this._openColorPicker(name, btn);
179
- return;
180
- }
181
-
182
- this.context.invoke('editor.focus');
183
- if (_ACTIONS[name]) _ACTIONS[name](this.context);
184
- this._syncActive();
185
- });
186
- el.appendChild(btn);
187
- }
188
-
189
- document.body.appendChild(el);
190
- this._el = el;
191
- // Cache button references once — avoids querySelectorAll on every selectionchange
192
- this._btnCache = Array.from(el.querySelectorAll('.an-bubble-btn'));
193
- }
194
-
195
- _buildColorPicker() {
196
- const picker = document.createElement('div');
197
- picker.className = 'an-bubble-color-picker';
198
- picker.style.display = 'none';
199
-
200
- // Prevent any mousedown inside the picker from collapsing the editor selection
201
- picker.addEventListener('mousedown', (e) => e.preventDefault());
202
-
203
- const palette = document.createElement('div');
204
- palette.className = 'an-context-color-palette';
205
-
206
- COLOR_PRESETS.forEach((color) => {
207
- const sw = document.createElement('div');
208
- sw.className = 'an-context-color-swatch';
209
- sw.title = color;
210
- sw.style.background = color;
211
- sw.setAttribute('role', 'button');
212
- sw.setAttribute('aria-label', color);
213
- sw.addEventListener('click', (e) => {
214
- e.stopPropagation();
215
- this._applyColor(this._pickerType, color);
216
- });
217
- palette.appendChild(sw);
218
- });
219
-
220
- // "No highlight" swatch — only shown for hiliteColor
221
- const noColorBtn = document.createElement('div');
222
- noColorBtn.className = 'an-context-color-swatch an-context-color-none';
223
- noColorBtn.title = 'No highlight';
224
- noColorBtn.setAttribute('role', 'button');
225
- noColorBtn.setAttribute('aria-label', 'No highlight');
226
- noColorBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="4" y1="4" x2="20" y2="20"/><line x1="20" y1="4" x2="4" y2="20"/></svg>`;
227
- noColorBtn.addEventListener('click', (e) => {
228
- e.stopPropagation();
229
- this._applyColor('hiliteColor', 'transparent');
230
- });
231
-
232
- // Custom color input row
233
- const customRow = document.createElement('div');
234
- customRow.className = 'an-context-color-custom';
235
- const colorInput = document.createElement('input');
236
- colorInput.type = 'color';
237
- colorInput.value = '#000000';
238
- colorInput.title = 'Custom color';
239
- // stopPropagation so the picker's mousedown-preventDefault doesn't block the native color dialog
240
- colorInput.addEventListener('mousedown', (e) => e.stopPropagation());
241
- colorInput.addEventListener('change', () => {
242
- this._applyColor(this._pickerType, colorInput.value);
243
- });
244
- const customLabel = document.createElement('span');
245
- customLabel.textContent = 'Custom…';
246
- customRow.appendChild(colorInput);
247
- customRow.appendChild(customLabel);
248
-
249
- picker.appendChild(palette);
250
- picker.appendChild(customRow);
251
- document.body.appendChild(picker);
252
-
253
- this._picker = picker;
254
- const pickerAny = /** @type {any} */ (picker);
255
- pickerAny._paletteEl = palette;
256
- pickerAny._noColorBtn = noColorBtn;
257
- pickerAny._colorInput = colorInput;
258
- }
259
-
260
- _openColorPicker(type, anchorBtn) {
261
- // Save current selection before the picker might shift focus
262
- const sel = globalThis.getSelection();
263
- if (sel?.rangeCount > 0) {
264
- this._savedRange = sel.getRangeAt(0).cloneRange();
265
- }
266
-
267
- this._pickerType = type;
268
-
269
- // Toggle "No highlight" swatch based on type
270
- const pickerAny = /** @type {any} */ (this._picker);
271
- const palette = pickerAny._paletteEl;
272
- const noColorBtn = pickerAny._noColorBtn;
273
- if (type === 'hiliteColor') {
274
- if (!palette.contains(noColorBtn)) palette.appendChild(noColorBtn);
275
- } else if (palette.contains(noColorBtn)) {
276
- noColorBtn.remove();
277
- }
278
-
279
- // Seed the custom color input
280
- /** @type {any} */ (this._picker)._colorInput.value = type === 'foreColor' ? '#000000' : '#ffff00';
281
-
282
- // Position the picker above the bubble toolbar (not blocking the content below it).
283
- // Fall back to below the toolbar if there's not enough room above.
284
- this._picker.style.display = 'block';
285
- const pw = this._picker.offsetWidth;
286
- const ph = this._picker.offsetHeight;
287
- const toolbarRect = this._el.getBoundingClientRect();
288
-
289
- let top = toolbarRect.top - ph - 6;
290
- if (top < 8) top = toolbarRect.bottom + 6;
291
-
292
- // Align horizontally with the clicked button, clamped to viewport
293
- const btnRect = anchorBtn.getBoundingClientRect();
294
- let left = btnRect.left;
295
- left = Math.max(8, Math.min(left, globalThis.innerWidth - pw - 8));
296
-
297
- this._picker.style.left = `${left}px`;
298
- this._picker.style.top = `${top}px`;
299
- }
300
-
301
- _closeColorPicker() {
302
- if (this._picker) this._picker.style.display = 'none';
303
- this._pickerType = null;
304
- }
305
-
306
- /** Restore the saved selection, apply execCommand, update the color strip, then close the picker. */
307
- _applyColor(type, color) {
308
- const editable = this.context.layoutInfo?.editable;
309
- if (!editable || !this._savedRange) return;
310
-
311
- editable.focus();
312
- const sel = globalThis.getSelection();
313
- sel.removeAllRanges();
314
- try { sel.addRange(this._savedRange.cloneRange()); } catch (_) { void _; return; }
315
-
316
- // Firefox does not support 'hiliteColor'; fall back to 'backColor'
317
- const cmd = type === 'hiliteColor' ? 'hiliteColor' : type;
318
- if (!document.execCommand(cmd, false, color) && cmd === 'hiliteColor') {
319
- document.execCommand('backColor', false, color);
320
- }
321
- this.context.invoke('editor.afterCommand');
322
-
323
- // Update the color strip on the corresponding button
324
- const name = type === 'hiliteColor' ? 'hiliteColor' : 'foreColor';
325
- const btn = this._el?.querySelector(`[data-name="${name}"]`);
326
- const strip = btn?.querySelector('.an-bubble-color-strip');
327
- if (strip) /** @type {HTMLElement} */ (strip).style.background = color === 'transparent' ? 'transparent' : color;
328
-
329
- this._closeColorPicker();
330
- this._syncActive();
331
- }
332
-
333
- // ---------------------------------------------------------------------------
334
- // Show / hide
335
- // ---------------------------------------------------------------------------
336
-
337
- _show(rect) {
338
- if (!this._el) return;
339
- const el = this._el;
340
-
341
- // Measure while invisible — position:fixed so no scroll offset needed
342
- el.style.visibility = 'hidden';
343
- el.style.display = 'flex';
344
-
345
- const bw = el.offsetWidth;
346
- const bh = el.offsetHeight;
347
- const gap = 8;
348
-
349
- // Center horizontally above the selection; flip below if no room above
350
- let left = rect.left + rect.width / 2 - bw / 2;
351
- let top = rect.top - bh - gap;
352
-
353
- left = Math.max(8, Math.min(left, globalThis.innerWidth - bw - 8));
354
-
355
- if (top < 8) {
356
- top = rect.bottom + gap;
357
- }
358
-
359
- // If the Table Tooltip is visible, avoid overlapping it by flipping below the selection.
360
- // The Table Tooltip sits above the table, which is the same vertical zone the bubble
361
- // toolbar would normally occupy when text is selected inside a table cell.
362
- const tableTooltipEl = document.querySelector('.an-table-tooltip');
363
- if (tableTooltipEl && /** @type {HTMLElement} */ (tableTooltipEl).style.display !== 'none') {
364
- const ttRect = tableTooltipEl.getBoundingClientRect();
365
- const overlapsVertically = top < ttRect.bottom + gap && top + bh > ttRect.top - gap;
366
- if (overlapsVertically) {
367
- top = rect.bottom + gap;
368
- if (top + bh > globalThis.innerHeight - 8) top = ttRect.bottom + gap;
369
- }
370
- }
371
-
372
- el.style.top = `${top}px`;
373
- el.style.left = `${left}px`;
374
- el.style.visibility = '';
375
-
376
- this._syncActive();
377
- this._syncColorStrips();
378
- this._visible = true;
379
- }
380
-
381
- _hide() {
382
- if (!this._el) return;
383
- this._el.style.display = 'none';
384
- this._visible = false;
385
- this._closeColorPicker();
386
- }
387
-
388
- _syncActive() {
389
- if (!this._btnCache) return;
390
- this._btnCache.forEach((btn) => {
391
- const activeFn = _ACTIVE[/** @type {HTMLElement} */ (btn).dataset.name];
392
- btn.classList.toggle('an-active', !!(activeFn?.()));
393
- });
394
- }
395
-
396
- /** Read the current selection's color and update the color-strip indicators. */
397
- _syncColorStrips() {
398
- if (!this._el) return;
399
- const sel = globalThis.getSelection();
400
- if (!sel?.rangeCount) return;
401
- let node = sel.getRangeAt(0).startContainer;
402
- if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
403
- if (!node) return;
404
- const cs = globalThis.getComputedStyle(/** @type {Element} */ (node));
405
-
406
- const foreBtn = this._el.querySelector('[data-name="foreColor"]');
407
- const foreStrip = foreBtn?.querySelector('.an-bubble-color-strip');
408
- if (foreStrip) /** @type {HTMLElement} */ (foreStrip).style.background = cs.color || '#000000';
409
-
410
- const hiliteBtn = this._el.querySelector('[data-name="hiliteColor"]');
411
- const hiliteStrip = hiliteBtn?.querySelector('.an-bubble-color-strip');
412
- if (hiliteStrip) {
413
- const bg = cs.backgroundColor;
414
- /** @type {HTMLElement} */ (hiliteStrip).style.background = (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') ? 'transparent' : bg;
415
- }
416
- }
417
-
418
- // ---------------------------------------------------------------------------
419
- // Events
420
- // ---------------------------------------------------------------------------
421
-
422
- _onSelectionChange() {
423
- cancelAnimationFrame(this._rafId);
424
- this._rafId = requestAnimationFrame(() => {
425
- if (this._contextMenuOpen) return;
426
- // Keep toolbar visible while color picker is open
427
- if (this._picker && this._picker.style.display !== 'none') return;
428
-
429
- const sel = globalThis.getSelection();
430
- if (!sel || sel.isCollapsed || !sel.rangeCount) {
431
- this._hide();
432
- return;
433
- }
434
-
435
- const editable = this.context.layoutInfo.editable;
436
- if (!editable.contains(sel.anchorNode)) {
437
- this._hide();
438
- return;
439
- }
440
-
441
- // Don't show in read-only mode
442
- if (this.context.options.readOnly) {
443
- this._hide();
444
- return;
445
- }
446
-
447
- const range = sel.getRangeAt(0);
448
- const rect = range.getBoundingClientRect();
449
- if (!rect || rect.width === 0) {
450
- this._hide();
451
- return;
452
- }
453
-
454
- this._show(rect);
455
- });
456
- }
457
-
458
- _onMousedown(e) {
459
- // Hide when clicking outside both the editable, the bubble toolbar, and the color picker
460
- if (!this._visible) return;
461
- if (this._el?.contains(e.target)) return;
462
- if (this._picker?.contains(e.target)) return;
463
- const editable = this.context.layoutInfo.editable;
464
- if (editable.contains(e.target)) return;
465
- this._hide();
466
- }
467
-
468
- _onKeydown(e) {
469
- if (e.key === 'Escape') {
470
- if (this._picker && this._picker.style.display !== 'none') {
471
- this._closeColorPicker();
472
- } else if (this._visible) {
473
- this._hide();
474
- }
475
- }
476
- }
477
-
478
- _onContextMenu() {
479
- // Hide immediately on right-click — contextMenu:show event will also set the flag,
480
- // but firing here prevents a one-frame flicker before the event propagates.
481
- this._hide();
482
- }
483
- }