autumnnote 1.1.1 → 1.2.1
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 +3 -3
- package/dist/autumnnote.css +67 -3
- package/dist/autumnnote.es.js +205 -15
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +205 -15
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/image/banner.png +0 -0
- package/dist/image/favicon.ico +0 -0
- package/package.json +1 -1
- package/src/js/module/BubbleToolbar.js +219 -15
- package/src/js/module/Toolbar.js +22 -2
- package/src/js/settings.js +1 -1
- package/src/styles/autumnnote.scss +82 -3
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
* controlled by `bubbleToolbarItems` (array of button name strings).
|
|
8
8
|
*
|
|
9
9
|
* Built-in names: 'bold', 'italic', 'underline', 'strikethrough',
|
|
10
|
-
* 'link', 'foreColor', 'removeFormat', 'inlineCode'.
|
|
10
|
+
* 'link', 'foreColor', 'hiliteColor', 'removeFormat', 'inlineCode'.
|
|
11
11
|
* Any name not found in the built-in map is silently ignored.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { on } from '../core/dom.js';
|
|
15
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
|
+
|
|
16
22
|
// Minimal SVG icon set — only what the bubble toolbar needs.
|
|
17
23
|
const _S = 'stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"';
|
|
18
24
|
const _svg = (p) =>
|
|
@@ -25,21 +31,26 @@ const _ICONS = {
|
|
|
25
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"/>'),
|
|
26
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"/>'),
|
|
27
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"/>'),
|
|
28
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"/>'),
|
|
29
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"/>'),
|
|
30
37
|
};
|
|
31
38
|
|
|
39
|
+
// Color buttons are handled via _openColorPicker; not in _ACTIONS.
|
|
32
40
|
const _ACTIONS = {
|
|
33
41
|
bold: (ctx) => ctx.invoke('editor.bold'),
|
|
34
42
|
italic: (ctx) => ctx.invoke('editor.italic'),
|
|
35
43
|
underline: (ctx) => ctx.invoke('editor.underline'),
|
|
36
44
|
strikethrough: (ctx) => ctx.invoke('editor.strikethrough'),
|
|
37
45
|
link: (ctx) => ctx.invoke('linkDialog.show'),
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
removeFormat: (ctx) => {
|
|
47
|
+
// editor.removeFormat does not exist — call execCommand directly
|
|
48
|
+
const editable = ctx.layoutInfo && ctx.layoutInfo.editable;
|
|
49
|
+
if (!editable) return;
|
|
50
|
+
editable.focus();
|
|
51
|
+
document.execCommand('removeFormat');
|
|
52
|
+
ctx.invoke('editor.afterCommand');
|
|
41
53
|
},
|
|
42
|
-
removeFormat: (ctx) => ctx.invoke('editor.removeFormat'),
|
|
43
54
|
inlineCode: (ctx) => ctx.invoke('editor.inlineCode'),
|
|
44
55
|
};
|
|
45
56
|
|
|
@@ -50,6 +61,8 @@ const _ACTIVE = {
|
|
|
50
61
|
strikethrough: () => document.queryCommandState('strikeThrough'),
|
|
51
62
|
};
|
|
52
63
|
|
|
64
|
+
const _COLOR_TYPES = { foreColor: true, hiliteColor: true };
|
|
65
|
+
|
|
53
66
|
export class BubbleToolbar {
|
|
54
67
|
/** @param {import('../Context.js').Context} context */
|
|
55
68
|
constructor(context) {
|
|
@@ -58,6 +71,10 @@ export class BubbleToolbar {
|
|
|
58
71
|
|
|
59
72
|
/** @type {HTMLElement|null} */
|
|
60
73
|
this._el = null;
|
|
74
|
+
/** @type {HTMLElement|null} */
|
|
75
|
+
this._picker = null;
|
|
76
|
+
this._pickerType = null; // 'foreColor' | 'hiliteColor'
|
|
77
|
+
this._savedRange = null;
|
|
61
78
|
this._visible = false;
|
|
62
79
|
this._rafId = null;
|
|
63
80
|
this._contextMenuOpen = false;
|
|
@@ -72,6 +89,7 @@ export class BubbleToolbar {
|
|
|
72
89
|
initialize() {
|
|
73
90
|
if (!this.options.bubbleToolbar) return this;
|
|
74
91
|
this._build();
|
|
92
|
+
this._buildColorPicker();
|
|
75
93
|
const d1 = on(document, 'selectionchange', this._onSelectionChange);
|
|
76
94
|
const d2 = on(document, 'mousedown', this._onMousedown);
|
|
77
95
|
const d3 = on(document, 'keydown', this._onKeydown);
|
|
@@ -90,6 +108,8 @@ export class BubbleToolbar {
|
|
|
90
108
|
destroy() {
|
|
91
109
|
if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
|
|
92
110
|
this._el = null;
|
|
111
|
+
if (this._picker && this._picker.parentNode) this._picker.parentNode.removeChild(this._picker);
|
|
112
|
+
this._picker = null;
|
|
93
113
|
this._disposers.forEach((d) => d());
|
|
94
114
|
this._disposers = [];
|
|
95
115
|
cancelAnimationFrame(this._rafId);
|
|
@@ -105,15 +125,30 @@ export class BubbleToolbar {
|
|
|
105
125
|
el.setAttribute('role', 'toolbar');
|
|
106
126
|
el.setAttribute('aria-label', 'Formatting');
|
|
107
127
|
|
|
108
|
-
const items = this.options.bubbleToolbarItems || ['bold', 'italic', 'underline', 'link', 'foreColor', 'removeFormat'];
|
|
128
|
+
const items = this.options.bubbleToolbarItems || ['bold', 'italic', 'underline', 'link', 'foreColor', 'hiliteColor', 'removeFormat'];
|
|
109
129
|
for (const name of items) {
|
|
110
|
-
if (!_ICONS[name]
|
|
130
|
+
if (!_ICONS[name]) continue;
|
|
111
131
|
const btn = document.createElement('button');
|
|
112
132
|
btn.type = 'button';
|
|
113
|
-
btn.className = 'an-bubble-btn';
|
|
114
133
|
btn.dataset.name = name;
|
|
115
134
|
btn.setAttribute('aria-label', name);
|
|
116
|
-
|
|
135
|
+
|
|
136
|
+
if (_COLOR_TYPES[name]) {
|
|
137
|
+
// Color button: SVG icon stacked above a colored strip, matching ContextMenu style
|
|
138
|
+
btn.className = 'an-bubble-btn an-bubble-btn--color';
|
|
139
|
+
const iconWrap = document.createElement('span');
|
|
140
|
+
iconWrap.className = 'an-bubble-btn-svg';
|
|
141
|
+
iconWrap.innerHTML = _ICONS[name];
|
|
142
|
+
const strip = document.createElement('span');
|
|
143
|
+
strip.className = 'an-bubble-color-strip';
|
|
144
|
+
strip.style.background = name === 'foreColor' ? '#000000' : 'transparent';
|
|
145
|
+
btn.appendChild(iconWrap);
|
|
146
|
+
btn.appendChild(strip);
|
|
147
|
+
} else {
|
|
148
|
+
btn.className = 'an-bubble-btn';
|
|
149
|
+
btn.innerHTML = _ICONS[name];
|
|
150
|
+
}
|
|
151
|
+
|
|
117
152
|
btn.addEventListener('mousedown', (e) => {
|
|
118
153
|
// Prevent mousedown from collapsing selection before click fires
|
|
119
154
|
e.preventDefault();
|
|
@@ -121,9 +156,15 @@ export class BubbleToolbar {
|
|
|
121
156
|
btn.addEventListener('click', (e) => {
|
|
122
157
|
e.preventDefault();
|
|
123
158
|
e.stopPropagation();
|
|
159
|
+
|
|
160
|
+
// Color buttons: save selection and open the picker
|
|
161
|
+
if (_COLOR_TYPES[name]) {
|
|
162
|
+
this._openColorPicker(name, btn);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
124
166
|
this.context.invoke('editor.focus');
|
|
125
|
-
_ACTIONS[name](this.context);
|
|
126
|
-
this.context.invoke('editor.afterCommand');
|
|
167
|
+
if (_ACTIONS[name]) _ACTIONS[name](this.context);
|
|
127
168
|
this._syncActive();
|
|
128
169
|
});
|
|
129
170
|
el.appendChild(btn);
|
|
@@ -133,6 +174,138 @@ export class BubbleToolbar {
|
|
|
133
174
|
this._el = el;
|
|
134
175
|
}
|
|
135
176
|
|
|
177
|
+
_buildColorPicker() {
|
|
178
|
+
const picker = document.createElement('div');
|
|
179
|
+
picker.className = 'an-bubble-color-picker';
|
|
180
|
+
picker.style.display = 'none';
|
|
181
|
+
|
|
182
|
+
// Prevent any mousedown inside the picker from collapsing the editor selection
|
|
183
|
+
picker.addEventListener('mousedown', (e) => e.preventDefault());
|
|
184
|
+
|
|
185
|
+
const palette = document.createElement('div');
|
|
186
|
+
palette.className = 'an-context-color-palette';
|
|
187
|
+
|
|
188
|
+
COLOR_PRESETS.forEach((color) => {
|
|
189
|
+
const sw = document.createElement('div');
|
|
190
|
+
sw.className = 'an-context-color-swatch';
|
|
191
|
+
sw.title = color;
|
|
192
|
+
sw.style.background = color;
|
|
193
|
+
sw.setAttribute('role', 'button');
|
|
194
|
+
sw.setAttribute('aria-label', color);
|
|
195
|
+
sw.addEventListener('click', (e) => {
|
|
196
|
+
e.stopPropagation();
|
|
197
|
+
this._applyColor(this._pickerType, color);
|
|
198
|
+
});
|
|
199
|
+
palette.appendChild(sw);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// "No highlight" swatch — only shown for hiliteColor
|
|
203
|
+
const noColorBtn = document.createElement('div');
|
|
204
|
+
noColorBtn.className = 'an-context-color-swatch an-context-color-none';
|
|
205
|
+
noColorBtn.title = 'No highlight';
|
|
206
|
+
noColorBtn.setAttribute('role', 'button');
|
|
207
|
+
noColorBtn.setAttribute('aria-label', 'No highlight');
|
|
208
|
+
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>`;
|
|
209
|
+
noColorBtn.addEventListener('click', (e) => {
|
|
210
|
+
e.stopPropagation();
|
|
211
|
+
this._applyColor('hiliteColor', 'transparent');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Custom color input row
|
|
215
|
+
const customRow = document.createElement('div');
|
|
216
|
+
customRow.className = 'an-context-color-custom';
|
|
217
|
+
const colorInput = document.createElement('input');
|
|
218
|
+
colorInput.type = 'color';
|
|
219
|
+
colorInput.value = '#000000';
|
|
220
|
+
colorInput.title = 'Custom color';
|
|
221
|
+
// stopPropagation so the picker's mousedown-preventDefault doesn't block the native color dialog
|
|
222
|
+
colorInput.addEventListener('mousedown', (e) => e.stopPropagation());
|
|
223
|
+
colorInput.addEventListener('change', () => {
|
|
224
|
+
this._applyColor(this._pickerType, colorInput.value);
|
|
225
|
+
});
|
|
226
|
+
const customLabel = document.createElement('span');
|
|
227
|
+
customLabel.textContent = 'Custom…';
|
|
228
|
+
customRow.appendChild(colorInput);
|
|
229
|
+
customRow.appendChild(customLabel);
|
|
230
|
+
|
|
231
|
+
picker.appendChild(palette);
|
|
232
|
+
picker.appendChild(customRow);
|
|
233
|
+
document.body.appendChild(picker);
|
|
234
|
+
|
|
235
|
+
this._picker = picker;
|
|
236
|
+
this._picker._paletteEl = palette;
|
|
237
|
+
this._picker._noColorBtn = noColorBtn;
|
|
238
|
+
this._picker._colorInput = colorInput;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_openColorPicker(type, anchorBtn) {
|
|
242
|
+
// Save current selection before the picker might shift focus
|
|
243
|
+
const sel = window.getSelection();
|
|
244
|
+
if (sel && sel.rangeCount > 0) {
|
|
245
|
+
this._savedRange = sel.getRangeAt(0).cloneRange();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this._pickerType = type;
|
|
249
|
+
|
|
250
|
+
// Toggle "No highlight" swatch based on type
|
|
251
|
+
const palette = this._picker._paletteEl;
|
|
252
|
+
const noColorBtn = this._picker._noColorBtn;
|
|
253
|
+
if (type === 'hiliteColor') {
|
|
254
|
+
if (!palette.contains(noColorBtn)) palette.appendChild(noColorBtn);
|
|
255
|
+
} else {
|
|
256
|
+
if (palette.contains(noColorBtn)) palette.removeChild(noColorBtn);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Seed the custom color input
|
|
260
|
+
this._picker._colorInput.value = type === 'foreColor' ? '#000000' : '#ffff00';
|
|
261
|
+
|
|
262
|
+
// Position the picker above the bubble toolbar (not blocking the content below it).
|
|
263
|
+
// Fall back to below the toolbar if there's not enough room above.
|
|
264
|
+
this._picker.style.display = 'block';
|
|
265
|
+
const pw = this._picker.offsetWidth;
|
|
266
|
+
const ph = this._picker.offsetHeight;
|
|
267
|
+
const toolbarRect = this._el.getBoundingClientRect();
|
|
268
|
+
|
|
269
|
+
let top = toolbarRect.top - ph - 6;
|
|
270
|
+
if (top < 8) top = toolbarRect.bottom + 6;
|
|
271
|
+
|
|
272
|
+
// Align horizontally with the clicked button, clamped to viewport
|
|
273
|
+
const btnRect = anchorBtn.getBoundingClientRect();
|
|
274
|
+
let left = btnRect.left;
|
|
275
|
+
left = Math.max(8, Math.min(left, window.innerWidth - pw - 8));
|
|
276
|
+
|
|
277
|
+
this._picker.style.left = `${left}px`;
|
|
278
|
+
this._picker.style.top = `${top}px`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_closeColorPicker() {
|
|
282
|
+
if (this._picker) this._picker.style.display = 'none';
|
|
283
|
+
this._pickerType = null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Restore the saved selection, apply execCommand, update the color strip, then close the picker. */
|
|
287
|
+
_applyColor(type, color) {
|
|
288
|
+
const editable = this.context.layoutInfo && this.context.layoutInfo.editable;
|
|
289
|
+
if (!editable || !this._savedRange) return;
|
|
290
|
+
|
|
291
|
+
editable.focus();
|
|
292
|
+
const sel = window.getSelection();
|
|
293
|
+
sel.removeAllRanges();
|
|
294
|
+
sel.addRange(this._savedRange.cloneRange());
|
|
295
|
+
|
|
296
|
+
document.execCommand(type, false, color);
|
|
297
|
+
this.context.invoke('editor.afterCommand');
|
|
298
|
+
|
|
299
|
+
// Update the color strip on the corresponding button
|
|
300
|
+
const name = type === 'hiliteColor' ? 'hiliteColor' : 'foreColor';
|
|
301
|
+
const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
|
|
302
|
+
const strip = btn && btn.querySelector('.an-bubble-color-strip');
|
|
303
|
+
if (strip) strip.style.background = color === 'transparent' ? 'transparent' : color;
|
|
304
|
+
|
|
305
|
+
this._closeColorPicker();
|
|
306
|
+
this._syncActive();
|
|
307
|
+
}
|
|
308
|
+
|
|
136
309
|
// ---------------------------------------------------------------------------
|
|
137
310
|
// Show / hide
|
|
138
311
|
// ---------------------------------------------------------------------------
|
|
@@ -164,6 +337,7 @@ export class BubbleToolbar {
|
|
|
164
337
|
el.style.visibility = '';
|
|
165
338
|
|
|
166
339
|
this._syncActive();
|
|
340
|
+
this._syncColorStrips();
|
|
167
341
|
this._visible = true;
|
|
168
342
|
}
|
|
169
343
|
|
|
@@ -171,6 +345,7 @@ export class BubbleToolbar {
|
|
|
171
345
|
if (!this._el) return;
|
|
172
346
|
this._el.style.display = 'none';
|
|
173
347
|
this._visible = false;
|
|
348
|
+
this._closeColorPicker();
|
|
174
349
|
}
|
|
175
350
|
|
|
176
351
|
_syncActive() {
|
|
@@ -182,6 +357,28 @@ export class BubbleToolbar {
|
|
|
182
357
|
});
|
|
183
358
|
}
|
|
184
359
|
|
|
360
|
+
/** Read the current selection's color and update the color-strip indicators. */
|
|
361
|
+
_syncColorStrips() {
|
|
362
|
+
if (!this._el) return;
|
|
363
|
+
const sel = window.getSelection();
|
|
364
|
+
if (!sel || !sel.rangeCount) return;
|
|
365
|
+
let node = sel.getRangeAt(0).startContainer;
|
|
366
|
+
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
|
367
|
+
if (!node) return;
|
|
368
|
+
const cs = window.getComputedStyle(node);
|
|
369
|
+
|
|
370
|
+
const foreBtn = this._el.querySelector('[data-name="foreColor"]');
|
|
371
|
+
const foreStrip = foreBtn && foreBtn.querySelector('.an-bubble-color-strip');
|
|
372
|
+
if (foreStrip) foreStrip.style.background = cs.color || '#000000';
|
|
373
|
+
|
|
374
|
+
const hiliteBtn = this._el.querySelector('[data-name="hiliteColor"]');
|
|
375
|
+
const hiliteStrip = hiliteBtn && hiliteBtn.querySelector('.an-bubble-color-strip');
|
|
376
|
+
if (hiliteStrip) {
|
|
377
|
+
const bg = cs.backgroundColor;
|
|
378
|
+
hiliteStrip.style.background = (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') ? 'transparent' : bg;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
185
382
|
// ---------------------------------------------------------------------------
|
|
186
383
|
// Events
|
|
187
384
|
// ---------------------------------------------------------------------------
|
|
@@ -190,6 +387,8 @@ export class BubbleToolbar {
|
|
|
190
387
|
cancelAnimationFrame(this._rafId);
|
|
191
388
|
this._rafId = requestAnimationFrame(() => {
|
|
192
389
|
if (this._contextMenuOpen) return;
|
|
390
|
+
// Keep toolbar visible while color picker is open
|
|
391
|
+
if (this._picker && this._picker.style.display !== 'none') return;
|
|
193
392
|
|
|
194
393
|
const sel = window.getSelection();
|
|
195
394
|
if (!sel || sel.isCollapsed || !sel.rangeCount) {
|
|
@@ -203,7 +402,7 @@ export class BubbleToolbar {
|
|
|
203
402
|
return;
|
|
204
403
|
}
|
|
205
404
|
|
|
206
|
-
// Don't show
|
|
405
|
+
// Don't show in read-only mode
|
|
207
406
|
if (this.context.options.readOnly) {
|
|
208
407
|
this._hide();
|
|
209
408
|
return;
|
|
@@ -221,17 +420,22 @@ export class BubbleToolbar {
|
|
|
221
420
|
}
|
|
222
421
|
|
|
223
422
|
_onMousedown(e) {
|
|
224
|
-
// Hide when clicking outside both the editable and the
|
|
423
|
+
// Hide when clicking outside both the editable, the bubble toolbar, and the color picker
|
|
225
424
|
if (!this._visible) return;
|
|
226
425
|
if (this._el && this._el.contains(e.target)) return;
|
|
426
|
+
if (this._picker && this._picker.contains(e.target)) return;
|
|
227
427
|
const editable = this.context.layoutInfo.editable;
|
|
228
428
|
if (editable.contains(e.target)) return;
|
|
229
429
|
this._hide();
|
|
230
430
|
}
|
|
231
431
|
|
|
232
432
|
_onKeydown(e) {
|
|
233
|
-
if (e.key === 'Escape'
|
|
234
|
-
this.
|
|
433
|
+
if (e.key === 'Escape') {
|
|
434
|
+
if (this._picker && this._picker.style.display !== 'none') {
|
|
435
|
+
this._closeColorPicker();
|
|
436
|
+
} else if (this._visible) {
|
|
437
|
+
this._hide();
|
|
438
|
+
}
|
|
235
439
|
}
|
|
236
440
|
}
|
|
237
441
|
|
package/src/js/module/Toolbar.js
CHANGED
|
@@ -237,7 +237,23 @@ export class Toolbar {
|
|
|
237
237
|
|
|
238
238
|
const openPopup = () => {
|
|
239
239
|
isOpen = true;
|
|
240
|
+
const rect = btn.getBoundingClientRect();
|
|
241
|
+
|
|
242
|
+
// Measure popup dimensions while invisible so we can set the correct
|
|
243
|
+
// position before the browser paints (matches the color-picker pattern).
|
|
244
|
+
popup.style.visibility = 'hidden';
|
|
240
245
|
popup.style.display = 'block';
|
|
246
|
+
const pw = popup.offsetWidth;
|
|
247
|
+
const ph = popup.offsetHeight;
|
|
248
|
+
|
|
249
|
+
let left = rect.left;
|
|
250
|
+
let top = rect.bottom + 4;
|
|
251
|
+
if (left + pw > window.innerWidth - 8) left = Math.max(8, window.innerWidth - pw - 8);
|
|
252
|
+
if (top + ph > window.innerHeight - 8) top = rect.top - ph - 4;
|
|
253
|
+
|
|
254
|
+
popup.style.left = `${left}px`;
|
|
255
|
+
popup.style.top = `${top}px`;
|
|
256
|
+
popup.style.visibility = '';
|
|
241
257
|
btn.setAttribute('aria-expanded', 'true');
|
|
242
258
|
};
|
|
243
259
|
|
|
@@ -273,10 +289,14 @@ export class Toolbar {
|
|
|
273
289
|
|
|
274
290
|
const d5 = on(document, 'click', () => { if (isOpen) closePopup(); });
|
|
275
291
|
|
|
276
|
-
|
|
292
|
+
// Append popup to body so position:fixed is truly viewport-relative,
|
|
293
|
+
// unaffected by any ancestor transform / filter (same pattern as color picker).
|
|
294
|
+
this._disposers.push(d1, d2, d3, d4, d5, () => {
|
|
295
|
+
if (popup.parentNode) popup.parentNode.removeChild(popup);
|
|
296
|
+
});
|
|
277
297
|
|
|
278
298
|
wrap.appendChild(btn);
|
|
279
|
-
|
|
299
|
+
document.body.appendChild(popup);
|
|
280
300
|
return wrap;
|
|
281
301
|
}
|
|
282
302
|
|
package/src/js/settings.js
CHANGED
|
@@ -153,7 +153,7 @@ export const defaultOptions = {
|
|
|
153
153
|
|
|
154
154
|
// Bubble toolbar: show a mini floating toolbar above text selections.
|
|
155
155
|
bubbleToolbar: false,
|
|
156
|
-
bubbleToolbarItems: ['bold', 'italic', 'underline', 'link', 'foreColor', 'removeFormat'],
|
|
156
|
+
bubbleToolbarItems: ['bold', 'italic', 'underline', 'link', 'foreColor', 'hiliteColor', 'removeFormat'],
|
|
157
157
|
|
|
158
158
|
// @mention support. mention.onSearch(query, callback) must be provided to activate.
|
|
159
159
|
// mention.minChars defaults to 0 — dropdown opens immediately on trigger character.
|
|
@@ -108,6 +108,8 @@
|
|
|
108
108
|
background: $an-bg-toolbar;
|
|
109
109
|
border-bottom: 1px solid $an-border;
|
|
110
110
|
user-select: none;
|
|
111
|
+
// Round the top corners to match the container border-radius
|
|
112
|
+
border-radius: $an-radius $an-radius 0 0;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
.an-btn-group {
|
|
@@ -193,9 +195,7 @@
|
|
|
193
195
|
|
|
194
196
|
.an-table-picker-popup {
|
|
195
197
|
display: none;
|
|
196
|
-
position:
|
|
197
|
-
top: calc(100% + 4px);
|
|
198
|
-
left: 0;
|
|
198
|
+
position: fixed; // top/left set dynamically by JS — escapes overflow-clipping ancestors
|
|
199
199
|
background: $an-bg;
|
|
200
200
|
border: 1px solid $an-border;
|
|
201
201
|
border-radius: 6px;
|
|
@@ -761,6 +761,13 @@
|
|
|
761
761
|
font-size: 11px;
|
|
762
762
|
color: $an-muted;
|
|
763
763
|
user-select: none;
|
|
764
|
+
// Round the bottom corners to match the container border-radius
|
|
765
|
+
border-radius: 0 0 $an-radius $an-radius;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// When there is no statusbar, the editable area is the last child — round its bottom corners
|
|
769
|
+
.an-container > .an-editable:last-child {
|
|
770
|
+
border-radius: 0 0 $an-radius $an-radius;
|
|
764
771
|
}
|
|
765
772
|
|
|
766
773
|
.an-status-info {
|
|
@@ -1890,6 +1897,37 @@ $an-video-accent: #8b5cf6; // violet-500
|
|
|
1890
1897
|
}
|
|
1891
1898
|
}
|
|
1892
1899
|
|
|
1900
|
+
// =============================================================================
|
|
1901
|
+
// Responsive — mobile / small-viewport toolbar
|
|
1902
|
+
// =============================================================================
|
|
1903
|
+
//
|
|
1904
|
+
// On screens ≤ 640 px the default flex-wrap:wrap behaviour creates a multi-row
|
|
1905
|
+
// toolbar that pushes the editable area too far down. We switch automatically
|
|
1906
|
+
// to a single horizontally-scrollable row (same as toolbarOverflow:'scroll'),
|
|
1907
|
+
// hide the scrollbar track, and compact the dropdown selects.
|
|
1908
|
+
|
|
1909
|
+
@media (max-width: 640px) {
|
|
1910
|
+
.an-toolbar {
|
|
1911
|
+
flex-wrap: nowrap;
|
|
1912
|
+
overflow-x: auto;
|
|
1913
|
+
// Hide scrollbar visually — users swipe naturally on touch devices
|
|
1914
|
+
scrollbar-width: none;
|
|
1915
|
+
-ms-overflow-style: none;
|
|
1916
|
+
|
|
1917
|
+
&::-webkit-scrollbar { display: none; }
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Compact dropdown selects to save horizontal real-estate on small screens
|
|
1921
|
+
.an-select {
|
|
1922
|
+
min-width: 72px;
|
|
1923
|
+
max-width: 90px;
|
|
1924
|
+
font-size: 11px;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
.an-select-style { min-width: 62px; max-width: 76px; }
|
|
1928
|
+
.an-select-narrow { min-width: 50px; max-width: 66px; }
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1893
1931
|
// =============================================================================
|
|
1894
1932
|
// RTL support
|
|
1895
1933
|
// =============================================================================
|
|
@@ -2117,6 +2155,30 @@ $an-video-accent: #8b5cf6; // violet-500
|
|
|
2117
2155
|
color: $an-primary;
|
|
2118
2156
|
}
|
|
2119
2157
|
|
|
2158
|
+
// Color variant: SVG stacked above a thin color-indicator strip
|
|
2159
|
+
&.an-bubble-btn--color {
|
|
2160
|
+
flex-direction: column;
|
|
2161
|
+
gap: 1px;
|
|
2162
|
+
padding: 4px 0 3px;
|
|
2163
|
+
|
|
2164
|
+
.an-bubble-btn-svg {
|
|
2165
|
+
display: flex;
|
|
2166
|
+
align-items: center;
|
|
2167
|
+
justify-content: center;
|
|
2168
|
+
line-height: 0;
|
|
2169
|
+
|
|
2170
|
+
svg { display: block; }
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
.an-bubble-color-strip {
|
|
2174
|
+
display: block;
|
|
2175
|
+
width: 14px;
|
|
2176
|
+
height: 3px;
|
|
2177
|
+
border-radius: 1px;
|
|
2178
|
+
pointer-events: none;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2120
2182
|
.an-theme-dark & {
|
|
2121
2183
|
color: #cdd6f4;
|
|
2122
2184
|
|
|
@@ -2131,6 +2193,23 @@ $an-video-accent: #8b5cf6; // violet-500
|
|
|
2131
2193
|
}
|
|
2132
2194
|
}
|
|
2133
2195
|
|
|
2196
|
+
// Bubble toolbar — color picker popup
|
|
2197
|
+
.an-bubble-color-picker {
|
|
2198
|
+
position: fixed;
|
|
2199
|
+
z-index: 10041;
|
|
2200
|
+
background: $an-bg;
|
|
2201
|
+
border: 1px solid $an-border;
|
|
2202
|
+
border-radius: 8px;
|
|
2203
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.14);
|
|
2204
|
+
padding: 4px 0;
|
|
2205
|
+
min-width: 160px;
|
|
2206
|
+
|
|
2207
|
+
.an-theme-dark & {
|
|
2208
|
+
background: #24273a;
|
|
2209
|
+
border-color: #3f3f5f;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2134
2213
|
// =============================================================================
|
|
2135
2214
|
// @mention dropdown + chip (#5)
|
|
2136
2215
|
// =============================================================================
|