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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ContextMenu.js - Right-click context menu for editor actions
|
|
1
|
+
// ContextMenu.js - Right-click context menu for editor actions
|
|
2
2
|
import { createElement, on } from '../core/dom.js';
|
|
3
3
|
import { sanitiseHTML } from '../core/sanitise.js';
|
|
4
4
|
|
|
@@ -60,7 +60,7 @@ const defaultItems = [
|
|
|
60
60
|
{ name: 'copy', label: 'Copy', icon: ICONS.copy, action: () => document.execCommand('copy') },
|
|
61
61
|
{ name: 'paste', label: 'Paste', icon: ICONS.paste, action: (ctx) => {
|
|
62
62
|
if (!navigator.clipboard) return;
|
|
63
|
-
const editable = ctx.layoutInfo
|
|
63
|
+
const editable = ctx.layoutInfo?.editable;
|
|
64
64
|
if (!editable) return;
|
|
65
65
|
// Prefer read() to get rich HTML; fall back to readText() for plain text
|
|
66
66
|
const doInsert = (html, text) => {
|
|
@@ -131,24 +131,24 @@ export class ContextMenu {
|
|
|
131
131
|
|
|
132
132
|
this._renderItems(this._items);
|
|
133
133
|
|
|
134
|
-
const editable = this.context.layoutInfo
|
|
134
|
+
const editable = this.context.layoutInfo?.editable;
|
|
135
135
|
if (editable) {
|
|
136
136
|
this._disposers.push(on(editable, 'contextmenu', (e) => this._onContextMenu(e)));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
this._disposers.push(on(document, 'click', (e) => this._maybeHide(e)));
|
|
140
|
-
this._disposers.push(on(document, 'keydown', (e) => { if (e.key === 'Escape') this.hide(); }));
|
|
141
|
-
this._disposers.push(on(
|
|
140
|
+
this._disposers.push(on(document, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Escape') this.hide(); }));
|
|
141
|
+
this._disposers.push(on(globalThis, 'scroll', () => this.hide(), { passive: true }));
|
|
142
142
|
|
|
143
143
|
return this;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
destroy() {
|
|
147
|
-
this._menuDisposers.forEach((d) => { try { d(); } catch (
|
|
147
|
+
this._menuDisposers.forEach((d) => { try { d(); } catch (_e) {} });
|
|
148
148
|
this._menuDisposers = [];
|
|
149
|
-
this._disposers.forEach((d) => { try { d(); } catch (
|
|
149
|
+
this._disposers.forEach((d) => { try { d(); } catch (_e) {} });
|
|
150
150
|
this._disposers = [];
|
|
151
|
-
if (this.el
|
|
151
|
+
if (this.el) this.el.remove();
|
|
152
152
|
this.el = null;
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -176,8 +176,8 @@ export class ContextMenu {
|
|
|
176
176
|
backBtn.appendChild(createElement('span', { class: 'an-context-label' }, [backLabel]));
|
|
177
177
|
const off = on(backBtn, 'click', (e) => {
|
|
178
178
|
e.stopPropagation();
|
|
179
|
-
const curLeft = parseFloat(this.el.style.left);
|
|
180
|
-
const curTop = parseFloat(this.el.style.top);
|
|
179
|
+
const curLeft = Number.parseFloat(this.el.style.left);
|
|
180
|
+
const curTop = Number.parseFloat(this.el.style.top);
|
|
181
181
|
this._renderItems(it.navigate());
|
|
182
182
|
this._reposition(curLeft, curTop);
|
|
183
183
|
});
|
|
@@ -212,8 +212,8 @@ export class ContextMenu {
|
|
|
212
212
|
btn.appendChild(chevron);
|
|
213
213
|
const off = on(btn, 'click', (e) => {
|
|
214
214
|
e.stopPropagation();
|
|
215
|
-
const curLeft = parseFloat(this.el.style.left);
|
|
216
|
-
const curTop = parseFloat(this.el.style.top);
|
|
215
|
+
const curLeft = Number.parseFloat(this.el.style.left);
|
|
216
|
+
const curTop = Number.parseFloat(this.el.style.top);
|
|
217
217
|
this._renderItems(it.navigate());
|
|
218
218
|
this._reposition(curLeft, curTop);
|
|
219
219
|
});
|
|
@@ -253,12 +253,12 @@ export class ContextMenu {
|
|
|
253
253
|
|
|
254
254
|
// Custom color row
|
|
255
255
|
const customRow = createElement('div', { class: 'an-context-color-custom' });
|
|
256
|
-
const colorInput = createElement('input', {
|
|
256
|
+
const colorInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
257
257
|
type: 'color',
|
|
258
258
|
value: it.colorType === 'foreColor' ? '#000000' : '#ffff00',
|
|
259
259
|
title: this.context.locale.contextMenu.customColor || 'Custom color',
|
|
260
260
|
'aria-label': this.context.locale.contextMenu.customColor || 'Custom color',
|
|
261
|
-
});
|
|
261
|
+
}));
|
|
262
262
|
const customLabel = createElement('span', {}, [this.context.locale.contextMenu.customColorLabel || 'Custom…']);
|
|
263
263
|
const offCustom = on(colorInput, 'change', () => this._applyColor(it.colorType, colorInput.value));
|
|
264
264
|
this._menuDisposers.push(offCustom);
|
|
@@ -324,20 +324,20 @@ export class ContextMenu {
|
|
|
324
324
|
this._menuDisposers.push(offHeader);
|
|
325
325
|
|
|
326
326
|
const offMove = on(gridEl, 'mousemove', (e) => {
|
|
327
|
-
const cell = e.target
|
|
327
|
+
const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-row]'));
|
|
328
328
|
if (!cell) return;
|
|
329
329
|
setHighlight(+cell.dataset.row, +cell.dataset.col);
|
|
330
330
|
});
|
|
331
331
|
const offLeave = on(gridEl, 'mouseleave', () => setHighlight(0, 0));
|
|
332
332
|
const offClick = on(gridEl, 'click', (e) => {
|
|
333
|
-
const cell = e.target
|
|
333
|
+
const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-row]'));
|
|
334
334
|
if (!cell) return;
|
|
335
335
|
const rows = +cell.dataset.row;
|
|
336
336
|
const cols = +cell.dataset.col;
|
|
337
|
-
const editable = this.context.layoutInfo
|
|
337
|
+
const editable = this.context.layoutInfo?.editable;
|
|
338
338
|
if (editable && this._savedRange) {
|
|
339
339
|
editable.focus();
|
|
340
|
-
const sel =
|
|
340
|
+
const sel = globalThis.getSelection();
|
|
341
341
|
sel.removeAllRanges();
|
|
342
342
|
sel.addRange(this._savedRange.cloneRange());
|
|
343
343
|
}
|
|
@@ -354,7 +354,7 @@ export class ContextMenu {
|
|
|
354
354
|
|
|
355
355
|
// Regular item
|
|
356
356
|
const btn = createElement('button', { type: 'button', class: 'an-context-item', 'data-name': it.name || '' });
|
|
357
|
-
if (typeof it.disabled === 'function' ? it.disabled(this.context) : !!it.disabled) btn.disabled = true;
|
|
357
|
+
if (typeof it.disabled === 'function' ? it.disabled(this.context) : !!it.disabled) /** @type {HTMLButtonElement} */ (btn).disabled = true;
|
|
358
358
|
if (it.icon) {
|
|
359
359
|
const iconSpan = createElement('span', { class: 'an-context-icon', 'aria-hidden': 'true' });
|
|
360
360
|
iconSpan.innerHTML = it.icon;
|
|
@@ -372,13 +372,13 @@ export class ContextMenu {
|
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
_onContextMenu(event) {
|
|
375
|
-
const editable = this.context.layoutInfo
|
|
375
|
+
const editable = this.context.layoutInfo?.editable;
|
|
376
376
|
if (!editable) return;
|
|
377
377
|
if (!editable.contains(event.target)) return;
|
|
378
378
|
if (this.context.layoutInfo.container.classList.contains('an-disabled')) return;
|
|
379
379
|
event.preventDefault();
|
|
380
380
|
|
|
381
|
-
const winSel =
|
|
381
|
+
const winSel = globalThis.getSelection();
|
|
382
382
|
this._savedRange = (winSel && winSel.rangeCount > 0) ? winSel.getRangeAt(0).cloneRange() : null;
|
|
383
383
|
this._renderItems(this._items);
|
|
384
384
|
|
|
@@ -386,7 +386,7 @@ export class ContextMenu {
|
|
|
386
386
|
// Only apply when there is a real (non-collapsed) selection — a collapsed range
|
|
387
387
|
// (cursor only) also has height > 0, which would misplace the menu relative
|
|
388
388
|
// to the actual click point.
|
|
389
|
-
|
|
389
|
+
const openX = event.clientX;
|
|
390
390
|
let openY = event.clientY;
|
|
391
391
|
if (this._savedRange && !this._savedRange.collapsed) {
|
|
392
392
|
try {
|
|
@@ -425,9 +425,9 @@ export class ContextMenu {
|
|
|
425
425
|
let left = rx;
|
|
426
426
|
let top = ry;
|
|
427
427
|
// Clamp to viewport so the menu never overflows the screen edge
|
|
428
|
-
if (left + w >
|
|
428
|
+
if (left + w > globalThis.innerWidth - 8) left = globalThis.innerWidth - w - 8;
|
|
429
429
|
if (left < 8) left = 8;
|
|
430
|
-
if (top + h >
|
|
430
|
+
if (top + h > globalThis.innerHeight - 8) top = globalThis.innerHeight - h - 8;
|
|
431
431
|
if (top < 8) top = 8;
|
|
432
432
|
this.el.style.left = `${left}px`;
|
|
433
433
|
this.el.style.top = `${top}px`;
|
|
@@ -454,7 +454,7 @@ export class ContextMenu {
|
|
|
454
454
|
let node = range.startContainer;
|
|
455
455
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
|
456
456
|
if (!node) return type === 'foreColor' ? '#000000' : 'transparent';
|
|
457
|
-
const cs =
|
|
457
|
+
const cs = globalThis.getComputedStyle(/** @type {Element} */ (node));
|
|
458
458
|
if (type === 'foreColor') {
|
|
459
459
|
return cs.color || '#000000';
|
|
460
460
|
}
|
|
@@ -464,10 +464,10 @@ export class ContextMenu {
|
|
|
464
464
|
|
|
465
465
|
/** Restore selection, apply a color command, then hide the menu. */
|
|
466
466
|
_applyColor(type, color) {
|
|
467
|
-
const editable = this.context.layoutInfo
|
|
467
|
+
const editable = this.context.layoutInfo?.editable;
|
|
468
468
|
if (!editable || !this._savedRange) return;
|
|
469
469
|
editable.focus();
|
|
470
|
-
const sel =
|
|
470
|
+
const sel = globalThis.getSelection();
|
|
471
471
|
sel.removeAllRanges();
|
|
472
472
|
sel.addRange(this._savedRange.cloneRange());
|
|
473
473
|
document.execCommand(type, false, color);
|
|
@@ -482,20 +482,20 @@ export class ContextMenu {
|
|
|
482
482
|
copyFormat() {
|
|
483
483
|
const range = this._savedRange;
|
|
484
484
|
if (!range) return;
|
|
485
|
-
const editable = this.context.layoutInfo
|
|
485
|
+
const editable = this.context.layoutInfo?.editable;
|
|
486
486
|
let node = range.startContainer;
|
|
487
487
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
|
488
488
|
if (!node || !editable || !editable.contains(node)) return;
|
|
489
489
|
|
|
490
490
|
// Walk up to collect explicitly-set inline properties from the nearest styled ancestor
|
|
491
|
-
const cs =
|
|
491
|
+
const cs = globalThis.getComputedStyle(/** @type {Element} */ (node));
|
|
492
492
|
|
|
493
493
|
// Detect if an explicit font-family / font-size is set on an element (not just inherited)
|
|
494
494
|
const explicitFontFamily = this._findExplicitStyle(node, editable, 'fontFamily');
|
|
495
495
|
const explicitFontSize = this._findExplicitStyle(node, editable, 'fontSize');
|
|
496
496
|
|
|
497
497
|
this._copiedFormat = {
|
|
498
|
-
bold: parseInt(cs.fontWeight, 10) >= 700,
|
|
498
|
+
bold: Number.parseInt(cs.fontWeight, 10) >= 700,
|
|
499
499
|
italic: cs.fontStyle === 'italic' || cs.fontStyle === 'oblique',
|
|
500
500
|
underline: (cs.textDecorationLine || '').includes('underline'),
|
|
501
501
|
strikethrough: (cs.textDecorationLine || '').includes('line-through'),
|
|
@@ -533,13 +533,13 @@ export class ContextMenu {
|
|
|
533
533
|
pasteFormat() {
|
|
534
534
|
if (!this._copiedFormat || !this._savedRange) return;
|
|
535
535
|
const fmt = this._copiedFormat;
|
|
536
|
-
const editable = this.context.layoutInfo
|
|
536
|
+
const editable = this.context.layoutInfo?.editable;
|
|
537
537
|
if (!editable) return;
|
|
538
538
|
|
|
539
539
|
// CRITICAL: focus the editable FIRST, then restore selection.
|
|
540
540
|
// Calling focus() after addRange() can wipe the selection in Chrome/Firefox.
|
|
541
541
|
editable.focus();
|
|
542
|
-
const sel =
|
|
542
|
+
const sel = globalThis.getSelection();
|
|
543
543
|
sel.removeAllRanges();
|
|
544
544
|
sel.addRange(this._savedRange.cloneRange());
|
|
545
545
|
|
|
@@ -575,14 +575,14 @@ export class ContextMenu {
|
|
|
575
575
|
document.execCommand('fontSize', false, '7');
|
|
576
576
|
// Mark only the NEWLY created font[size="7"] elements.
|
|
577
577
|
editable.querySelectorAll('font[size="7"]').forEach((el) => {
|
|
578
|
-
if (!preExisting.has(el)) el.
|
|
578
|
+
if (!preExisting.has(el)) /** @type {HTMLElement} */ (el).dataset.anTmp = marker;
|
|
579
579
|
});
|
|
580
580
|
editable.querySelectorAll(`[data-an-tmp="${marker}"]`).forEach((el) => {
|
|
581
581
|
const span = document.createElement('span');
|
|
582
582
|
span.style.fontSize = fmt.fontSize;
|
|
583
583
|
el.parentNode.insertBefore(span, el);
|
|
584
584
|
while (el.firstChild) span.appendChild(el.firstChild);
|
|
585
|
-
el.
|
|
585
|
+
el.remove();
|
|
586
586
|
});
|
|
587
587
|
}
|
|
588
588
|
|
|
@@ -592,11 +592,11 @@ export class ContextMenu {
|
|
|
592
592
|
/** Strip all inline formatting from the saved selection. */
|
|
593
593
|
removeFormat() {
|
|
594
594
|
if (!this._savedRange) return;
|
|
595
|
-
const editable = this.context.layoutInfo
|
|
595
|
+
const editable = this.context.layoutInfo?.editable;
|
|
596
596
|
if (!editable) return;
|
|
597
597
|
|
|
598
598
|
editable.focus();
|
|
599
|
-
const sel =
|
|
599
|
+
const sel = globalThis.getSelection();
|
|
600
600
|
sel.removeAllRanges();
|
|
601
601
|
sel.addRange(this._savedRange.cloneRange());
|
|
602
602
|
|
|
@@ -609,7 +609,7 @@ export class ContextMenu {
|
|
|
609
609
|
let el;
|
|
610
610
|
while ((el = iter.nextNode())) {
|
|
611
611
|
if (!editable.contains(el) || el === editable) continue;
|
|
612
|
-
try { if (range.intersectsNode(el)) el.removeAttribute('style'); } catch { /* ignore */ }
|
|
612
|
+
try { if (range.intersectsNode(el)) /** @type {Element} */ (el).removeAttribute('style'); } catch { /* ignore */ }
|
|
613
613
|
}
|
|
614
614
|
|
|
615
615
|
this.context.invoke('editor.afterCommand');
|
package/src/js/module/Editor.js
CHANGED
|
@@ -12,6 +12,7 @@ import { handleKeydown } from '../editing/Typing.js';
|
|
|
12
12
|
import { on } from '../core/dom.js';
|
|
13
13
|
import { sanitiseHTML, sanitiseUrl } from '../core/sanitise.js';
|
|
14
14
|
import { markdownToHTML, htmlToMarkdown } from '../core/markdown.js';
|
|
15
|
+
import { detectLang } from '../core/detectLang.js';
|
|
15
16
|
|
|
16
17
|
export class Editor {
|
|
17
18
|
/**
|
|
@@ -60,7 +61,7 @@ export class Editor {
|
|
|
60
61
|
// Refresh toolbar on selection change, scoped to this editor
|
|
61
62
|
const onSelChange = () => {
|
|
62
63
|
if (!this.context._alive) return;
|
|
63
|
-
const sel =
|
|
64
|
+
const sel = globalThis.getSelection();
|
|
64
65
|
if (sel && sel.rangeCount > 0 && editable.contains(sel.anchorNode)) {
|
|
65
66
|
this.context.invoke('toolbar.refresh');
|
|
66
67
|
if (typeof this.options.onSelectionChange === 'function') {
|
|
@@ -84,7 +85,7 @@ export class Editor {
|
|
|
84
85
|
// lands WHERE the user actually clicked (middle, end of text…).
|
|
85
86
|
// keyup : arrow-key navigation may land at <li>[0]; move to start-of-text.
|
|
86
87
|
const fixChecklistCursor = (event) => {
|
|
87
|
-
const sel =
|
|
88
|
+
const sel = globalThis.getSelection();
|
|
88
89
|
if (!sel || !sel.rangeCount) return;
|
|
89
90
|
const r = sel.getRangeAt(0);
|
|
90
91
|
if (!r.collapsed) return;
|
|
@@ -93,7 +94,8 @@ export class Editor {
|
|
|
93
94
|
// Only act when cursor is at the <li> element node itself (not inside
|
|
94
95
|
// a text node — the browser already placed it correctly in that case).
|
|
95
96
|
if (sc.nodeType !== Node.ELEMENT_NODE) return;
|
|
96
|
-
const
|
|
97
|
+
const scEl = /** @type {Element} */ (sc);
|
|
98
|
+
const li = scEl.matches('.an-checklist li') ? scEl : null;
|
|
97
99
|
if (!li) return;
|
|
98
100
|
const cb = li.querySelector('input[type="checkbox"]');
|
|
99
101
|
if (!cb) return;
|
|
@@ -160,9 +162,9 @@ export class Editor {
|
|
|
160
162
|
// the mouse and moving outside the wrapper before releasing.
|
|
161
163
|
on(editable, 'dragstart', (e) => {
|
|
162
164
|
if (isReadOnly()) { e.preventDefault(); return; }
|
|
163
|
-
const target = e.target;
|
|
165
|
+
const target = /** @type {Element} */ (e.target);
|
|
164
166
|
if (target && (target.nodeName === 'IFRAME' ||
|
|
165
|
-
|
|
167
|
+
target.closest('.an-video-wrapper'))) {
|
|
166
168
|
e.preventDefault();
|
|
167
169
|
}
|
|
168
170
|
}),
|
|
@@ -178,13 +180,14 @@ export class Editor {
|
|
|
178
180
|
/** @type {string|null} 'superscript' | 'subscript' | null */
|
|
179
181
|
let _compositionSupSub = null;
|
|
180
182
|
const onCompositionStart = () => {
|
|
181
|
-
const sel =
|
|
183
|
+
const sel = globalThis.getSelection();
|
|
182
184
|
if (!sel || !sel.rangeCount) { _compositionSupSub = null; return; }
|
|
183
185
|
let node = sel.getRangeAt(0).startContainer;
|
|
184
186
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
|
185
|
-
if (node
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
if (node) {
|
|
188
|
+
const el = /** @type {Element} */ (node);
|
|
189
|
+
if (el.closest('sup')) _compositionSupSub = 'superscript';
|
|
190
|
+
else if (el.closest('sub')) _compositionSupSub = 'subscript';
|
|
188
191
|
else _compositionSupSub = null;
|
|
189
192
|
}
|
|
190
193
|
};
|
|
@@ -192,12 +195,12 @@ export class Editor {
|
|
|
192
195
|
const tag = _compositionSupSub;
|
|
193
196
|
_compositionSupSub = null;
|
|
194
197
|
if (!tag) return;
|
|
195
|
-
const sel =
|
|
198
|
+
const sel = globalThis.getSelection();
|
|
196
199
|
if (!sel || !sel.rangeCount) return;
|
|
197
200
|
let node = sel.getRangeAt(0).startContainer;
|
|
198
201
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
|
199
|
-
const
|
|
200
|
-
|
|
202
|
+
const el = /** @type {Element} */ (node);
|
|
203
|
+
const inContext = tag === 'superscript' ? el?.closest('sup') : el?.closest('sub');
|
|
201
204
|
if (!inContext) {
|
|
202
205
|
// The composed character escaped the sup/sub — re-apply the format.
|
|
203
206
|
document.execCommand(tag);
|
|
@@ -284,7 +287,7 @@ export class Editor {
|
|
|
284
287
|
if (!type.startsWith('insert')) return;
|
|
285
288
|
|
|
286
289
|
const text = this.context.layoutInfo.editable.innerText || '';
|
|
287
|
-
const chars = text.
|
|
290
|
+
const chars = text.replaceAll('\n', '').length;
|
|
288
291
|
|
|
289
292
|
if (maxChars && chars >= maxChars) {
|
|
290
293
|
event.preventDefault();
|
|
@@ -314,6 +317,9 @@ export class Editor {
|
|
|
314
317
|
// C4: Remove figure.an-figure elements whose <img> has been deleted so
|
|
315
318
|
// orphaned figcaptions do not accumulate in the DOM.
|
|
316
319
|
this._cleanOrphanedFigures();
|
|
320
|
+
// Ensure the editable always ends with a paragraph so the user can click
|
|
321
|
+
// and type after block elements that trap the cursor (pre, table, etc.).
|
|
322
|
+
this._ensureTrailingParagraph();
|
|
317
323
|
// Immediate: keep toolbar and statusbar in sync on every mutation.
|
|
318
324
|
this.context.invoke('toolbar.refresh');
|
|
319
325
|
this.context.invoke('statusbar.update');
|
|
@@ -346,11 +352,30 @@ export class Editor {
|
|
|
346
352
|
const editable = this.context.layoutInfo.editable;
|
|
347
353
|
editable.querySelectorAll('figure.an-figure').forEach((fig) => {
|
|
348
354
|
if (!fig.querySelector('img')) {
|
|
349
|
-
fig.
|
|
355
|
+
fig.remove();
|
|
350
356
|
}
|
|
351
357
|
});
|
|
352
358
|
}
|
|
353
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Ensures the editable always ends with a plain paragraph so the cursor can
|
|
362
|
+
* be placed after block elements that do not naturally allow it
|
|
363
|
+
* (pre, blockquote, table, figure, ul, ol, hr).
|
|
364
|
+
* Without this, clicking below the last such element does nothing.
|
|
365
|
+
*/
|
|
366
|
+
_ensureTrailingParagraph() {
|
|
367
|
+
const editable = this.context.layoutInfo.editable;
|
|
368
|
+
if (!editable) return;
|
|
369
|
+
const last = editable.lastElementChild;
|
|
370
|
+
if (!last) return;
|
|
371
|
+
const TRAPPING = new Set(['PRE', 'BLOCKQUOTE', 'TABLE', 'FIGURE', 'UL', 'OL', 'HR']);
|
|
372
|
+
if (TRAPPING.has(last.nodeName)) {
|
|
373
|
+
const p = document.createElement('p');
|
|
374
|
+
p.innerHTML = '<br>';
|
|
375
|
+
editable.appendChild(p);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
354
379
|
// ---------------------------------------------------------------------------
|
|
355
380
|
// Focus management
|
|
356
381
|
// ---------------------------------------------------------------------------
|
|
@@ -370,7 +395,7 @@ export class Editor {
|
|
|
370
395
|
*/
|
|
371
396
|
getHTML() {
|
|
372
397
|
// Strip zero-width spaces inserted after icons to allow caret placement.
|
|
373
|
-
const raw = this.context.layoutInfo.editable.innerHTML.
|
|
398
|
+
const raw = this.context.layoutInfo.editable.innerHTML.replaceAll('\u200B', '');
|
|
374
399
|
// Replace any blob: URLs (lightweight DOM references to pasted/dropped images)
|
|
375
400
|
// with their original data URLs so the returned HTML is fully self-contained.
|
|
376
401
|
return this.context.invoke('clipboard.resolveImages', raw) ?? raw;
|
|
@@ -425,7 +450,7 @@ export class Editor {
|
|
|
425
450
|
isEmpty() {
|
|
426
451
|
const text = (this.context.layoutInfo.editable.innerText || '')
|
|
427
452
|
.trim()
|
|
428
|
-
.
|
|
453
|
+
.replaceAll('\u00a0', '');
|
|
429
454
|
const hasMedia = !!this.context.layoutInfo.editable.querySelector('img, video, iframe, table');
|
|
430
455
|
return !text && !hasMedia;
|
|
431
456
|
}
|
|
@@ -523,9 +548,35 @@ export class Editor {
|
|
|
523
548
|
print() { this.context.print(); }
|
|
524
549
|
|
|
525
550
|
/**
|
|
526
|
-
* @param {string} tagName - e.g. 'h1', 'p', 'blockquote'
|
|
551
|
+
* @param {string} tagName - e.g. 'h1', 'p', 'blockquote', 'pre'
|
|
527
552
|
*/
|
|
528
|
-
formatBlock(tagName) {
|
|
553
|
+
formatBlock(tagName) {
|
|
554
|
+
Style.formatBlock(tagName);
|
|
555
|
+
|
|
556
|
+
// Auto-detect the programming language when the user formats a code block.
|
|
557
|
+
// Only runs when converting TO <pre> and the block has no language yet.
|
|
558
|
+
if (tagName === 'pre') {
|
|
559
|
+
const sel = globalThis.getSelection();
|
|
560
|
+
if (sel && sel.rangeCount > 0) {
|
|
561
|
+
const container = sel.getRangeAt(0).commonAncestorContainer;
|
|
562
|
+
const pre = /** @type {Element|null} */ (
|
|
563
|
+
container.nodeType === 1
|
|
564
|
+
? /** @type {Element} */ (container).closest('pre')
|
|
565
|
+
: (/** @type {Element|null} */ (container.parentElement))?.closest('pre')
|
|
566
|
+
);
|
|
567
|
+
if (pre && !/** @type {HTMLElement} */ (pre).dataset.language) {
|
|
568
|
+
const code = pre.textContent || '';
|
|
569
|
+
const lang = detectLang(code);
|
|
570
|
+
if (lang) {
|
|
571
|
+
this.context.invoke('codeTooltip.applyLanguage', pre, lang);
|
|
572
|
+
return; // applyLanguage already calls afterCommand internally
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this.afterCommand();
|
|
579
|
+
}
|
|
529
580
|
|
|
530
581
|
/**
|
|
531
582
|
* @param {string} color
|
|
@@ -566,7 +617,7 @@ export class Editor {
|
|
|
566
617
|
* @param {boolean} [openInNewTab=false]
|
|
567
618
|
*/
|
|
568
619
|
insertLink(url, text, openInNewTab = false) {
|
|
569
|
-
const sel =
|
|
620
|
+
const sel = globalThis.getSelection();
|
|
570
621
|
if (!sel || sel.rangeCount === 0) return;
|
|
571
622
|
const safeUrl = sanitiseUrl(url);
|
|
572
623
|
if (!safeUrl) return;
|
|
@@ -580,8 +631,8 @@ export class Editor {
|
|
|
580
631
|
if (openInNewTab) {
|
|
581
632
|
const link = this._getClosestAnchor();
|
|
582
633
|
if (link) {
|
|
583
|
-
link.setAttribute('target', '_blank');
|
|
584
|
-
link.setAttribute('rel', 'noopener noreferrer');
|
|
634
|
+
/** @type {Element} */ (link).setAttribute('target', '_blank');
|
|
635
|
+
/** @type {Element} */ (link).setAttribute('rel', 'noopener noreferrer');
|
|
585
636
|
}
|
|
586
637
|
}
|
|
587
638
|
}
|
|
@@ -641,7 +692,7 @@ export class Editor {
|
|
|
641
692
|
// ---------------------------------------------------------------------------
|
|
642
693
|
|
|
643
694
|
_getClosestAnchor() {
|
|
644
|
-
const sel =
|
|
695
|
+
const sel = globalThis.getSelection();
|
|
645
696
|
if (!sel || sel.rangeCount === 0) return null;
|
|
646
697
|
let node = sel.getRangeAt(0).startContainer;
|
|
647
698
|
while (node) {
|
|
@@ -658,10 +709,10 @@ export class Editor {
|
|
|
658
709
|
*/
|
|
659
710
|
_escapeAttr(str) {
|
|
660
711
|
return String(str ?? '')
|
|
661
|
-
.
|
|
662
|
-
.
|
|
663
|
-
.
|
|
664
|
-
.
|
|
712
|
+
.replaceAll('&', '&')
|
|
713
|
+
.replaceAll('"', '"')
|
|
714
|
+
.replaceAll('<', '<')
|
|
715
|
+
.replaceAll('>', '>');
|
|
665
716
|
}
|
|
666
717
|
|
|
667
718
|
// --- delegated to shared sanitise.js ---
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Click an emoji to insert it directly at the caret — no extra "Insert" step.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createElement, on, trapFocus } from '../core/dom.js';
|
|
6
|
+
import { createElement, on, trapFocus, makeDraggable } from '../core/dom.js';
|
|
7
7
|
import { withSavedRange } from '../core/range.js';
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
@@ -526,7 +526,7 @@ export class EmojiDialog {
|
|
|
526
526
|
this._disposers.forEach((d) => d());
|
|
527
527
|
this._disposers = [];
|
|
528
528
|
if (this._dialog && this._dialog.parentNode) {
|
|
529
|
-
this._dialog.
|
|
529
|
+
this._dialog.remove();
|
|
530
530
|
}
|
|
531
531
|
this._dialog = null;
|
|
532
532
|
}
|
|
@@ -566,19 +566,23 @@ export class EmojiDialog {
|
|
|
566
566
|
|
|
567
567
|
// Title row
|
|
568
568
|
const titleRow = createElement('div', { class: 'an-icon-title-row' });
|
|
569
|
+
const titleGroup = createElement('div', { class: 'an-dialog-title-group' });
|
|
570
|
+
const iconEl = createElement('span', { class: 'an-dialog-icon an-dialog-icon--sm' });
|
|
571
|
+
iconEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 13s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/></svg>`;
|
|
569
572
|
const title = createElement('h3', { class: 'an-dialog-title' });
|
|
570
573
|
title.textContent = L.title;
|
|
574
|
+
titleGroup.append(iconEl, title);
|
|
571
575
|
const closeBtn = createElement('button', { type: 'button', class: 'an-icon-close', 'aria-label': L.close });
|
|
572
576
|
closeBtn.innerHTML = '×';
|
|
573
|
-
titleRow.append(
|
|
577
|
+
titleRow.append(titleGroup, closeBtn);
|
|
574
578
|
|
|
575
579
|
// Search
|
|
576
|
-
const searchInput = createElement('input', {
|
|
580
|
+
const searchInput = /** @type {HTMLInputElement} */ (createElement('input', {
|
|
577
581
|
type: 'search',
|
|
578
582
|
class: 'an-input an-icon-search',
|
|
579
583
|
placeholder: L.searchPlaceholder,
|
|
580
584
|
autocomplete: 'off',
|
|
581
|
-
});
|
|
585
|
+
}));
|
|
582
586
|
this._searchInput = searchInput;
|
|
583
587
|
|
|
584
588
|
// Category tabs
|
|
@@ -588,7 +592,7 @@ export class EmojiDialog {
|
|
|
588
592
|
catBar.appendChild(allTab);
|
|
589
593
|
EMOJI_CATS.forEach(({ id, label }) => {
|
|
590
594
|
const tab = createElement('button', { type: 'button', class: 'an-icon-cat', 'data-cat': id });
|
|
591
|
-
tab.textContent =
|
|
595
|
+
tab.textContent = L.categories?.[id] || label;
|
|
592
596
|
catBar.appendChild(tab);
|
|
593
597
|
});
|
|
594
598
|
this._catBar = catBar;
|
|
@@ -617,6 +621,7 @@ export class EmojiDialog {
|
|
|
617
621
|
|
|
618
622
|
box.append(titleRow, searchInput, catBar, grid, btnRow);
|
|
619
623
|
overlay.appendChild(box);
|
|
624
|
+
makeDraggable(titleRow, box);
|
|
620
625
|
|
|
621
626
|
// Events
|
|
622
627
|
const d1 = on(closeBtn, 'click', () => this._close());
|
|
@@ -624,7 +629,7 @@ export class EmojiDialog {
|
|
|
624
629
|
const d3 = on(overlay, 'click', (e) => { if (e.target === overlay) this._close(); });
|
|
625
630
|
const d4 = on(searchInput, 'input', () => this._filterEmojis(searchInput.value, this._activeCat));
|
|
626
631
|
const d5 = on(catBar, 'click', (e) => {
|
|
627
|
-
const tab = e.target
|
|
632
|
+
const tab = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('[data-cat]'));
|
|
628
633
|
if (tab) {
|
|
629
634
|
this._activeCat = tab.dataset.cat;
|
|
630
635
|
this._updateCatTabs();
|
|
@@ -632,7 +637,7 @@ export class EmojiDialog {
|
|
|
632
637
|
}
|
|
633
638
|
});
|
|
634
639
|
const d6 = on(grid, 'click', (e) => {
|
|
635
|
-
const cell = e.target
|
|
640
|
+
const cell = /** @type {HTMLElement} */ (/** @type {Element} */ (e.target)?.closest('.an-emoji-cell'));
|
|
636
641
|
if (cell) this._onEmojiClick(cell.dataset.char);
|
|
637
642
|
});
|
|
638
643
|
this._disposers.push(d1, d2, d3, d4, d5, d6);
|
|
@@ -645,7 +650,7 @@ export class EmojiDialog {
|
|
|
645
650
|
|
|
646
651
|
_updateCatTabs() {
|
|
647
652
|
this._catBar.querySelectorAll('.an-icon-cat').forEach((tab) => {
|
|
648
|
-
tab.classList.toggle('active', tab.dataset.cat === this._activeCat);
|
|
653
|
+
tab.classList.toggle('active', /** @type {HTMLElement} */ (tab).dataset.cat === this._activeCat);
|
|
649
654
|
});
|
|
650
655
|
}
|
|
651
656
|
|
|
@@ -653,10 +658,11 @@ export class EmojiDialog {
|
|
|
653
658
|
const q = (query || '').trim().toLowerCase();
|
|
654
659
|
let count = 0;
|
|
655
660
|
this._grid.querySelectorAll('.an-emoji-cell').forEach((cell) => {
|
|
656
|
-
const
|
|
657
|
-
const
|
|
661
|
+
const hCell = /** @type {HTMLElement} */ (cell);
|
|
662
|
+
const matchCat = !cat || cat === 'all' || hCell.dataset.cat === cat;
|
|
663
|
+
const matchQuery = !q || hCell.dataset.keywords.includes(q) || hCell.dataset.char === q;
|
|
658
664
|
const visible = matchCat && matchQuery;
|
|
659
|
-
|
|
665
|
+
hCell.style.display = visible ? '' : 'none';
|
|
660
666
|
if (visible) count++;
|
|
661
667
|
});
|
|
662
668
|
let empty = this._grid.querySelector('.an-icon-empty');
|
|
@@ -665,7 +671,7 @@ export class EmojiDialog {
|
|
|
665
671
|
empty.textContent = 'No emojis found';
|
|
666
672
|
this._grid.appendChild(empty);
|
|
667
673
|
}
|
|
668
|
-
empty.style.display = count > 0 ? 'none' : '';
|
|
674
|
+
/** @type {HTMLElement} */ (empty).style.display = count > 0 ? 'none' : '';
|
|
669
675
|
}
|
|
670
676
|
|
|
671
677
|
// ---------------------------------------------------------------------------
|
|
@@ -679,7 +685,7 @@ export class EmojiDialog {
|
|
|
679
685
|
// 1. Restore selection while dialog is still mounted (same pattern as ImageDialog)
|
|
680
686
|
if (savedRange) savedRange.select();
|
|
681
687
|
|
|
682
|
-
const sel =
|
|
688
|
+
const sel = globalThis.getSelection();
|
|
683
689
|
let range = sel && sel.rangeCount > 0 ? sel.getRangeAt(0) : null;
|
|
684
690
|
if (!range) {
|
|
685
691
|
range = document.createRange();
|
|
@@ -694,10 +700,10 @@ export class EmojiDialog {
|
|
|
694
700
|
// deleteContents() can drift the range endpoint outside the cell in some
|
|
695
701
|
// browsers. Save the cell reference first and re-anchor after deletion.
|
|
696
702
|
const _sc = range.startContainer;
|
|
697
|
-
const _tdAnchor = (_sc.nodeType === 1 ? _sc : _sc.parentElement)
|
|
698
|
-
?.closest
|
|
703
|
+
const _tdAnchor = /** @type {Element|null} */ (_sc.nodeType === 1 ? _sc : _sc.parentElement)
|
|
704
|
+
?.closest('td, th');
|
|
699
705
|
range.deleteContents();
|
|
700
|
-
if (_tdAnchor
|
|
706
|
+
if (_tdAnchor?.isConnected && !_tdAnchor.contains(range.startContainer)) {
|
|
701
707
|
range.setStart(_tdAnchor, 0);
|
|
702
708
|
range.collapse(true);
|
|
703
709
|
}
|
|
@@ -726,7 +732,7 @@ export class EmojiDialog {
|
|
|
726
732
|
if (this._dialog) {
|
|
727
733
|
this._dialog.style.display = 'flex';
|
|
728
734
|
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
729
|
-
setTimeout(() => this._searchInput
|
|
735
|
+
setTimeout(() => this._searchInput?.focus(), 50);
|
|
730
736
|
}
|
|
731
737
|
}
|
|
732
738
|
|