autumnnote 1.0.0 → 1.0.3
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 +6 -2
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +2366 -1994
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +29 -29
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +3 -3
- package/src/js/core/sanitise.js +43 -0
- package/src/js/editing/History.js +10 -2
- package/src/js/editing/Style.js +65 -4
- package/src/js/editing/Typing.js +243 -37
- package/src/js/index.js +1 -1
- package/src/js/index.umd.js +12 -0
- package/src/js/module/Buttons.js +15 -5
- package/src/js/module/CodeTooltip.js +18 -7
- package/src/js/module/Codeview.js +6 -1
- package/src/js/module/ContextMenu.js +13 -6
- package/src/js/module/Editor.js +70 -2
- package/src/js/module/IconDialog.js +27 -13
- package/src/js/module/ImageResizer.js +22 -3
- package/src/js/module/ImageTooltip.js +37 -10
- package/src/js/module/Placeholder.js +2 -1
- package/src/js/module/Statusbar.js +16 -2
- package/src/js/module/TableTooltip.js +119 -23
- package/src/js/module/Toolbar.js +103 -17
- package/src/js/module/VideoDialog.js +3 -3
- package/src/js/module/VideoResizer.js +22 -3
- package/src/js/module/VideoTooltip.js +73 -0
- package/src/js/renderer.js +5 -0
- package/src/js/settings.js +4 -0
- package/src/styles/autumnnote.scss +61 -31
- package/types/index.d.ts +4 -4
package/src/js/module/Toolbar.js
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { createElement, on } from '../core/dom.js';
|
|
7
7
|
|
|
8
|
+
// Module-level cache for FontAwesome detection.
|
|
9
|
+
// Evaluated once per page load so all Toolbar instances on the same page agree
|
|
10
|
+
// on whether the HOST PAGE included FA — regardless of whether IconDialog later
|
|
11
|
+
// auto-injects its own FA <link> for the icon-picker glyph rendering.
|
|
12
|
+
let _faPageLevelReady = null;
|
|
13
|
+
|
|
8
14
|
export class Toolbar {
|
|
9
15
|
/**
|
|
10
16
|
* @param {import('../Context.js').Context} context
|
|
@@ -16,6 +22,8 @@ export class Toolbar {
|
|
|
16
22
|
this.el = null;
|
|
17
23
|
/** @type {Array<() => void>} disposers */
|
|
18
24
|
this._disposers = [];
|
|
25
|
+
/** @type {Array<() => void>} closers for all open color picker popups */
|
|
26
|
+
this._colorPickerClosers = [];
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
// ---------------------------------------------------------------------------
|
|
@@ -253,9 +261,36 @@ export class Toolbar {
|
|
|
253
261
|
|
|
254
262
|
// ---- State ----
|
|
255
263
|
let isOpen = false;
|
|
264
|
+
/** @type {Range|null} saved selection range before popup opens */
|
|
265
|
+
let savedRange = null;
|
|
266
|
+
|
|
267
|
+
const saveSelection = () => {
|
|
268
|
+
const sel = window.getSelection();
|
|
269
|
+
savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const restoreSelection = () => {
|
|
273
|
+
if (!savedRange) return;
|
|
274
|
+
const sel = window.getSelection();
|
|
275
|
+
if (sel) {
|
|
276
|
+
sel.removeAllRanges();
|
|
277
|
+
sel.addRange(savedRange);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
256
280
|
|
|
257
281
|
const openPopup = () => {
|
|
282
|
+
// Close any other open color picker before opening this one
|
|
283
|
+
this._colorPickerClosers.forEach((fn) => { if (fn !== closePopup) fn(); });
|
|
284
|
+
saveSelection();
|
|
258
285
|
isOpen = true;
|
|
286
|
+
// Use fixed positioning so the popup escapes any overflow-clipping ancestor
|
|
287
|
+
// (notably toolbar scroll mode, where overflow-x:auto coerces overflow-y)
|
|
288
|
+
const rect = arrowBtn.getBoundingClientRect();
|
|
289
|
+
const popupMinW = 184;
|
|
290
|
+
let left = rect.left;
|
|
291
|
+
if (left + popupMinW > window.innerWidth) left = rect.right - popupMinW;
|
|
292
|
+
popup.style.top = `${rect.bottom + 4}px`;
|
|
293
|
+
popup.style.left = `${Math.max(4, left)}px`;
|
|
259
294
|
popup.style.display = 'block';
|
|
260
295
|
arrowBtn.setAttribute('aria-expanded', 'true');
|
|
261
296
|
};
|
|
@@ -263,6 +298,8 @@ export class Toolbar {
|
|
|
263
298
|
const closePopup = () => {
|
|
264
299
|
isOpen = false;
|
|
265
300
|
popup.style.display = 'none';
|
|
301
|
+
popup.style.top = '';
|
|
302
|
+
popup.style.left = '';
|
|
266
303
|
arrowBtn.setAttribute('aria-expanded', 'false');
|
|
267
304
|
};
|
|
268
305
|
|
|
@@ -270,7 +307,7 @@ export class Toolbar {
|
|
|
270
307
|
currentColor = color;
|
|
271
308
|
strip.style.background = color;
|
|
272
309
|
colorInput.value = color;
|
|
273
|
-
|
|
310
|
+
restoreSelection();
|
|
274
311
|
def.action(this.context, color);
|
|
275
312
|
this.context.invoke('editor.afterCommand');
|
|
276
313
|
closePopup();
|
|
@@ -278,17 +315,27 @@ export class Toolbar {
|
|
|
278
315
|
|
|
279
316
|
const d1 = on(applyBtn, 'click', (e) => {
|
|
280
317
|
e.preventDefault();
|
|
281
|
-
|
|
318
|
+
restoreSelection();
|
|
282
319
|
def.action(this.context, currentColor);
|
|
283
320
|
this.context.invoke('editor.afterCommand');
|
|
284
321
|
});
|
|
285
322
|
|
|
286
|
-
const d2 = on(arrowBtn, '
|
|
323
|
+
const d2 = on(arrowBtn, 'mousedown', (e) => {
|
|
324
|
+
// Prevent editor blur so selection is preserved when the popup opens
|
|
325
|
+
e.preventDefault();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const d2b = on(arrowBtn, 'click', (e) => {
|
|
287
329
|
e.stopPropagation();
|
|
288
330
|
if (isOpen) closePopup(); else openPopup();
|
|
289
331
|
});
|
|
290
332
|
|
|
291
|
-
const d3 = on(swatches, '
|
|
333
|
+
const d3 = on(swatches, 'mousedown', (e) => {
|
|
334
|
+
// Prevent blur before the click handler fires
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const d3b = on(swatches, 'click', (e) => {
|
|
292
339
|
const sw = e.target.closest('.an-color-swatch');
|
|
293
340
|
if (sw) applyColor(sw.dataset.color);
|
|
294
341
|
});
|
|
@@ -298,16 +345,38 @@ export class Toolbar {
|
|
|
298
345
|
});
|
|
299
346
|
|
|
300
347
|
const d5 = on(document, 'click', (e) => {
|
|
301
|
-
|
|
348
|
+
// popup is in document.body, not inside wrap — check both
|
|
349
|
+
if (isOpen && !wrap.contains(e.target) && !popup.contains(e.target)) closePopup();
|
|
302
350
|
});
|
|
303
351
|
|
|
304
352
|
const d6 = on(popup, 'click', (e) => e.stopPropagation());
|
|
305
353
|
|
|
306
|
-
|
|
354
|
+
// Close the popup when the viewport scrolls or resizes so the fixed-position
|
|
355
|
+
// popup doesn't drift away from the button it belongs to.
|
|
356
|
+
const onScrollResize = () => { if (isOpen) closePopup(); };
|
|
357
|
+
document.addEventListener('scroll', onScrollResize, { passive: true, capture: true });
|
|
358
|
+
window.addEventListener('resize', onScrollResize, { passive: true });
|
|
359
|
+
|
|
360
|
+
this._disposers.push(d1, d2, d2b, d3, d3b, d4, d5, d6,
|
|
361
|
+
() => document.removeEventListener('scroll', onScrollResize, { capture: true }),
|
|
362
|
+
() => window.removeEventListener('resize', onScrollResize),
|
|
363
|
+
// Remove popup from body on editor destroy
|
|
364
|
+
() => { if (popup.parentNode) popup.parentNode.removeChild(popup); },
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Register this popup's closer so other color pickers can close it
|
|
368
|
+
this._colorPickerClosers.push(closePopup);
|
|
369
|
+
this._disposers.push(() => {
|
|
370
|
+
const idx = this._colorPickerClosers.indexOf(closePopup);
|
|
371
|
+
if (idx !== -1) this._colorPickerClosers.splice(idx, 1);
|
|
372
|
+
});
|
|
307
373
|
|
|
374
|
+
// Append popup to document.body so it escapes all overflow-clipping and
|
|
375
|
+
// contain:layout ancestors (contain:layout makes the container a fixed-pos
|
|
376
|
+
// containing block per the CSS Contain spec, breaking viewport coordinates).
|
|
308
377
|
wrap.appendChild(applyBtn);
|
|
309
378
|
wrap.appendChild(arrowBtn);
|
|
310
|
-
|
|
379
|
+
document.body.appendChild(popup);
|
|
311
380
|
return wrap;
|
|
312
381
|
}
|
|
313
382
|
|
|
@@ -329,22 +398,27 @@ export class Toolbar {
|
|
|
329
398
|
'aria-label': def.tooltip || def.name,
|
|
330
399
|
});
|
|
331
400
|
|
|
332
|
-
// Blank "placeholder" option
|
|
401
|
+
// Blank "placeholder" option (non-selectable header)
|
|
333
402
|
const placeholderText = def.placeholder || 'Font';
|
|
334
|
-
const placeholder = createElement('option', { value: '' }, [placeholderText]);
|
|
403
|
+
const placeholder = createElement('option', { value: '', disabled: '', hidden: '' }, [placeholderText]);
|
|
335
404
|
select.appendChild(placeholder);
|
|
336
405
|
|
|
337
406
|
items.forEach((item) => {
|
|
338
|
-
const value
|
|
339
|
-
const label
|
|
340
|
-
const
|
|
341
|
-
|
|
407
|
+
const value = (typeof item === 'object') ? item.value : item;
|
|
408
|
+
const label = (typeof item === 'object') ? item.label : item;
|
|
409
|
+
const isHeader = (typeof item === 'object') && !!item.disabled;
|
|
410
|
+
const attrs = { value };
|
|
411
|
+
if (isHeader) attrs.disabled = '';
|
|
412
|
+
const opt = createElement('option', attrs, [label]);
|
|
413
|
+
// Only apply fontFamily face preview on real (non-header) entries
|
|
414
|
+
if (def.name === 'fontFamily' && !isHeader) opt.style.fontFamily = value;
|
|
342
415
|
select.appendChild(opt);
|
|
343
416
|
});
|
|
344
417
|
|
|
345
418
|
const disposer = on(select, 'change', (e) => {
|
|
346
419
|
const value = e.target.value;
|
|
347
|
-
|
|
420
|
+
const selectedOpt = e.target.options[e.target.selectedIndex];
|
|
421
|
+
if (!value || selectedOpt.disabled) return;
|
|
348
422
|
this.context.invoke('editor.focus');
|
|
349
423
|
def.action(this.context, value);
|
|
350
424
|
this.context.invoke('editor.afterCommand');
|
|
@@ -502,9 +576,21 @@ export class Toolbar {
|
|
|
502
576
|
|
|
503
577
|
_detectFontAwesome() {
|
|
504
578
|
if (!this.options.useFontAwesome) return false;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
579
|
+
// Return cached result when available. This ensures that a later-initialised
|
|
580
|
+
// toolbar sees the same detection state as the first one — even if IconDialog
|
|
581
|
+
// has since injected its own FA <link> into <head> for the icon-picker UI.
|
|
582
|
+
if (_faPageLevelReady !== null) return _faPageLevelReady;
|
|
583
|
+
if (document.querySelector('.fa, .fas, .far, .fal, .fab, .fa-solid')) {
|
|
584
|
+
_faPageLevelReady = true;
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
// Exclude the editor-self-injected link (id='an-fontawesome-css') so it doesn't
|
|
588
|
+
// count as "the host page loaded FA" for toolbar icon rendering purposes.
|
|
589
|
+
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
590
|
+
.filter((l) => l.id !== 'an-fontawesome-css')
|
|
591
|
+
.map((l) => l.href || '').join(' ');
|
|
592
|
+
_faPageLevelReady = /fontawesome|font-awesome|use\.fontawesome|all\.css/.test(links);
|
|
593
|
+
return _faPageLevelReady;
|
|
508
594
|
}
|
|
509
595
|
|
|
510
596
|
// ---------------------------------------------------------------------------
|
|
@@ -219,7 +219,7 @@ export class VideoDialog {
|
|
|
219
219
|
if (info && (info.type === 'YouTube' || info.type === 'YouTube Shorts' || info.type === 'Vimeo')) {
|
|
220
220
|
const iframeTitle = `${info.type} video player`;
|
|
221
221
|
return (
|
|
222
|
-
`<div class="an-video-wrapper" style="position:relative;display:
|
|
222
|
+
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
223
223
|
`<iframe src="${info.embedUrl}" width="${width}" height="${height}" ` +
|
|
224
224
|
`title="${iframeTitle}" ` +
|
|
225
225
|
`frameborder="0" allowfullscreen ` +
|
|
@@ -233,7 +233,7 @@ export class VideoDialog {
|
|
|
233
233
|
if (info && info.type === 'Direct video') {
|
|
234
234
|
const src = info.embedUrl.replace(/"/g, '%22');
|
|
235
235
|
return (
|
|
236
|
-
`<div class="an-video-wrapper" style="position:relative;display:
|
|
236
|
+
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
237
237
|
`<video src="${src}" width="${width}" height="${height}" controls ` +
|
|
238
238
|
`style="display:block;max-width:100%"></video>` +
|
|
239
239
|
`<div class="an-video-shield"></div>` +
|
|
@@ -253,7 +253,7 @@ export class VideoDialog {
|
|
|
253
253
|
|
|
254
254
|
const escapedSrc = safeSrc.replace(/"/g, '%22');
|
|
255
255
|
return (
|
|
256
|
-
`<div class="an-video-wrapper" style="position:relative;display:
|
|
256
|
+
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
257
257
|
`<video src="${escapedSrc}" width="${width}" height="${height}" controls ` +
|
|
258
258
|
`style="display:block;max-width:100%"></video>` +
|
|
259
259
|
`<div class="an-video-shield"></div>` +
|
|
@@ -21,11 +21,14 @@ export class VideoResizer {
|
|
|
21
21
|
this._activeWrapper = null;
|
|
22
22
|
this._overlay = null;
|
|
23
23
|
this._disposers = [];
|
|
24
|
+
this._positionRaf = null;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
initialize() {
|
|
27
28
|
this._overlay = this._buildOverlay();
|
|
28
|
-
|
|
29
|
+
const container = this.context.layoutInfo.editable.closest('.an-container') || document.body;
|
|
30
|
+
container.appendChild(this._overlay);
|
|
31
|
+
this._container = container;
|
|
29
32
|
|
|
30
33
|
const editable = this.context.layoutInfo.editable;
|
|
31
34
|
|
|
@@ -51,6 +54,10 @@ export class VideoResizer {
|
|
|
51
54
|
this._dragDisposers.forEach((d) => d());
|
|
52
55
|
this._dragDisposers = null;
|
|
53
56
|
}
|
|
57
|
+
if (this._positionRaf) {
|
|
58
|
+
cancelAnimationFrame(this._positionRaf);
|
|
59
|
+
this._positionRaf = null;
|
|
60
|
+
}
|
|
54
61
|
this._deselect();
|
|
55
62
|
if (this._overlay && this._overlay.parentNode) {
|
|
56
63
|
this._overlay.parentNode.removeChild(this._overlay);
|
|
@@ -152,10 +159,22 @@ export class VideoResizer {
|
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
_updateOverlayPosition() {
|
|
162
|
+
if (this._positionRaf) cancelAnimationFrame(this._positionRaf);
|
|
163
|
+
this._positionRaf = requestAnimationFrame(() => {
|
|
164
|
+
this._positionRaf = null;
|
|
165
|
+
this._updateOverlayPositionNow();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_updateOverlayPositionNow() {
|
|
155
170
|
if (!this._activeWrapper || !this._overlay) return;
|
|
171
|
+
const offsetParent = this._overlay.offsetParent || this._container;
|
|
172
|
+
const containerRect = offsetParent.getBoundingClientRect();
|
|
156
173
|
const rect = this._activeWrapper.getBoundingClientRect();
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
const left = rect.left - containerRect.left + offsetParent.scrollLeft;
|
|
175
|
+
const top = rect.top - containerRect.top + offsetParent.scrollTop;
|
|
176
|
+
this._overlay.style.left = `${left}px`;
|
|
177
|
+
this._overlay.style.top = `${top}px`;
|
|
159
178
|
this._overlay.style.width = `${rect.width}px`;
|
|
160
179
|
this._overlay.style.height = `${rect.height}px`;
|
|
161
180
|
}
|
|
@@ -10,6 +10,7 @@ const ICONS = {
|
|
|
10
10
|
alignCenter: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="4" width="10" height="8" rx="1"/><line x1="3" y1="16" x2="21" y2="16"/><line x1="6" y1="20" x2="18" y2="20"/></svg>`,
|
|
11
11
|
originalSize:`<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>`,
|
|
12
12
|
deleteVideo: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
|
|
13
|
+
preview: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"/></svg>`,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
const SHOW_DELAY = 100;
|
|
@@ -24,6 +25,8 @@ export class VideoTooltip {
|
|
|
24
25
|
this._showTimer = null;
|
|
25
26
|
this._hideTimer = null;
|
|
26
27
|
this._disposers = [];
|
|
28
|
+
this._previewMode = false;
|
|
29
|
+
this._previewClickOff = null;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
initialize() {
|
|
@@ -61,6 +64,7 @@ export class VideoTooltip {
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
destroy() {
|
|
67
|
+
if (this._previewMode) this._exitPreview();
|
|
64
68
|
this._clearTimers();
|
|
65
69
|
this._disposers.forEach((d) => d());
|
|
66
70
|
this._disposers = [];
|
|
@@ -105,6 +109,11 @@ export class VideoTooltip {
|
|
|
105
109
|
|
|
106
110
|
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
107
111
|
|
|
112
|
+
this._previewBtn = this._makeBtn(ICONS.preview, 'Preview Video', () => this._togglePreview());
|
|
113
|
+
el.appendChild(this._previewBtn);
|
|
114
|
+
|
|
115
|
+
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
116
|
+
|
|
108
117
|
this._deleteBtn = this._makeBtn(ICONS.deleteVideo, 'Delete Video', () => this._delete(), true);
|
|
109
118
|
|
|
110
119
|
el.appendChild(this._deleteBtn);
|
|
@@ -155,6 +164,8 @@ export class VideoTooltip {
|
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
_scheduleHide() {
|
|
167
|
+
// Keep tooltip alive while preview mode is active
|
|
168
|
+
if (this._previewMode) return;
|
|
158
169
|
clearTimeout(this._showTimer);
|
|
159
170
|
this._showTimer = null;
|
|
160
171
|
if (this._hideTimer) return;
|
|
@@ -167,6 +178,7 @@ export class VideoTooltip {
|
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
_hide() {
|
|
181
|
+
if (this._previewMode) this._exitPreview();
|
|
170
182
|
this._el.style.display = 'none';
|
|
171
183
|
this._activeWrapper = null;
|
|
172
184
|
this._clearTimers();
|
|
@@ -249,4 +261,65 @@ export class VideoTooltip {
|
|
|
249
261
|
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
|
|
250
262
|
this.context.invoke('editor.afterCommand');
|
|
251
263
|
}
|
|
264
|
+
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Preview mode — temporarily disables the shield so the video is interactive
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
_togglePreview() {
|
|
270
|
+
if (this._previewMode) {
|
|
271
|
+
this._exitPreview();
|
|
272
|
+
} else {
|
|
273
|
+
this._enterPreview();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_enterPreview() {
|
|
278
|
+
const wrapper = this._activeWrapper;
|
|
279
|
+
if (!wrapper) return;
|
|
280
|
+
this._previewMode = true;
|
|
281
|
+
|
|
282
|
+
// Hide the shield so the iframe / video receives pointer events directly
|
|
283
|
+
const shield = wrapper.querySelector('.an-video-shield');
|
|
284
|
+
if (shield) shield.style.display = 'none';
|
|
285
|
+
|
|
286
|
+
// Hide the resize overlay — it would block interaction with the embed
|
|
287
|
+
this.context.invoke('videoResizer.deselect');
|
|
288
|
+
|
|
289
|
+
// Visual feedback: button turns primary-coloured
|
|
290
|
+
this._previewBtn.classList.add('an-link-tooltip-btn--copied');
|
|
291
|
+
this._previewBtn.title = 'Exit Preview';
|
|
292
|
+
|
|
293
|
+
// Exit preview on mousedown outside the wrapper.
|
|
294
|
+
// Using 'mousedown' (not 'click') so clicks inside an iframe
|
|
295
|
+
// — which never bubble to the outer document — don't accidentally
|
|
296
|
+
// leave preview mode stuck. For <video> elements, mousedown on the
|
|
297
|
+
// video is inside the wrapper so contains() returns true → no exit.
|
|
298
|
+
this._previewClickOff = (e) => {
|
|
299
|
+
if (!wrapper.contains(e.target) && !this._el.contains(e.target)) {
|
|
300
|
+
this._exitPreview();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
document.addEventListener('mousedown', this._previewClickOff, true);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_exitPreview() {
|
|
307
|
+
this._previewMode = false;
|
|
308
|
+
|
|
309
|
+
const wrapper = this._activeWrapper;
|
|
310
|
+
if (wrapper) {
|
|
311
|
+
// Restore the shield
|
|
312
|
+
const shield = wrapper.querySelector('.an-video-shield');
|
|
313
|
+
if (shield) shield.style.display = '';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Reset button appearance
|
|
317
|
+
this._previewBtn.classList.remove('an-link-tooltip-btn--copied');
|
|
318
|
+
this._previewBtn.title = 'Preview Video';
|
|
319
|
+
|
|
320
|
+
if (this._previewClickOff) {
|
|
321
|
+
document.removeEventListener('mousedown', this._previewClickOff, true);
|
|
322
|
+
this._previewClickOff = null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
252
325
|
}
|
package/src/js/renderer.js
CHANGED
|
@@ -99,6 +99,11 @@ export function renderLayout(targetEl, options) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Custom focus ring colour
|
|
103
|
+
if (options.focusColor) {
|
|
104
|
+
container.style.setProperty('--an-focus-color', options.focusColor);
|
|
105
|
+
}
|
|
106
|
+
|
|
102
107
|
// Hide the original element; keep it in DOM for form submission
|
|
103
108
|
targetEl.style.display = 'none';
|
|
104
109
|
targetEl.insertAdjacentElement('afterend', container);
|
package/src/js/settings.js
CHANGED
|
@@ -45,6 +45,7 @@ import { defaultToolbar } from './module/Buttons.js';
|
|
|
45
45
|
* @property {Function} [onDestroy] - Callback fired when the editor is destroyed: (context) => void
|
|
46
46
|
* @property {Function} [onCharLimitReached] - Callback fired when the character limit is hit: (context) => void
|
|
47
47
|
* @property {Function} [onWordLimitReached] - Callback fired when the word limit is hit: (context) => void
|
|
48
|
+
* @property {string} [focusColor] - Custom focus ring colour, e.g. '#f97316'. Overrides the default blue.
|
|
48
49
|
*/
|
|
49
50
|
|
|
50
51
|
/** @type {AsnOptions} */
|
|
@@ -131,4 +132,7 @@ export const defaultOptions = {
|
|
|
131
132
|
onCharLimitReached: null,
|
|
132
133
|
// Callback fired when the word limit is reached: function(context)
|
|
133
134
|
onWordLimitReached: null,
|
|
135
|
+
// Custom focus ring colour — overrides the default blue when set.
|
|
136
|
+
// Accepts any valid CSS colour string, e.g. '#f97316', 'hsl(25,90%,55%)'.
|
|
137
|
+
focusColor: null,
|
|
134
138
|
};
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
.an-container {
|
|
13
13
|
display: flex;
|
|
14
14
|
flex-direction: column;
|
|
15
|
+
position: relative;
|
|
15
16
|
border: 1px solid $an-border;
|
|
16
17
|
border-radius: $an-radius;
|
|
17
18
|
font-family: $an-font-family;
|
|
18
19
|
font-size: $an-font-size;
|
|
19
20
|
box-sizing: border-box;
|
|
20
21
|
background: $an-bg;
|
|
21
|
-
overflow: hidden;
|
|
22
22
|
// Prevent the container from participating in ancestor layout recalculations.
|
|
23
23
|
// This limits reflow thrashing when editing large documents.
|
|
24
24
|
contain: layout;
|
|
@@ -30,14 +30,27 @@
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
&.an-focused {
|
|
33
|
-
border-color: $an-primary;
|
|
34
|
-
|
|
33
|
+
border-color: var(--an-focus-color, #{$an-primary});
|
|
34
|
+
// color-mix() blends the focus colour with transparent for the glow ring.
|
|
35
|
+
// Falls back gracefully to the default blue glow in older browsers.
|
|
36
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--an-focus-color, #{$an-primary}) 18%, transparent);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
&.an-disabled {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
// Hide toolbar only — read-only view has no editing controls.
|
|
41
|
+
.an-toolbar {
|
|
42
|
+
display: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Allow the user to select and copy text but block actual editing.
|
|
46
|
+
// Do NOT set pointer-events:none or user-select:none on the container —
|
|
47
|
+
// that would prevent text selection inside the editable area.
|
|
48
|
+
.an-editable {
|
|
49
|
+
cursor: text;
|
|
50
|
+
user-select: text;
|
|
51
|
+
// Subtle visual cue that the area is read-only.
|
|
52
|
+
background: #fafafa;
|
|
53
|
+
}
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
&.an-fullscreen {
|
|
@@ -234,9 +247,9 @@
|
|
|
234
247
|
|
|
235
248
|
// Swatch popup panel
|
|
236
249
|
.an-color-popup {
|
|
237
|
-
position:
|
|
238
|
-
top
|
|
239
|
-
|
|
250
|
+
position: fixed;
|
|
251
|
+
// top / left are set dynamically by JS (getBoundingClientRect) so the popup
|
|
252
|
+
// escapes overflow:hidden / overflow:auto ancestors (toolbar scroll mode, etc.)
|
|
240
253
|
z-index: 1100;
|
|
241
254
|
background: $an-bg;
|
|
242
255
|
border: 1px solid $an-border;
|
|
@@ -377,19 +390,25 @@
|
|
|
377
390
|
.an-editable {
|
|
378
391
|
flex: 1;
|
|
379
392
|
padding: $an-editable-pad;
|
|
380
|
-
min-height:
|
|
393
|
+
min-height: 0;
|
|
381
394
|
outline: none;
|
|
382
395
|
line-height: $an-line-height;
|
|
383
396
|
color: $an-text;
|
|
384
397
|
overflow-y: auto;
|
|
385
398
|
word-break: break-word;
|
|
386
399
|
|
|
387
|
-
// Placeholder
|
|
388
|
-
|
|
400
|
+
// Placeholder — only shown when not focused.
|
|
401
|
+
// float:left is the standard contenteditable placeholder technique (used by
|
|
402
|
+
// Quill.js): the pseudo-element is out of normal flow so it never shifts the
|
|
403
|
+
// cursor, yet it renders visually at the start of the empty editing area.
|
|
404
|
+
// Avoid position:absolute here — without position:relative on the parent it
|
|
405
|
+
// would be positioned relative to .an-container (contain:layout) and overlap
|
|
406
|
+
// the toolbar.
|
|
407
|
+
&.an-placeholder:not(:focus)::before {
|
|
389
408
|
content: attr(data-placeholder);
|
|
390
409
|
color: $an-muted;
|
|
391
410
|
pointer-events: none;
|
|
392
|
-
|
|
411
|
+
float: left;
|
|
393
412
|
}
|
|
394
413
|
|
|
395
414
|
// Content styles
|
|
@@ -435,7 +454,7 @@
|
|
|
435
454
|
|
|
436
455
|
.an-video-wrapper {
|
|
437
456
|
position: relative;
|
|
438
|
-
display:
|
|
457
|
+
display: block;
|
|
439
458
|
max-width: 100%;
|
|
440
459
|
margin: 4px 0;
|
|
441
460
|
|
|
@@ -667,6 +686,7 @@
|
|
|
667
686
|
display: flex;
|
|
668
687
|
align-items: center;
|
|
669
688
|
justify-content: space-between;
|
|
689
|
+
flex-shrink: 0;
|
|
670
690
|
height: $an-statusbar-h;
|
|
671
691
|
padding: 0 8px;
|
|
672
692
|
background: $an-statusbar-bg;
|
|
@@ -919,7 +939,7 @@
|
|
|
919
939
|
// ---------------------------------------------------------------------------
|
|
920
940
|
|
|
921
941
|
.an-image-resizer {
|
|
922
|
-
position:
|
|
942
|
+
position: absolute;
|
|
923
943
|
z-index: 10000;
|
|
924
944
|
box-sizing: border-box;
|
|
925
945
|
border: 2px solid $an-primary;
|
|
@@ -956,7 +976,7 @@
|
|
|
956
976
|
$an-video-accent: #8b5cf6; // violet-500
|
|
957
977
|
|
|
958
978
|
.an-video-resizer {
|
|
959
|
-
position:
|
|
979
|
+
position: absolute;
|
|
960
980
|
z-index: 10000;
|
|
961
981
|
box-sizing: border-box;
|
|
962
982
|
border: 2px solid $an-video-accent;
|
|
@@ -986,11 +1006,10 @@ $an-video-accent: #8b5cf6; // violet-500
|
|
|
986
1006
|
.an-resize-w { top: calc(50% - 4px); left: -4px; cursor: w-resize; }
|
|
987
1007
|
}
|
|
988
1008
|
|
|
989
|
-
//
|
|
990
|
-
.an-video-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}
|
|
1009
|
+
// .an-video-selected is applied by VideoResizer for JS state tracking.
|
|
1010
|
+
// Visual selection is handled entirely by the .an-video-resizer overlay,
|
|
1011
|
+
// so no CSS outline is needed here (two visible frames would appear otherwise).
|
|
1012
|
+
.an-video-selected {}
|
|
994
1013
|
|
|
995
1014
|
// ---------------------------------------------------------------------------
|
|
996
1015
|
// Icon dialog
|
|
@@ -1082,7 +1101,7 @@ $an-video-accent: #8b5cf6; // violet-500
|
|
|
1082
1101
|
display: grid;
|
|
1083
1102
|
grid-template-columns: repeat(auto-fill, 56px);
|
|
1084
1103
|
gap: 3px;
|
|
1085
|
-
|
|
1104
|
+
height: 256px;
|
|
1086
1105
|
overflow-y: auto;
|
|
1087
1106
|
margin-bottom: 10px;
|
|
1088
1107
|
padding: 4px;
|
|
@@ -1244,7 +1263,7 @@ $an-video-accent: #8b5cf6; // violet-500
|
|
|
1244
1263
|
display: grid;
|
|
1245
1264
|
grid-template-columns: repeat(auto-fill, 42px);
|
|
1246
1265
|
gap: 2px;
|
|
1247
|
-
|
|
1266
|
+
height: 288px;
|
|
1248
1267
|
overflow-y: auto;
|
|
1249
1268
|
margin-bottom: 10px;
|
|
1250
1269
|
padding: 4px;
|
|
@@ -1572,6 +1591,11 @@ $an-video-accent: #8b5cf6; // violet-500
|
|
|
1572
1591
|
.an-table-cell {
|
|
1573
1592
|
background: #24273a;
|
|
1574
1593
|
border-color: #3f3f5f;
|
|
1594
|
+
|
|
1595
|
+
&.active {
|
|
1596
|
+
background: rgba(99, 102, 241, 0.3);
|
|
1597
|
+
border-color: #6366f1;
|
|
1598
|
+
}
|
|
1575
1599
|
}
|
|
1576
1600
|
|
|
1577
1601
|
.an-context-item { color: #cdd6f4; }
|
|
@@ -1670,25 +1694,31 @@ $an-video-accent: #8b5cf6; // violet-500
|
|
|
1670
1694
|
.an-editable {
|
|
1671
1695
|
ul.an-checklist {
|
|
1672
1696
|
list-style: none;
|
|
1673
|
-
padding-left: 0
|
|
1697
|
+
padding-left: 0;
|
|
1674
1698
|
margin: 0.5em 0;
|
|
1675
1699
|
|
|
1676
1700
|
li {
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1701
|
+
// Use padding-left + absolute checkbox so text is a normal inline flow
|
|
1702
|
+
// (not an anonymous flex item). This ensures font-size, line-height and
|
|
1703
|
+
// all inline formats are identical to regular paragraph text.
|
|
1704
|
+
position: relative;
|
|
1705
|
+
padding-left: 22px;
|
|
1706
|
+
padding-top: 1px;
|
|
1707
|
+
padding-bottom: 1px;
|
|
1708
|
+
min-height: 1.4em;
|
|
1681
1709
|
line-height: inherit;
|
|
1710
|
+
font-size: inherit;
|
|
1711
|
+
font-family: inherit;
|
|
1682
1712
|
|
|
1683
1713
|
input[type='checkbox'] {
|
|
1684
|
-
|
|
1714
|
+
position: absolute;
|
|
1715
|
+
left: 0;
|
|
1716
|
+
top: 0.25em;
|
|
1685
1717
|
width: 14px;
|
|
1686
1718
|
height: 14px;
|
|
1687
1719
|
margin: 0;
|
|
1688
1720
|
cursor: pointer;
|
|
1689
1721
|
accent-color: $an-primary;
|
|
1690
|
-
position: relative;
|
|
1691
|
-
top: 2px;
|
|
1692
1722
|
}
|
|
1693
1723
|
|
|
1694
1724
|
// Checked item: dim + strike-through the text node
|
package/types/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface AsnOptions {
|
|
|
43
43
|
/** Number of spaces inserted when Tab is pressed (outside a list). */
|
|
44
44
|
tabSize?: number;
|
|
45
45
|
/** Callback on content change. */
|
|
46
|
-
onChange?: (
|
|
46
|
+
onChange?: (html: string) => void;
|
|
47
47
|
/** Callback on editor focus. */
|
|
48
48
|
onFocus?: (context: Context) => void;
|
|
49
49
|
/** Callback fired on editor blur. */
|
|
@@ -59,9 +59,9 @@ export interface AsnOptions {
|
|
|
59
59
|
/** Callback fired when the word limit is reached. */
|
|
60
60
|
onWordLimitReached?: (context: Context) => void;
|
|
61
61
|
/** Custom image upload handler. */
|
|
62
|
-
onImageUpload?: (files:
|
|
62
|
+
onImageUpload?: (files: File[]) => void;
|
|
63
63
|
/** Callback when an image upload error occurs. */
|
|
64
|
-
onImageError?: (error:
|
|
64
|
+
onImageError?: (error: { file?: File; message: string; error?: unknown }) => void;
|
|
65
65
|
/** Stick the toolbar to the viewport top when scrolling. */
|
|
66
66
|
stickyToolbar?: boolean;
|
|
67
67
|
/** Top offset in px for sticky toolbar (e.g. height of a fixed nav bar). */
|
|
@@ -101,7 +101,7 @@ export interface AsnOptions {
|
|
|
101
101
|
/** Insert a header row (<thead>) when creating new tables. */
|
|
102
102
|
tableHeaderRow?: boolean;
|
|
103
103
|
/** Callback fired after every paste event. */
|
|
104
|
-
onPaste?: (data: { text: string; html: string }) => void;
|
|
104
|
+
onPaste?: (data: { text: string; html: string | null }) => void;
|
|
105
105
|
/** Additional color swatches shown at the top of the color picker. */
|
|
106
106
|
colorSwatches?: string[];
|
|
107
107
|
}
|