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.
- package/README.md +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +661 -798
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +13 -4
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -25
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -231
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -312
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1521
- package/src/js/module/Toolbar.js +0 -750
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- 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
|
-
}
|