autumnnote 1.5.0 → 1.6.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 +10 -8
- package/dist/autumnnote.css +324 -4
- package/dist/autumnnote.es.js +943 -526
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +935 -519
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +21 -3
- package/src/js/Context.js +23 -16
- package/src/js/core/detectLang.js +98 -0
- package/src/js/core/dom.js +67 -11
- package/src/js/core/env.js +1 -1
- package/src/js/core/func.js +1 -1
- package/src/js/core/lists.js +1 -1
- package/src/js/core/markdown.js +32 -31
- package/src/js/core/range.js +8 -8
- package/src/js/editing/History.js +10 -10
- package/src/js/editing/Style.js +44 -44
- package/src/js/editing/Table.js +5 -7
- package/src/js/editing/Typing.js +19 -19
- package/src/js/i18n/en.js +2 -0
- package/src/js/i18n/vi.js +2 -0
- package/src/js/index.js +3 -2
- package/src/js/module/AutoSaveRestore.js +2 -4
- package/src/js/module/BubbleToolbar.js +51 -34
- package/src/js/module/Buttons.js +20 -18
- package/src/js/module/Clipboard.js +16 -17
- package/src/js/module/CodeTooltip.js +48 -24
- package/src/js/module/Codeview.js +5 -7
- package/src/js/module/ContextMenu.js +37 -37
- package/src/js/module/Editor.js +77 -26
- package/src/js/module/EmojiDialog.js +24 -18
- package/src/js/module/FindReplace.js +93 -66
- package/src/js/module/Fullscreen.js +1 -1
- package/src/js/module/IconDialog.js +39 -35
- package/src/js/module/ImageCropOverlay.js +13 -13
- package/src/js/module/ImageDialog.js +19 -13
- package/src/js/module/ImageResizer.js +5 -5
- package/src/js/module/ImageTooltip.js +20 -16
- package/src/js/module/LinkDialog.js +20 -14
- package/src/js/module/LinkTooltip.js +15 -12
- package/src/js/module/MarkdownShortcuts.js +11 -14
- package/src/js/module/Mention.js +11 -13
- package/src/js/module/Placeholder.js +1 -1
- package/src/js/module/ShortcutsDialog.js +2 -4
- package/src/js/module/Statusbar.js +5 -7
- package/src/js/module/TableTooltip.js +196 -36
- package/src/js/module/Toolbar.js +35 -34
- package/src/js/module/VideoDialog.js +16 -10
- package/src/js/module/VideoResizer.js +7 -9
- package/src/js/module/VideoTooltip.js +15 -10
- package/src/js/renderer.js +5 -3
- package/src/js/settings.js +53 -36
- package/src/styles/autumnnote.scss +332 -7
package/src/js/module/Toolbar.js
CHANGED
|
@@ -146,7 +146,7 @@ export class Toolbar {
|
|
|
146
146
|
this._disposers.forEach((d) => d());
|
|
147
147
|
this._disposers = [];
|
|
148
148
|
if (this.el && this.el.parentNode) {
|
|
149
|
-
this.el.
|
|
149
|
+
this.el.remove();
|
|
150
150
|
}
|
|
151
151
|
this.el = null;
|
|
152
152
|
}
|
|
@@ -244,8 +244,8 @@ export class Toolbar {
|
|
|
244
244
|
|
|
245
245
|
const setHighlight = (rows, cols) => {
|
|
246
246
|
cells.forEach((cell) => {
|
|
247
|
-
const r = +cell.
|
|
248
|
-
const c = +cell.
|
|
247
|
+
const r = +cell.dataset.row;
|
|
248
|
+
const c = +cell.dataset.col;
|
|
249
249
|
cell.classList.toggle('active', r <= rows && c <= cols);
|
|
250
250
|
});
|
|
251
251
|
label.textContent = (rows && cols) ? `${rows} × ${cols}` : (this.context.locale.toolbar.insertTableLabel || 'Insert Table');
|
|
@@ -264,8 +264,8 @@ export class Toolbar {
|
|
|
264
264
|
|
|
265
265
|
let left = rect.left;
|
|
266
266
|
let top = rect.bottom + 4;
|
|
267
|
-
if (left + pw >
|
|
268
|
-
if (top + ph >
|
|
267
|
+
if (left + pw > globalThis.innerWidth - 8) left = Math.max(8, globalThis.innerWidth - pw - 8);
|
|
268
|
+
if (top + ph > globalThis.innerHeight - 8) top = rect.top - ph - 4;
|
|
269
269
|
|
|
270
270
|
popup.style.left = `${left}px`;
|
|
271
271
|
popup.style.top = `${top}px`;
|
|
@@ -286,18 +286,18 @@ export class Toolbar {
|
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
const d2 = on(grid, 'mouseover', (e) => {
|
|
289
|
-
const cell = e.target
|
|
289
|
+
const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
|
|
290
290
|
if (!cell) return;
|
|
291
|
-
setHighlight(+cell.
|
|
291
|
+
setHighlight(+cell.dataset.row, +cell.dataset.col);
|
|
292
292
|
});
|
|
293
293
|
|
|
294
294
|
const d3 = on(grid, 'mouseleave', () => setHighlight(0, 0));
|
|
295
295
|
|
|
296
296
|
const d4 = on(grid, 'click', (e) => {
|
|
297
|
-
const cell = e.target
|
|
297
|
+
const cell = /** @type {HTMLElement|null} */ (/** @type {Element} */ (e.target)?.closest('.an-table-cell'));
|
|
298
298
|
if (!cell) return;
|
|
299
|
-
const rows = +cell.
|
|
300
|
-
const cols = +cell.
|
|
299
|
+
const rows = +cell.dataset.row;
|
|
300
|
+
const cols = +cell.dataset.col;
|
|
301
301
|
closePopup();
|
|
302
302
|
this.context.invoke('editor.focus');
|
|
303
303
|
def.action(this.context, rows, cols);
|
|
@@ -308,12 +308,12 @@ export class Toolbar {
|
|
|
308
308
|
// Append popup to body so position:fixed is truly viewport-relative,
|
|
309
309
|
// unaffected by any ancestor transform / filter (same pattern as color picker).
|
|
310
310
|
this._disposers.push(d1, d2, d3, d4, d5, () => {
|
|
311
|
-
if (popup.parentNode) popup.
|
|
311
|
+
if (popup.parentNode) popup.remove();
|
|
312
312
|
});
|
|
313
313
|
|
|
314
314
|
wrap.appendChild(btn);
|
|
315
315
|
document.body.appendChild(popup);
|
|
316
|
-
return wrap;
|
|
316
|
+
return /** @type {HTMLDivElement} */ (wrap);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
/**
|
|
@@ -384,7 +384,7 @@ export class Toolbar {
|
|
|
384
384
|
});
|
|
385
385
|
|
|
386
386
|
const customRow = createElement('div', { class: 'an-color-custom' });
|
|
387
|
-
const colorInput = createElement('input', { type: 'color', value: currentColor, title: this.context.locale.toolbar.customColor || 'Custom color' });
|
|
387
|
+
const colorInput = /** @type {HTMLInputElement} */ (createElement('input', { type: 'color', value: currentColor, title: this.context.locale.toolbar.customColor || 'Custom color' }));
|
|
388
388
|
const customLabel = createElement('span', {}, [this.context.locale.toolbar.customColor || 'Custom color']);
|
|
389
389
|
customRow.appendChild(colorInput);
|
|
390
390
|
customRow.appendChild(customLabel);
|
|
@@ -398,14 +398,14 @@ export class Toolbar {
|
|
|
398
398
|
let savedRange = null;
|
|
399
399
|
|
|
400
400
|
const saveSelection = () => {
|
|
401
|
-
const sel =
|
|
401
|
+
const sel = globalThis.getSelection();
|
|
402
402
|
savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
|
|
403
403
|
};
|
|
404
404
|
|
|
405
405
|
const restoreSelection = () => {
|
|
406
406
|
if (!savedRange) return;
|
|
407
407
|
try {
|
|
408
|
-
const sel =
|
|
408
|
+
const sel = globalThis.getSelection();
|
|
409
409
|
if (!sel) return;
|
|
410
410
|
sel.removeAllRanges();
|
|
411
411
|
sel.addRange(savedRange);
|
|
@@ -424,7 +424,7 @@ export class Toolbar {
|
|
|
424
424
|
const rect = arrowBtn.getBoundingClientRect();
|
|
425
425
|
const popupMinW = 184;
|
|
426
426
|
let left = rect.left;
|
|
427
|
-
if (left + popupMinW >
|
|
427
|
+
if (left + popupMinW > globalThis.innerWidth) left = rect.right - popupMinW;
|
|
428
428
|
popup.style.top = `${rect.bottom + 4}px`;
|
|
429
429
|
popup.style.left = `${Math.max(4, left)}px`;
|
|
430
430
|
popup.style.display = 'block';
|
|
@@ -472,17 +472,17 @@ export class Toolbar {
|
|
|
472
472
|
});
|
|
473
473
|
|
|
474
474
|
const d3b = on(swatches, 'click', (e) => {
|
|
475
|
-
const sw = e.target
|
|
476
|
-
if (sw) applyColor(sw.dataset.color);
|
|
475
|
+
const sw = /** @type {Element} */ (e.target)?.closest('.an-color-swatch');
|
|
476
|
+
if (sw) applyColor(/** @type {HTMLElement} */ (sw).dataset.color);
|
|
477
477
|
});
|
|
478
478
|
|
|
479
479
|
const d4 = on(colorInput, 'change', (e) => {
|
|
480
|
-
applyColor(e.target.value);
|
|
480
|
+
applyColor(/** @type {HTMLInputElement} */ (e.target).value);
|
|
481
481
|
});
|
|
482
482
|
|
|
483
483
|
const d5 = on(document, 'click', (e) => {
|
|
484
484
|
// popup is in document.body, not inside wrap — check both
|
|
485
|
-
if (isOpen && !wrap.contains(e.target) && !popup.contains(e.target)) closePopup();
|
|
485
|
+
if (isOpen && !wrap.contains(/** @type {Node} */ (e.target)) && !popup.contains(/** @type {Node} */ (e.target))) closePopup();
|
|
486
486
|
});
|
|
487
487
|
|
|
488
488
|
const d6 = on(popup, 'click', (e) => e.stopPropagation());
|
|
@@ -491,13 +491,13 @@ export class Toolbar {
|
|
|
491
491
|
// popup doesn't drift away from the button it belongs to.
|
|
492
492
|
const onScrollResize = () => { if (isOpen) closePopup(); };
|
|
493
493
|
document.addEventListener('scroll', onScrollResize, { passive: true, capture: true });
|
|
494
|
-
|
|
494
|
+
globalThis.addEventListener('resize', onScrollResize, { passive: true });
|
|
495
495
|
|
|
496
496
|
this._disposers.push(d1, d2, d2b, d3, d3b, d4, d5, d6,
|
|
497
497
|
() => document.removeEventListener('scroll', onScrollResize, { capture: true }),
|
|
498
|
-
() =>
|
|
498
|
+
() => globalThis.removeEventListener('resize', onScrollResize),
|
|
499
499
|
// Remove popup from body on editor destroy
|
|
500
|
-
() => { if (popup.parentNode) popup.
|
|
500
|
+
() => { if (popup.parentNode) popup.remove(); },
|
|
501
501
|
);
|
|
502
502
|
|
|
503
503
|
// Register this popup's closer so other color pickers can close it
|
|
@@ -513,7 +513,7 @@ export class Toolbar {
|
|
|
513
513
|
wrap.appendChild(applyBtn);
|
|
514
514
|
wrap.appendChild(arrowBtn);
|
|
515
515
|
document.body.appendChild(popup);
|
|
516
|
-
return wrap;
|
|
516
|
+
return /** @type {HTMLDivElement} */ (wrap);
|
|
517
517
|
}
|
|
518
518
|
|
|
519
519
|
/**
|
|
@@ -562,7 +562,7 @@ export class Toolbar {
|
|
|
562
562
|
/** @type {Range|null} */
|
|
563
563
|
let _savedRange = null;
|
|
564
564
|
const dMousedown = on(select, 'mousedown', () => {
|
|
565
|
-
const sel =
|
|
565
|
+
const sel = globalThis.getSelection();
|
|
566
566
|
_savedRange = (sel && sel.rangeCount) ? sel.getRangeAt(0).cloneRange() : null;
|
|
567
567
|
});
|
|
568
568
|
|
|
@@ -574,7 +574,7 @@ export class Toolbar {
|
|
|
574
574
|
// Restore selection saved on mousedown so the action targets the correct text.
|
|
575
575
|
if (_savedRange) {
|
|
576
576
|
try {
|
|
577
|
-
const sel =
|
|
577
|
+
const sel = globalThis.getSelection();
|
|
578
578
|
if (sel) { sel.removeAllRanges(); sel.addRange(_savedRange); }
|
|
579
579
|
} catch (_) { /* range may be stale if DOM changed */ }
|
|
580
580
|
}
|
|
@@ -583,7 +583,7 @@ export class Toolbar {
|
|
|
583
583
|
});
|
|
584
584
|
|
|
585
585
|
this._disposers.push(dMousedown, disposer);
|
|
586
|
-
return select;
|
|
586
|
+
return /** @type {HTMLSelectElement} */ (select);
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
/**
|
|
@@ -638,7 +638,7 @@ export class Toolbar {
|
|
|
638
638
|
});
|
|
639
639
|
|
|
640
640
|
this._disposers.push(disposer);
|
|
641
|
-
return btn;
|
|
641
|
+
return /** @type {HTMLButtonElement} */ (btn);
|
|
642
642
|
}
|
|
643
643
|
|
|
644
644
|
// ---------------------------------------------------------------------------
|
|
@@ -659,7 +659,7 @@ export class Toolbar {
|
|
|
659
659
|
// count as "the host page loaded FA" for toolbar icon rendering purposes.
|
|
660
660
|
const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
661
661
|
.filter((l) => l.id !== 'an-fontawesome-css')
|
|
662
|
-
.map((l) => l.href || '').join(' ');
|
|
662
|
+
.map((l) => /** @type {HTMLLinkElement} */ (l).href || '').join(' ');
|
|
663
663
|
_faPageLevelReady = /fontawesome|font-awesome|use\.fontawesome|all\.css/.test(links);
|
|
664
664
|
return _faPageLevelReady;
|
|
665
665
|
}
|
|
@@ -684,18 +684,18 @@ export class Toolbar {
|
|
|
684
684
|
|
|
685
685
|
// Sync button active states
|
|
686
686
|
this.el.querySelectorAll('button[data-btn]').forEach((btn) => {
|
|
687
|
-
const def = btnMap.get(btn.
|
|
687
|
+
const def = btnMap.get(/** @type {HTMLElement} */ (btn).dataset.btn);
|
|
688
688
|
if (def && typeof def.isActive === 'function') {
|
|
689
689
|
btn.classList.toggle('active', !!def.isActive(this.context));
|
|
690
690
|
}
|
|
691
691
|
if (def && typeof def.isDisabled === 'function') {
|
|
692
|
-
btn.disabled = !!def.isDisabled(this.context);
|
|
692
|
+
/** @type {HTMLButtonElement} */ (btn).disabled = !!def.isDisabled(this.context);
|
|
693
693
|
}
|
|
694
694
|
});
|
|
695
695
|
|
|
696
696
|
// Sync select dropdowns (e.g. font family) with current cursor position
|
|
697
697
|
this.el.querySelectorAll('select[data-btn]').forEach((select) => {
|
|
698
|
-
const def = btnMap.get(select.
|
|
698
|
+
const def = btnMap.get(/** @type {HTMLElement} */ (select).dataset.btn);
|
|
699
699
|
if (!def || typeof def.getValue !== 'function') return;
|
|
700
700
|
// queryCommandValue returns the font name, possibly quoted — strip quotes
|
|
701
701
|
let raw = (def.getValue(this.context) || '').replace(/["']/g, '').trim();
|
|
@@ -706,10 +706,11 @@ export class Toolbar {
|
|
|
706
706
|
|| '';
|
|
707
707
|
}
|
|
708
708
|
// Try to match against available options (case-insensitive)
|
|
709
|
-
const
|
|
709
|
+
const sel = /** @type {HTMLSelectElement} */ (select);
|
|
710
|
+
const matched = Array.from(sel.options).find(
|
|
710
711
|
(opt) => opt.value && opt.value.toLowerCase() === raw.toLowerCase()
|
|
711
712
|
);
|
|
712
|
-
|
|
713
|
+
sel.value = matched ? matched.value : '';
|
|
713
714
|
});
|
|
714
715
|
}
|
|
715
716
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* • Direct video URLs → <video> element (.mp4 / .webm / .ogg)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createElement, on, trapFocus } from '../core/dom.js';
|
|
10
|
+
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
11
11
|
import { withSavedRange } from '../core/range.js';
|
|
12
12
|
|
|
13
13
|
export class VideoDialog {
|
|
@@ -35,7 +35,7 @@ export class VideoDialog {
|
|
|
35
35
|
this._disposers.forEach((d) => d());
|
|
36
36
|
this._disposers = [];
|
|
37
37
|
if (this._dialog && this._dialog.parentNode) {
|
|
38
|
-
this._dialog.
|
|
38
|
+
this._dialog.remove();
|
|
39
39
|
}
|
|
40
40
|
this._dialog = null;
|
|
41
41
|
}
|
|
@@ -68,8 +68,13 @@ export class VideoDialog {
|
|
|
68
68
|
});
|
|
69
69
|
const box = createElement('div', { class: 'an-dialog-box' });
|
|
70
70
|
|
|
71
|
+
const header = createElement('div', { class: 'an-dialog-header' });
|
|
72
|
+
const iconEl = createElement('span', { class: 'an-dialog-icon' });
|
|
73
|
+
iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/></svg>`;
|
|
71
74
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
72
75
|
title.textContent = L.title;
|
|
76
|
+
header.appendChild(iconEl);
|
|
77
|
+
header.appendChild(title);
|
|
73
78
|
|
|
74
79
|
// URL input
|
|
75
80
|
const urlLabel = createElement('label', { class: 'an-label' });
|
|
@@ -108,8 +113,9 @@ export class VideoDialog {
|
|
|
108
113
|
btnRow.appendChild(insertBtn);
|
|
109
114
|
btnRow.appendChild(cancelBtn);
|
|
110
115
|
|
|
111
|
-
box.append(
|
|
116
|
+
box.append(header, urlLabel, urlInput, hintEl, widthLabel, widthInput, btnRow);
|
|
112
117
|
overlay.appendChild(box);
|
|
118
|
+
makeDraggable(header, box);
|
|
113
119
|
|
|
114
120
|
// Live URL hint
|
|
115
121
|
const d0 = on(urlInput, 'input', () => {
|
|
@@ -132,7 +138,7 @@ export class VideoDialog {
|
|
|
132
138
|
|
|
133
139
|
_onInsert() {
|
|
134
140
|
const rawUrl = this._urlInput.value.trim();
|
|
135
|
-
const width = Math.max(80, parseInt(this._widthInput.value, 10) || 560);
|
|
141
|
+
const width = Math.max(80, Number.parseInt(this._widthInput.value, 10) || 560);
|
|
136
142
|
|
|
137
143
|
if (!rawUrl) {
|
|
138
144
|
this._urlInput.focus();
|
|
@@ -184,19 +190,19 @@ export class VideoDialog {
|
|
|
184
190
|
} catch { return null; }
|
|
185
191
|
|
|
186
192
|
// YouTube watch: https://www.youtube.com/watch?v=ID
|
|
187
|
-
const ytWatch =
|
|
193
|
+
const ytWatch = /(?:youtube\.com\/watch\?(?:.*&)?v=|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/.exec(url);
|
|
188
194
|
if (ytWatch) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytWatch[1]}` };
|
|
189
195
|
|
|
190
196
|
// YouTube short: https://youtu.be/ID
|
|
191
|
-
const ytShort =
|
|
197
|
+
const ytShort = /youtu\.be\/([a-zA-Z0-9_-]{11})/.exec(url);
|
|
192
198
|
if (ytShort) return { type: 'YouTube', embedUrl: `https://www.youtube.com/embed/${ytShort[1]}` };
|
|
193
199
|
|
|
194
200
|
// YouTube Shorts: https://www.youtube.com/shorts/ID
|
|
195
|
-
const ytShorts =
|
|
201
|
+
const ytShorts = /youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/.exec(url);
|
|
196
202
|
if (ytShorts) return { type: 'YouTube Shorts', embedUrl: `https://www.youtube.com/embed/${ytShorts[1]}` };
|
|
197
203
|
|
|
198
204
|
// Vimeo: https://vimeo.com/ID
|
|
199
|
-
const vimeo =
|
|
205
|
+
const vimeo = /vimeo\.com\/(\d+)/.exec(url);
|
|
200
206
|
if (vimeo) return { type: 'Vimeo', embedUrl: `https://player.vimeo.com/video/${vimeo[1]}` };
|
|
201
207
|
|
|
202
208
|
// Direct video file
|
|
@@ -232,7 +238,7 @@ export class VideoDialog {
|
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
if (info && info.type === 'Direct video') {
|
|
235
|
-
const src = info.embedUrl.
|
|
241
|
+
const src = info.embedUrl.replaceAll('"', '%22');
|
|
236
242
|
return (
|
|
237
243
|
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
238
244
|
`<video src="${src}" width="${width}" height="${height}" controls ` +
|
|
@@ -252,7 +258,7 @@ export class VideoDialog {
|
|
|
252
258
|
})();
|
|
253
259
|
if (!safeSrc) return null;
|
|
254
260
|
|
|
255
|
-
const escapedSrc = safeSrc.
|
|
261
|
+
const escapedSrc = safeSrc.replaceAll('"', '%22');
|
|
256
262
|
return (
|
|
257
263
|
`<div class="an-video-wrapper" style="position:relative;display:block;width:${width}px;max-width:100%">` +
|
|
258
264
|
`<video src="${escapedSrc}" width="${width}" height="${height}" controls ` +
|
|
@@ -46,8 +46,8 @@ export class VideoResizer {
|
|
|
46
46
|
if (wrapper) this._select(wrapper);
|
|
47
47
|
}),
|
|
48
48
|
on(document, 'click', (e) => this._onDocClick(e)),
|
|
49
|
-
on(
|
|
50
|
-
on(
|
|
49
|
+
on(globalThis, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
50
|
+
on(globalThis, 'resize', onWindowResize),
|
|
51
51
|
on(editable, 'scroll', () => this._updateOverlayPosition(), { passive: true }),
|
|
52
52
|
// D1: Prevent native browser drag of video wrappers. Without this, a user
|
|
53
53
|
// can hold-and-drag to produce a "copy" that lands outside .an-editable,
|
|
@@ -75,9 +75,7 @@ export class VideoResizer {
|
|
|
75
75
|
this._positionRaf = null;
|
|
76
76
|
}
|
|
77
77
|
this._deselect();
|
|
78
|
-
|
|
79
|
-
this._overlay.parentNode.removeChild(this._overlay);
|
|
80
|
-
}
|
|
78
|
+
this._overlay?.remove();
|
|
81
79
|
this._overlay = null;
|
|
82
80
|
}
|
|
83
81
|
|
|
@@ -111,10 +109,10 @@ export class VideoResizer {
|
|
|
111
109
|
_findWrapper(el) {
|
|
112
110
|
if (!el || !(el instanceof Element)) return null;
|
|
113
111
|
// Direct hit on wrapper
|
|
114
|
-
if (el.classList
|
|
112
|
+
if (el.classList?.contains('an-video-wrapper')) return /** @type {HTMLElement} */ (el);
|
|
115
113
|
// Child element (iframe, video, or nested)
|
|
116
114
|
const w = el.closest('.an-video-wrapper');
|
|
117
|
-
if (w) return w;
|
|
115
|
+
if (w) return /** @type {HTMLElement} */ (w);
|
|
118
116
|
return null;
|
|
119
117
|
}
|
|
120
118
|
|
|
@@ -152,7 +150,7 @@ export class VideoResizer {
|
|
|
152
150
|
_onDocClick(e) {
|
|
153
151
|
if (!this._activeWrapper) return;
|
|
154
152
|
if (this._activeWrapper.contains(e.target)) return;
|
|
155
|
-
if (this._overlay
|
|
153
|
+
if (this._overlay?.contains(e.target)) return;
|
|
156
154
|
if (e.target.closest('.an-contextmenu')) return;
|
|
157
155
|
this._deselect();
|
|
158
156
|
}
|
|
@@ -200,7 +198,7 @@ export class VideoResizer {
|
|
|
200
198
|
const wrapper = this._activeWrapper;
|
|
201
199
|
if (!wrapper) return;
|
|
202
200
|
|
|
203
|
-
const embed = wrapper.querySelector('iframe, video');
|
|
201
|
+
const embed = /** @type {HTMLElement|null} */ (wrapper.querySelector('iframe, video'));
|
|
204
202
|
const startX = e.clientX;
|
|
205
203
|
const startY = e.clientY;
|
|
206
204
|
const startW = wrapper.offsetWidth || 560;
|
|
@@ -39,26 +39,31 @@ export class VideoTooltip {
|
|
|
39
39
|
on(editable, 'mouseover', (e) => {
|
|
40
40
|
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
41
41
|
// The shield div sits on top of iframes — we detect hover via it or the wrapper
|
|
42
|
-
const
|
|
42
|
+
const target = /** @type {Element} */ (e.target);
|
|
43
|
+
const wrapper = target?.closest('.an-video-wrapper');
|
|
43
44
|
if (wrapper && editable.contains(wrapper)) {
|
|
44
45
|
this._scheduleShow(wrapper);
|
|
45
46
|
}
|
|
46
47
|
}, { passive: true }),
|
|
47
48
|
on(editable, 'mouseout', (e) => {
|
|
48
|
-
const to = e.relatedTarget;
|
|
49
|
-
if (!to || (!editable.contains(to) && !this._el.contains(to))) {
|
|
49
|
+
const to = /** @type {MouseEvent} */ (e).relatedTarget;
|
|
50
|
+
if (!to || (!editable.contains(/** @type {Node} */ (to)) && !this._el.contains(/** @type {Node} */ (to)))) {
|
|
50
51
|
this._scheduleHide();
|
|
51
52
|
}
|
|
52
53
|
}, { passive: true }),
|
|
53
54
|
on(document, 'click', (e) => {
|
|
55
|
+
const target = /** @type {Node} */ (e.target);
|
|
54
56
|
if (
|
|
55
57
|
this._activeWrapper &&
|
|
56
|
-
!this._activeWrapper.contains(
|
|
57
|
-
!this._el.contains(
|
|
58
|
+
!this._activeWrapper.contains(target) &&
|
|
59
|
+
!this._el.contains(target)
|
|
58
60
|
) {
|
|
59
61
|
this._hide();
|
|
60
62
|
}
|
|
61
63
|
}),
|
|
64
|
+
// Hide when the page scrolls or resizes — the tooltip position becomes stale
|
|
65
|
+
on(globalThis, 'scroll', () => this._hide(), { passive: true }),
|
|
66
|
+
on(globalThis, 'resize', () => this._hide(), { passive: true }),
|
|
62
67
|
);
|
|
63
68
|
|
|
64
69
|
return this;
|
|
@@ -69,7 +74,7 @@ export class VideoTooltip {
|
|
|
69
74
|
this._clearTimers();
|
|
70
75
|
this._disposers.forEach((d) => d());
|
|
71
76
|
this._disposers = [];
|
|
72
|
-
|
|
77
|
+
this._el?.remove();
|
|
73
78
|
this._el = null;
|
|
74
79
|
}
|
|
75
80
|
|
|
@@ -174,7 +179,7 @@ export class VideoTooltip {
|
|
|
174
179
|
this._hideTimer = setTimeout(() => this._hide(), HIDE_DELAY);
|
|
175
180
|
}
|
|
176
181
|
|
|
177
|
-
_show(
|
|
182
|
+
_show(_wrapper) {
|
|
178
183
|
this._el.style.display = 'flex';
|
|
179
184
|
// Defer: offsetWidth on a newly-visible element forces synchronous layout
|
|
180
185
|
requestAnimationFrame(() => {
|
|
@@ -205,8 +210,8 @@ export class VideoTooltip {
|
|
|
205
210
|
let top = rect.bottom + margin;
|
|
206
211
|
let left = rect.left + (rect.width - tipW) / 2;
|
|
207
212
|
|
|
208
|
-
if (top + tipH >
|
|
209
|
-
if (left + tipW >
|
|
213
|
+
if (top + tipH > globalThis.innerHeight - margin) top = rect.top - tipH - margin;
|
|
214
|
+
if (left + tipW > globalThis.innerWidth - margin) left = globalThis.innerWidth - tipW - margin;
|
|
210
215
|
if (left < margin) left = margin;
|
|
211
216
|
|
|
212
217
|
this._el.style.top = `${top}px`;
|
|
@@ -263,7 +268,7 @@ export class VideoTooltip {
|
|
|
263
268
|
if (!wrapper) return;
|
|
264
269
|
this._hide();
|
|
265
270
|
this.context.invoke('videoResizer.deselect');
|
|
266
|
-
|
|
271
|
+
wrapper.remove();
|
|
267
272
|
this.context.invoke('editor.afterCommand');
|
|
268
273
|
}
|
|
269
274
|
|
package/src/js/renderer.js
CHANGED
|
@@ -40,13 +40,13 @@ export function renderLayout(targetEl, options) {
|
|
|
40
40
|
}
|
|
41
41
|
if (!initialContent) {
|
|
42
42
|
initialContent = targetEl.tagName === 'TEXTAREA'
|
|
43
|
-
? (targetEl.value || '').trim()
|
|
43
|
+
? ((/** @type {HTMLTextAreaElement} */ (targetEl)).value || '').trim()
|
|
44
44
|
: (targetEl.innerHTML || '').trim();
|
|
45
45
|
}
|
|
46
46
|
editable.innerHTML = sanitiseHTML(initialContent, { allowIframes: true });
|
|
47
47
|
|
|
48
48
|
// Apply default font family so the editable renders in the configured font
|
|
49
|
-
const defaultFont = options.defaultFontFamily ||
|
|
49
|
+
const defaultFont = options.defaultFontFamily || options.fontFamilies?.[0];
|
|
50
50
|
if (defaultFont) {
|
|
51
51
|
editable.style.fontFamily = defaultFont;
|
|
52
52
|
}
|
|
@@ -70,9 +70,11 @@ export function renderLayout(targetEl, options) {
|
|
|
70
70
|
|
|
71
71
|
container.appendChild(editable);
|
|
72
72
|
|
|
73
|
-
// Apply dark theme
|
|
73
|
+
// Apply dark theme — also add to body so floating elements (dialogs, tooltips,
|
|
74
|
+
// popovers) appended to document.body inherit the dark CSS rules.
|
|
74
75
|
if (options.theme === 'dark') {
|
|
75
76
|
container.classList.add('an-theme-dark');
|
|
77
|
+
document.body.classList.add('an-theme-dark');
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
// Read-only mode
|
package/src/js/settings.js
CHANGED
|
@@ -7,48 +7,65 @@ import { defaultToolbar } from './module/Buttons.js';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {object} AsnOptions
|
|
10
|
-
* @property {string} [placeholder]
|
|
11
|
-
* @property {number} [height]
|
|
12
|
-
* @property {number} [minHeight]
|
|
13
|
-
* @property {number} [maxHeight]
|
|
14
|
-
* @property {boolean} [focus]
|
|
15
|
-
* @property {boolean} [resizable]
|
|
16
|
-
* @property {Array} [toolbar]
|
|
17
|
-
* @property {boolean} [
|
|
18
|
-
* @property {
|
|
10
|
+
* @property {string} [placeholder] - Placeholder text when editor is empty
|
|
11
|
+
* @property {number} [height] - Editor height in px (min)
|
|
12
|
+
* @property {number} [minHeight] - Minimum height in px
|
|
13
|
+
* @property {number} [maxHeight] - Maximum height in px (0 = unlimited)
|
|
14
|
+
* @property {boolean} [focus] - Auto-focus on init
|
|
15
|
+
* @property {boolean} [resizable] - Show resize handle
|
|
16
|
+
* @property {Array} [toolbar] - Toolbar button group config
|
|
17
|
+
* @property {boolean} [useBootstrap] - Use Bootstrap button classes on toolbar buttons
|
|
18
|
+
* @property {string} [toolbarButtonClass] - CSS classes for Bootstrap toolbar buttons
|
|
19
|
+
* @property {boolean} [useFontAwesome] - Use Font Awesome icons (default: true)
|
|
20
|
+
* @property {string} [fontAwesomeClass] - Font Awesome prefix class, e.g. 'fas' or 'fa-solid'
|
|
21
|
+
* @property {boolean} [pasteAsPlainText] - Force plain-text paste
|
|
22
|
+
* @property {boolean} [pasteCleanHTML] - Sanitise HTML on paste
|
|
19
23
|
* @property {boolean} [pasteStripAttributes] - Strip class/style/data-* from pasted HTML (default: false)
|
|
20
|
-
* @property {boolean} [allowImageUpload]
|
|
21
|
-
* @property {number} [maxImageSize]
|
|
22
|
-
* @property {number} [tabSize]
|
|
23
|
-
* @property {
|
|
24
|
-
* @property {
|
|
25
|
-
* @property {
|
|
26
|
-
* @property {
|
|
27
|
-
* @property {
|
|
28
|
-
* @property {
|
|
29
|
-
* @property {
|
|
30
|
-
* @property {
|
|
31
|
-
* @property {
|
|
32
|
-
* @property {
|
|
33
|
-
* @property {boolean} [
|
|
34
|
-
* @property {
|
|
35
|
-
* @property {string} [
|
|
36
|
-
* @property {
|
|
37
|
-
* @property {
|
|
38
|
-
* @property {
|
|
39
|
-
* @property {
|
|
40
|
-
* @property {
|
|
41
|
-
* @property {
|
|
42
|
-
* @property {
|
|
43
|
-
* @property {
|
|
44
|
-
* @property {string
|
|
24
|
+
* @property {boolean} [allowImageUpload] - Allow file upload in image dialog
|
|
25
|
+
* @property {number} [maxImageSize] - Max upload size in MB
|
|
26
|
+
* @property {number} [tabSize] - Spaces per tab in non-list context
|
|
27
|
+
* @property {number} [historyLimit] - Maximum undo/redo history steps
|
|
28
|
+
* @property {string} [defaultFontFamily] - Default font family applied to the editable area on init
|
|
29
|
+
* @property {string} [defaultFontSize] - Default font size applied to the editable area on init (e.g. '14px')
|
|
30
|
+
* @property {string[]} [fontFamilies] - Font families shown in the font-family toolbar dropdown
|
|
31
|
+
* @property {Function} [onChange] - Callback on content change
|
|
32
|
+
* @property {Function} [onFocus] - Callback on focus
|
|
33
|
+
* @property {Function} [onBlur] - Callback on blur
|
|
34
|
+
* @property {Function} [onInit] - Callback after the editor has initialised
|
|
35
|
+
* @property {Function} [onImageUpload] - Custom upload handler: (files) => void
|
|
36
|
+
* @property {Function} [onImageError] - Callback when an image upload error occurs
|
|
37
|
+
* @property {boolean} [stickyToolbar] - Stick the toolbar to the viewport top when scrolling
|
|
38
|
+
* @property {number} [stickyToolbarOffset] - Top offset in px for sticky toolbar (e.g. fixed nav height)
|
|
39
|
+
* @property {string} [theme] - 'light' (default) | 'dark'
|
|
40
|
+
* @property {boolean} [codeHighlight] - Auto-load Prism.js for syntax highlighting of code blocks
|
|
41
|
+
* @property {string} [codeHighlightCDN] - CDN base URL for Prism assets (defaults to cdnjs)
|
|
42
|
+
* @property {boolean} [markdownPaste] - Convert pasted Markdown text to HTML (default: true)
|
|
43
|
+
* @property {boolean} [readOnly] - Start editor in read-only / non-editable mode
|
|
44
|
+
* @property {boolean} [spellcheck] - Enable browser spellcheck in the editable area (default: true)
|
|
45
|
+
* @property {string} [direction] - Text direction: 'ltr' (default) | 'rtl'
|
|
46
|
+
* @property {string} [toolbarOverflow] - Toolbar overflow strategy: 'wrap' (default) | 'scroll'
|
|
47
|
+
* @property {boolean} [autoSave] - Auto-save content to localStorage on change
|
|
48
|
+
* @property {string} [autoSaveKey] - localStorage key used for auto-save (default: 'autumnnote-autosave')
|
|
49
|
+
* @property {number} [maxChars] - Maximum character count (0 = unlimited). Shows warning in statusbar.
|
|
50
|
+
* @property {number} [maxWords] - Maximum word count (0 = unlimited). Shows warning in statusbar.
|
|
51
|
+
* @property {boolean} [tableHeaderRow] - Insert a header row (<thead><th>) when creating tables
|
|
52
|
+
* @property {Function} [onPaste] - Callback fired on every paste: ({ text, html }) => void
|
|
53
|
+
* @property {Function} [onSelectionChange] - Callback fired on cursor/selection change: (context) => void
|
|
54
|
+
* @property {string[]} [colorSwatches] - Custom brand colour swatches prepended to the colour-picker palette
|
|
45
55
|
* @property {Function} [onDestroy] - Callback fired when the editor is destroyed: (context) => void
|
|
46
56
|
* @property {Function} [onCharLimitReached] - Callback fired when the character limit is hit: (context) => void
|
|
47
57
|
* @property {Function} [onWordLimitReached] - Callback fired when the word limit is hit: (context) => void
|
|
48
|
-
* @property {string} [focusColor]
|
|
58
|
+
* @property {string} [focusColor] - Custom focus ring colour, e.g. '#f97316'. Overrides the default blue.
|
|
59
|
+
* @property {boolean} [autoSaveRestore] - Show a restore banner when a previously auto-saved draft exists
|
|
60
|
+
* @property {number} [autoSaveRestoreTimeout] - Maximum age in days for a draft to be offered for restore (0 = no expiry)
|
|
61
|
+
* @property {Function} [onAutoSaveRestore] - Callback fired after the user chooses to restore a draft
|
|
62
|
+
* @property {boolean} [markdownShortcuts] - Convert markdown syntax typed inline to HTML
|
|
63
|
+
* @property {boolean} [bubbleToolbar] - Show a mini floating toolbar above text selections
|
|
64
|
+
* @property {string[]} [bubbleToolbarItems] - Button names for the bubble toolbar
|
|
65
|
+
* @property {object|null} [mention] - @mention configuration (onSearch, minChars, ...)
|
|
66
|
+
* @property {string} [lang] - Display language or partial locale object override
|
|
49
67
|
*/
|
|
50
68
|
|
|
51
|
-
/** @type {AsnOptions} */
|
|
52
69
|
export const defaultOptions = {
|
|
53
70
|
placeholder: '',
|
|
54
71
|
height: 200,
|