autumnnote 1.6.3 → 1.6.6
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/dist/autumnnote.es.js +133 -119
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +131 -117
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/js/Context.js +4 -4
- package/src/js/core/detectLang.js +1 -1
- package/src/js/core/dom.js +6 -8
- package/src/js/core/markdown.js +2 -2
- package/src/js/core/range.js +1 -1
- package/src/js/editing/History.js +2 -2
- package/src/js/editing/Style.js +17 -15
- package/src/js/editing/Typing.js +7 -7
- package/src/js/index.js +1 -1
- package/src/js/module/AutoSaveRestore.js +3 -2
- package/src/js/module/BaseDialog.js +1 -1
- package/src/js/module/BubbleToolbar.js +3 -3
- package/src/js/module/Buttons.js +8 -8
- package/src/js/module/Clipboard.js +4 -4
- package/src/js/module/CodeTooltip.js +1 -1
- package/src/js/module/ContextMenu.js +14 -12
- package/src/js/module/Editor.js +10 -10
- package/src/js/module/EmojiDialog.js +1 -1
- package/src/js/module/FindReplace.js +6 -7
- package/src/js/module/IconDialog.js +2 -2
- package/src/js/module/ImageCropOverlay.js +2 -1
- package/src/js/module/ImageDialog.js +3 -3
- package/src/js/module/ImageResizer.js +3 -3
- package/src/js/module/ImageTooltip.js +5 -3
- package/src/js/module/LinkDialog.js +1 -1
- package/src/js/module/LinkTooltip.js +3 -3
- package/src/js/module/Mention.js +3 -3
- package/src/js/module/TableTooltip.js +57 -39
- package/src/js/module/Toolbar.js +21 -20
- package/src/js/module/VideoDialog.js +5 -2
- package/src/js/renderer.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
4
4
|
"description": "WYSIWYG rich-text editor built with vanilla JavaScript — zero dependencies, no jQuery. Dark mode, @mention, markdown shortcuts, bubble toolbar. React and Vue 3 wrappers included.",
|
|
5
5
|
"main": "dist/autumnnote.umd.js",
|
|
6
6
|
"module": "dist/autumnnote.es.js",
|
package/src/js/Context.js
CHANGED
|
@@ -89,13 +89,13 @@ export class Context {
|
|
|
89
89
|
|
|
90
90
|
// 3. Attach toolbar/statusbar to container
|
|
91
91
|
const toolbar = this._modules.get('toolbar');
|
|
92
|
-
if (toolbar
|
|
92
|
+
if (toolbar?.el) {
|
|
93
93
|
container.insertBefore(toolbar.el, editable);
|
|
94
94
|
this.layoutInfo.toolbar = toolbar.el;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
const statusbar = this._modules.get('statusbar');
|
|
98
|
-
if (statusbar
|
|
98
|
+
if (statusbar?.el) {
|
|
99
99
|
container.appendChild(statusbar.el);
|
|
100
100
|
this.layoutInfo.statusbar = statusbar.el;
|
|
101
101
|
}
|
|
@@ -256,7 +256,7 @@ export class Context {
|
|
|
256
256
|
const key = this.options.autoSaveKey;
|
|
257
257
|
localStorage.setItem(key, this.getHTML());
|
|
258
258
|
localStorage.setItem(key + ':asrmeta', JSON.stringify({ savedAt: Date.now() }));
|
|
259
|
-
} catch (_) {}
|
|
259
|
+
} catch (_) { void _; }
|
|
260
260
|
});
|
|
261
261
|
this._disposers.push(d4);
|
|
262
262
|
}
|
|
@@ -551,7 +551,7 @@ export class Context {
|
|
|
551
551
|
|
|
552
552
|
for (const { plugin } of this._plugins.values()) {
|
|
553
553
|
if (typeof plugin.uninstall === 'function') {
|
|
554
|
-
try { plugin.uninstall(this); } catch (_) {}
|
|
554
|
+
try { plugin.uninstall(this); } catch (_) { void _; }
|
|
555
555
|
}
|
|
556
556
|
}
|
|
557
557
|
this._plugins.clear();
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @returns {string|null}
|
|
14
14
|
*/
|
|
15
15
|
export function detectLang(code) {
|
|
16
|
-
if (!code
|
|
16
|
+
if (!code?.trim()) return null;
|
|
17
17
|
const s = code.trim();
|
|
18
18
|
|
|
19
19
|
// ── TypeScript ─────────────────────────────────────────────────────────────
|
package/src/js/core/dom.js
CHANGED
|
@@ -11,9 +11,9 @@ export const ELEMENT_NODE = 1;
|
|
|
11
11
|
export const TEXT_NODE = 3;
|
|
12
12
|
|
|
13
13
|
/** @param {Node} node */
|
|
14
|
-
export const isElement = (node) => node
|
|
14
|
+
export const isElement = (node) => node?.nodeType === ELEMENT_NODE;
|
|
15
15
|
/** @param {Node} node */
|
|
16
|
-
export const isText = (node) => node
|
|
16
|
+
export const isText = (node) => node?.nodeType === TEXT_NODE;
|
|
17
17
|
/** @param {Node} node */
|
|
18
18
|
export const isVoid = (node) => isElement(node) && /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(node.nodeName);
|
|
19
19
|
/** @param {Node} node */
|
|
@@ -147,7 +147,7 @@ export function createElement(tag, attrs = {}, childNodes = []) {
|
|
|
147
147
|
* @param {Node} node
|
|
148
148
|
*/
|
|
149
149
|
export function remove(node) {
|
|
150
|
-
if (node
|
|
150
|
+
if (node?.parentNode) {
|
|
151
151
|
/** @type {ChildNode} */ (node).remove();
|
|
152
152
|
}
|
|
153
153
|
}
|
|
@@ -305,11 +305,9 @@ export function trapFocus(container, onEscape) {
|
|
|
305
305
|
e.preventDefault();
|
|
306
306
|
/** @type {HTMLElement} */ (last).focus();
|
|
307
307
|
}
|
|
308
|
-
} else {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
/** @type {HTMLElement} */ (first).focus();
|
|
312
|
-
}
|
|
308
|
+
} else if (document.activeElement === last) {
|
|
309
|
+
e.preventDefault();
|
|
310
|
+
/** @type {HTMLElement} */ (first).focus();
|
|
313
311
|
}
|
|
314
312
|
};
|
|
315
313
|
|
package/src/js/core/markdown.js
CHANGED
|
@@ -67,7 +67,7 @@ function _domToMd(node, depth = 0) {
|
|
|
67
67
|
}
|
|
68
68
|
case 'pre': {
|
|
69
69
|
const codeEl = el.querySelector('code');
|
|
70
|
-
const langMatch = /language-(\S+)/.exec(
|
|
70
|
+
const langMatch = /language-(\S+)/.exec(codeEl?.className || '');
|
|
71
71
|
const lang = langMatch ? langMatch[1] : '';
|
|
72
72
|
const content = (codeEl || el).textContent || '';
|
|
73
73
|
return `\n\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`;
|
|
@@ -105,7 +105,7 @@ function _domToMd(node, depth = 0) {
|
|
|
105
105
|
const rows = Array.from(el.querySelectorAll('tr'));
|
|
106
106
|
if (!rows.length) return inner();
|
|
107
107
|
const cellTexts = rows.map((tr) =>
|
|
108
|
-
Array.from(tr.querySelectorAll('th, td')).map((c) => c.textContent.trim().replaceAll('|',
|
|
108
|
+
Array.from(tr.querySelectorAll('th, td')).map((c) => c.textContent.trim().replaceAll('|', String.raw`\|`)),
|
|
109
109
|
);
|
|
110
110
|
const cols = Math.max(...cellTexts.map((r) => r.length));
|
|
111
111
|
const padRow = (row) => { const r = [...row]; while (r.length < cols) r.push(''); return r; };
|
package/src/js/core/range.js
CHANGED
|
@@ -101,14 +101,14 @@ export class History {
|
|
|
101
101
|
sel.removeAllRanges();
|
|
102
102
|
sel.addRange(range);
|
|
103
103
|
} catch (_) {
|
|
104
|
-
//
|
|
104
|
+
void _; // detached node — fall back to placing cursor at start of editable
|
|
105
105
|
try {
|
|
106
106
|
const fb = document.createRange();
|
|
107
107
|
fb.setStart(this.editable, 0);
|
|
108
108
|
fb.collapse(true);
|
|
109
109
|
const s = globalThis.getSelection();
|
|
110
110
|
if (s) { s.removeAllRanges(); s.addRange(fb); }
|
|
111
|
-
} catch (_2) { /* fully give up */ }
|
|
111
|
+
} catch (_2) { void _2; /* fully give up */ }
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
package/src/js/editing/Style.js
CHANGED
|
@@ -41,7 +41,7 @@ export const italic = () => execCommand('italic');
|
|
|
41
41
|
*/
|
|
42
42
|
export function underline() {
|
|
43
43
|
const sel = globalThis.getSelection();
|
|
44
|
-
if (!sel
|
|
44
|
+
if (!sel?.rangeCount) return;
|
|
45
45
|
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
46
46
|
if (container.nodeType === 3) container = container.parentElement;
|
|
47
47
|
// Check if we're inside a <u> (DOM truth), to guard against unreliable queryCommandState
|
|
@@ -65,7 +65,7 @@ export function underline() {
|
|
|
65
65
|
*/
|
|
66
66
|
export function strikethrough() {
|
|
67
67
|
const sel = globalThis.getSelection();
|
|
68
|
-
if (!sel
|
|
68
|
+
if (!sel?.rangeCount) return;
|
|
69
69
|
// Use startContainer for consistent detection across collapsed and range
|
|
70
70
|
// selections — commonAncestorContainer can miss ancestor <s>/<strike> tags
|
|
71
71
|
// when the selection spans across nested inline elements.
|
|
@@ -120,7 +120,7 @@ export const fontName = (name) => execCommand('fontName', name);
|
|
|
120
120
|
*/
|
|
121
121
|
export function fontSize(size, editable = document) {
|
|
122
122
|
const sel = globalThis.getSelection();
|
|
123
|
-
const wasCollapsed = !sel
|
|
123
|
+
const wasCollapsed = !sel?.rangeCount || sel.getRangeAt(0).collapsed;
|
|
124
124
|
|
|
125
125
|
// B-I-3/4: For a collapsed (caret) selection the browser's execCommand
|
|
126
126
|
// 'fontSize' leaves an internal "pending" state of size-7 (=48px) instead of
|
|
@@ -130,7 +130,7 @@ export function fontSize(size, editable = document) {
|
|
|
130
130
|
// Only applies when there IS an active selection (sel.rangeCount > 0); when
|
|
131
131
|
// there is no selection at all (e.g. jsdom unit tests) fall through to the
|
|
132
132
|
// execCommand path so the font-replacement logic still runs.
|
|
133
|
-
if (wasCollapsed && sel
|
|
133
|
+
if (wasCollapsed && sel?.rangeCount > 0) {
|
|
134
134
|
try {
|
|
135
135
|
const range = sel.getRangeAt(0);
|
|
136
136
|
const span = document.createElement('span');
|
|
@@ -143,7 +143,7 @@ export function fontSize(size, editable = document) {
|
|
|
143
143
|
nr.collapse(true);
|
|
144
144
|
sel.removeAllRanges();
|
|
145
145
|
sel.addRange(nr);
|
|
146
|
-
} catch (_) { /* ignore range errors on unusual DOM structures */ }
|
|
146
|
+
} catch (_) { void _; /* ignore range errors on unusual DOM structures */ }
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -176,7 +176,7 @@ export function fontSize(size, editable = document) {
|
|
|
176
176
|
nr.setEnd(endNode, endNode.nodeType === Node.TEXT_NODE ? endNode.textContent.length : endNode.childNodes.length);
|
|
177
177
|
sel.removeAllRanges();
|
|
178
178
|
sel.addRange(nr);
|
|
179
|
-
} catch (_) { /* ignore range errors on unusual DOM structures */ }
|
|
179
|
+
} catch (_) { void _; /* ignore range errors on unusual DOM structures */ }
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -223,7 +223,7 @@ export const indent = () => execCommand('indent');
|
|
|
223
223
|
*/
|
|
224
224
|
export function outdent() {
|
|
225
225
|
const sel = globalThis.getSelection();
|
|
226
|
-
if (sel
|
|
226
|
+
if (sel?.rangeCount) {
|
|
227
227
|
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
228
228
|
if (container.nodeType === 3) container = container.parentElement;
|
|
229
229
|
const checkLi = /** @type {Element|null} */ (container)?.closest('.an-checklist li');
|
|
@@ -286,7 +286,7 @@ function _checklistItemToP(checkLi) {
|
|
|
286
286
|
try {
|
|
287
287
|
const nr = document.createRange();
|
|
288
288
|
const firstChild = p.firstChild;
|
|
289
|
-
nr.setStart(firstChild
|
|
289
|
+
nr.setStart(firstChild?.nodeType === 3 ? firstChild : p, 0);
|
|
290
290
|
nr.collapse(true);
|
|
291
291
|
const s = globalThis.getSelection();
|
|
292
292
|
if (s) { s.removeAllRanges(); s.addRange(nr); }
|
|
@@ -406,7 +406,7 @@ export function currentStyle(editable) {
|
|
|
406
406
|
*/
|
|
407
407
|
export function toggleInlineCode(_editable) {
|
|
408
408
|
const sel = globalThis.getSelection();
|
|
409
|
-
if (!sel
|
|
409
|
+
if (!sel?.rangeCount) return;
|
|
410
410
|
const range = sel.getRangeAt(0);
|
|
411
411
|
let container = range.commonAncestorContainer;
|
|
412
412
|
if (container.nodeType === 3) container = container.parentElement;
|
|
@@ -432,7 +432,9 @@ export function toggleInlineCode(_editable) {
|
|
|
432
432
|
const lastMoved = movedChildren.at(-1);
|
|
433
433
|
const nr = document.createRange();
|
|
434
434
|
// Use the (possibly merged) live node if still in the DOM.
|
|
435
|
-
const anchorNode =
|
|
435
|
+
const anchorNode = firstMoved.parentNode === parent
|
|
436
|
+
? firstMoved
|
|
437
|
+
: (prevSibling ? prevSibling.nextSibling : parent.firstChild);
|
|
436
438
|
if (anchorNode) {
|
|
437
439
|
nr.setStart(anchorNode, 0);
|
|
438
440
|
const endAnchor = (lastMoved.parentNode === parent) ? lastMoved : anchorNode;
|
|
@@ -440,7 +442,7 @@ export function toggleInlineCode(_editable) {
|
|
|
440
442
|
sel.removeAllRanges();
|
|
441
443
|
sel.addRange(nr);
|
|
442
444
|
}
|
|
443
|
-
} catch (_) { /* ignore */ }
|
|
445
|
+
} catch (_) { void _; /* ignore */ }
|
|
444
446
|
}
|
|
445
447
|
} else {
|
|
446
448
|
if (range.collapsed) return;
|
|
@@ -477,7 +479,7 @@ export function toggleInlineCode(_editable) {
|
|
|
477
479
|
*/
|
|
478
480
|
export function isInlineCode() {
|
|
479
481
|
const sel = globalThis.getSelection();
|
|
480
|
-
if (!sel
|
|
482
|
+
if (!sel?.rangeCount) return false;
|
|
481
483
|
let sc = sel.getRangeAt(0).startContainer;
|
|
482
484
|
if (sc.nodeType === 3) sc = sc.parentElement;
|
|
483
485
|
const code = /** @type {Element|null} */ (sc)?.closest('code');
|
|
@@ -504,7 +506,7 @@ export function isInlineCode() {
|
|
|
504
506
|
*/
|
|
505
507
|
export function toggleChecklist() {
|
|
506
508
|
const sel = globalThis.getSelection();
|
|
507
|
-
if (!sel
|
|
509
|
+
if (!sel?.rangeCount) return;
|
|
508
510
|
const range = sel.getRangeAt(0);
|
|
509
511
|
let container = range.commonAncestorContainer;
|
|
510
512
|
if (container.nodeType === 3) container = container.parentElement;
|
|
@@ -552,7 +554,7 @@ export function toggleChecklist() {
|
|
|
552
554
|
// and convert it into a single checklist item.
|
|
553
555
|
const BLOCK_TAGS = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI']);
|
|
554
556
|
let block = /** @type {Element|null} */ (container);
|
|
555
|
-
while (block
|
|
557
|
+
while (block?.parentNode && !BLOCK_TAGS.has(block.tagName)) {
|
|
556
558
|
block = /** @type {Element|null} */ (block.parentNode);
|
|
557
559
|
}
|
|
558
560
|
// Fallback: if no block element found (e.g. cursor directly in editable root), use the
|
|
@@ -674,7 +676,7 @@ export function toggleChecklist() {
|
|
|
674
676
|
*/
|
|
675
677
|
export function isInChecklist() {
|
|
676
678
|
const sel = globalThis.getSelection();
|
|
677
|
-
if (!sel
|
|
679
|
+
if (!sel?.rangeCount) return false;
|
|
678
680
|
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
679
681
|
if (container.nodeType === 3) container = container.parentElement;
|
|
680
682
|
return !!(/** @type {Element|null} */ (container)?.closest('.an-checklist li'));
|
package/src/js/editing/Typing.js
CHANGED
|
@@ -68,7 +68,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
68
68
|
// (not a text node), causing the next Backspace to miss Cases A/B and
|
|
69
69
|
// requiring an extra keypress when two icons are adjacent.
|
|
70
70
|
const nr = document.createRange();
|
|
71
|
-
if (prevNode
|
|
71
|
+
if (prevNode?.nodeType === Node.TEXT_NODE) {
|
|
72
72
|
nr.setStart(prevNode, prevNode.textContent.length);
|
|
73
73
|
} else if (prevNode) {
|
|
74
74
|
nr.setStartAfter(prevNode);
|
|
@@ -128,7 +128,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
128
128
|
const icon = textNode.nextSibling;
|
|
129
129
|
const after = icon.nextSibling;
|
|
130
130
|
event.preventDefault();
|
|
131
|
-
if (after
|
|
131
|
+
if (after?.nodeType === Node.TEXT_NODE) {
|
|
132
132
|
const offset = ((after.textContent || '').startsWith('\u200B')) ? 1 : 0;
|
|
133
133
|
return moveCaret((nr) => nr.setStart(after, Math.min(offset, after.textContent.length)));
|
|
134
134
|
}
|
|
@@ -142,7 +142,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
142
142
|
const icon = textNode.nextSibling.nextSibling;
|
|
143
143
|
const after = icon.nextSibling;
|
|
144
144
|
event.preventDefault();
|
|
145
|
-
if (after
|
|
145
|
+
if (after?.nodeType === Node.TEXT_NODE) {
|
|
146
146
|
const offset = ((after.textContent || '').startsWith('\u200B')) ? 1 : 0;
|
|
147
147
|
return moveCaret((nr) => nr.setStart(after, Math.min(offset, after.textContent.length)));
|
|
148
148
|
}
|
|
@@ -168,7 +168,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
168
168
|
if (isFAIcon(next)) {
|
|
169
169
|
const after = next.nextSibling;
|
|
170
170
|
event.preventDefault();
|
|
171
|
-
if (after
|
|
171
|
+
if (after?.nodeType === Node.TEXT_NODE) {
|
|
172
172
|
const offset = ((after.textContent || '').startsWith('\u200B')) ? 1 : 0;
|
|
173
173
|
return moveCaret((nr) => nr.setStart(after, Math.min(offset, after.textContent.length)));
|
|
174
174
|
}
|
|
@@ -178,7 +178,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
178
178
|
const icon = next.nextSibling;
|
|
179
179
|
const after = icon.nextSibling;
|
|
180
180
|
event.preventDefault();
|
|
181
|
-
if (after
|
|
181
|
+
if (after?.nodeType === Node.TEXT_NODE) {
|
|
182
182
|
const offset = ((after.textContent || '').startsWith('\u200B')) ? 1 : 0;
|
|
183
183
|
return moveCaret((nr) => nr.setStart(after, Math.min(offset, after.textContent.length)));
|
|
184
184
|
}
|
|
@@ -341,11 +341,11 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
341
341
|
// may normalise it to element-level, placing the caret before the
|
|
342
342
|
// absolutely-positioned checkbox. \u200B is stripped by getHTML().
|
|
343
343
|
let cursorNode = newLi.childNodes[1]; // first child after checkbox
|
|
344
|
-
if (
|
|
344
|
+
if (cursorNode?.nodeType !== Node.TEXT_NODE) {
|
|
345
345
|
cursorNode = document.createTextNode('\u200B');
|
|
346
346
|
newLi.appendChild(cursorNode);
|
|
347
347
|
}
|
|
348
|
-
checkLi.
|
|
348
|
+
checkLi.after(newLi);
|
|
349
349
|
|
|
350
350
|
const nr = document.createRange();
|
|
351
351
|
nr.setStart(cursorNode, 0);
|
package/src/js/index.js
CHANGED
|
@@ -141,7 +141,7 @@ const AutumnNote = {
|
|
|
141
141
|
registerButton(btnDef) { registerButton(btnDef); return this; },
|
|
142
142
|
|
|
143
143
|
/** Library version */
|
|
144
|
-
version: '1.6.
|
|
144
|
+
version: '1.6.6',
|
|
145
145
|
};
|
|
146
146
|
|
|
147
147
|
// ---------------------------------------------------------------------------
|
|
@@ -30,6 +30,7 @@ export class AutoSaveRestore {
|
|
|
30
30
|
saved = localStorage.getItem(key);
|
|
31
31
|
meta = JSON.parse(localStorage.getItem(metaKey) || '{}');
|
|
32
32
|
} catch (_) {
|
|
33
|
+
void _;
|
|
33
34
|
return this;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -39,7 +40,7 @@ export class AutoSaveRestore {
|
|
|
39
40
|
if (timeout > 0) {
|
|
40
41
|
const ageMs = Date.now() - (meta.savedAt || 0);
|
|
41
42
|
if (ageMs > timeout * 86400000) {
|
|
42
|
-
try { localStorage.removeItem(key); localStorage.removeItem(metaKey); } catch (_) {}
|
|
43
|
+
try { localStorage.removeItem(key); localStorage.removeItem(metaKey); } catch (_) { void _; }
|
|
43
44
|
return this;
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -113,7 +114,7 @@ export class AutoSaveRestore {
|
|
|
113
114
|
try {
|
|
114
115
|
localStorage.removeItem(key);
|
|
115
116
|
localStorage.removeItem(key + ':asrmeta');
|
|
116
|
-
} catch (_) {}
|
|
117
|
+
} catch (_) { void _; }
|
|
117
118
|
this._removeBanner();
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -51,7 +51,7 @@ export class BaseDialog {
|
|
|
51
51
|
if (this._dialog) {
|
|
52
52
|
this._dialog.style.display = 'flex';
|
|
53
53
|
this._removeTrap = trapFocus(this._dialog, () => this._close());
|
|
54
|
-
setTimeout(() => this._firstInput
|
|
54
|
+
setTimeout(() => this._firstInput?.focus(), 50);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -311,7 +311,7 @@ export class BubbleToolbar {
|
|
|
311
311
|
editable.focus();
|
|
312
312
|
const sel = globalThis.getSelection();
|
|
313
313
|
sel.removeAllRanges();
|
|
314
|
-
try { sel.addRange(this._savedRange.cloneRange()); } catch (_) { return; }
|
|
314
|
+
try { sel.addRange(this._savedRange.cloneRange()); } catch (_) { void _; return; }
|
|
315
315
|
|
|
316
316
|
// Firefox does not support 'hiliteColor'; fall back to 'backColor'
|
|
317
317
|
const cmd = type === 'hiliteColor' ? 'hiliteColor' : type;
|
|
@@ -389,7 +389,7 @@ export class BubbleToolbar {
|
|
|
389
389
|
if (!this._btnCache) return;
|
|
390
390
|
this._btnCache.forEach((btn) => {
|
|
391
391
|
const activeFn = _ACTIVE[/** @type {HTMLElement} */ (btn).dataset.name];
|
|
392
|
-
btn.classList.toggle('an-active', !!(activeFn
|
|
392
|
+
btn.classList.toggle('an-active', !!(activeFn?.()));
|
|
393
393
|
});
|
|
394
394
|
}
|
|
395
395
|
|
|
@@ -397,7 +397,7 @@ export class BubbleToolbar {
|
|
|
397
397
|
_syncColorStrips() {
|
|
398
398
|
if (!this._el) return;
|
|
399
399
|
const sel = globalThis.getSelection();
|
|
400
|
-
if (!sel
|
|
400
|
+
if (!sel?.rangeCount) return;
|
|
401
401
|
let node = sel.getRangeAt(0).startContainer;
|
|
402
402
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
|
403
403
|
if (!node) return;
|
package/src/js/module/Buttons.js
CHANGED
|
@@ -102,10 +102,10 @@ export const underlineBtn = btn('underline', 'underline', 'Underline (Ctrl+U)',
|
|
|
102
102
|
// consistent behaviour across both collapsed and range selections.
|
|
103
103
|
if (document.queryCommandState('underline')) return true;
|
|
104
104
|
const sel = globalThis.getSelection();
|
|
105
|
-
if (!sel
|
|
105
|
+
if (!sel?.rangeCount) return false;
|
|
106
106
|
let sc = sel.getRangeAt(0).startContainer;
|
|
107
107
|
if (sc.nodeType === 3) sc = sc.parentElement;
|
|
108
|
-
return !!(
|
|
108
|
+
return !!(/** @type {Element} */ (sc)?.closest('u'));
|
|
109
109
|
});
|
|
110
110
|
export const strikeBtn = btn('strikethrough', 'strikethrough', 'Strikethrough', () => Style.strikethrough(), () => document.queryCommandState('strikeThrough'));
|
|
111
111
|
export const superscriptBtn = btn('superscript', 'superscript', 'Superscript', () => Style.superscript(), () => document.queryCommandState('superscript'));
|
|
@@ -180,11 +180,11 @@ export const fontSizeBtn = {
|
|
|
180
180
|
getValue: (ctx) => {
|
|
181
181
|
try {
|
|
182
182
|
const sel = globalThis.getSelection();
|
|
183
|
-
if (sel
|
|
183
|
+
if (sel?.rangeCount) {
|
|
184
184
|
let el = /** @type {Element|null} */ (sel.getRangeAt(0).startContainer);
|
|
185
|
-
if (el
|
|
186
|
-
while (el
|
|
187
|
-
const size =
|
|
185
|
+
if (el?.nodeType === 3) el = el.parentElement;
|
|
186
|
+
while (el?.nodeType === 1 && !/** @type {HTMLElement} */ (el).style.fontSize) el = el.parentElement;
|
|
187
|
+
const size = /** @type {HTMLElement} */ (el)?.style.fontSize || '';
|
|
188
188
|
if (size) return size;
|
|
189
189
|
}
|
|
190
190
|
// Fallback: read the base font size from the editable element itself
|
|
@@ -281,10 +281,10 @@ export const lineHeightBtn = {
|
|
|
281
281
|
getValue: () => {
|
|
282
282
|
try {
|
|
283
283
|
const sel = globalThis.getSelection();
|
|
284
|
-
if (!sel
|
|
284
|
+
if (!sel?.rangeCount) return '';
|
|
285
285
|
const BLOCKS = new Set(['P','DIV','H1','H2','H3','H4','H5','H6','LI','BLOCKQUOTE','PRE','TD','TH']);
|
|
286
286
|
let el = /** @type {Element|null} */ (sel.getRangeAt(0).startContainer);
|
|
287
|
-
if (el
|
|
287
|
+
if (el?.nodeType === 3) el = el.parentElement;
|
|
288
288
|
while (el && !BLOCKS.has(/** @type {Element} */ (el).tagName)) el = el.parentElement;
|
|
289
289
|
if (!el) return '';
|
|
290
290
|
return /** @type {HTMLElement} */ (el).style.lineHeight || getComputedStyle(/** @type {Element} */ (el)).lineHeight || '';
|
|
@@ -64,7 +64,7 @@ export class Clipboard {
|
|
|
64
64
|
* @param {Node} node
|
|
65
65
|
*/
|
|
66
66
|
_revokeRemovedBlobs(node) {
|
|
67
|
-
if (!this._blobRegistry
|
|
67
|
+
if (!this._blobRegistry?.size) return;
|
|
68
68
|
const imgs = /** @type {Element[]} */ ([]);
|
|
69
69
|
if (node.nodeName === 'IMG') {
|
|
70
70
|
imgs.push(/** @type {Element} */ (node));
|
|
@@ -267,7 +267,7 @@ export class Clipboard {
|
|
|
267
267
|
|
|
268
268
|
_onDrop(event) {
|
|
269
269
|
const dt = event.dataTransfer;
|
|
270
|
-
if (!dt
|
|
270
|
+
if (!dt?.files?.length) return;
|
|
271
271
|
|
|
272
272
|
const imageFiles = Array.from(dt.files).filter((f) => f.type.startsWith('image/'));
|
|
273
273
|
if (imageFiles.length === 0) return;
|
|
@@ -302,7 +302,7 @@ export class Clipboard {
|
|
|
302
302
|
const UNSUPPORTED = new Set(['image/tiff', 'image/x-tiff', 'image/bmp', 'image/x-bmp', 'image/x-ms-bmp']);
|
|
303
303
|
const maxBytes = (this.options.maxImageSize || 5) * 1024 * 1024;
|
|
304
304
|
files.forEach((file) => {
|
|
305
|
-
if (!file
|
|
305
|
+
if (!file?.type?.startsWith('image/')) return;
|
|
306
306
|
if (UNSUPPORTED.has(file.type)) {
|
|
307
307
|
const message = `Image format "${file.type}" is not supported for display in web browsers. Please convert to PNG, JPEG, or WebP first.`;
|
|
308
308
|
this.context.triggerEvent('imageError', { file, message });
|
|
@@ -339,7 +339,7 @@ export class Clipboard {
|
|
|
339
339
|
* @returns {string}
|
|
340
340
|
*/
|
|
341
341
|
resolveImages(html) {
|
|
342
|
-
if (!this._blobRegistry
|
|
342
|
+
if (!this._blobRegistry?.size) return html;
|
|
343
343
|
return html.replace(/blob:[^"'> \t\n\r]*/g, (url) => this._blobRegistry.get(url) || url);
|
|
344
344
|
}
|
|
345
345
|
|
|
@@ -273,7 +273,7 @@ export class CodeTooltip {
|
|
|
273
273
|
ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0';
|
|
274
274
|
document.body.appendChild(ta);
|
|
275
275
|
ta.select();
|
|
276
|
-
try { document.execCommand('copy'); this._flashCopied(); } catch (_) {}
|
|
276
|
+
try { document.execCommand('copy'); this._flashCopied(); } catch (_) { void _; }
|
|
277
277
|
ta.remove();
|
|
278
278
|
}
|
|
279
279
|
}
|
|
@@ -114,7 +114,7 @@ export class ContextMenu {
|
|
|
114
114
|
constructor(context) {
|
|
115
115
|
this.context = context;
|
|
116
116
|
this.options = context.options || {};
|
|
117
|
-
this._items =
|
|
117
|
+
this._items = this.options.contextMenu?.items || defaultItems;
|
|
118
118
|
this.el = null;
|
|
119
119
|
this._disposers = [];
|
|
120
120
|
this._menuDisposers = []; // disposers for dynamically-rendered menu buttons
|
|
@@ -136,17 +136,19 @@ export class ContextMenu {
|
|
|
136
136
|
this._disposers.push(on(editable, 'contextmenu', (e) => this._onContextMenu(e)));
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
this._disposers.push(
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
this._disposers.push(
|
|
140
|
+
on(document, 'click', (e) => this._maybeHide(e)),
|
|
141
|
+
on(document, 'keydown', (e) => { if (/** @type {KeyboardEvent} */ (e).key === 'Escape') this.hide(); }),
|
|
142
|
+
on(globalThis, 'scroll', () => this.hide(), { passive: true }),
|
|
143
|
+
);
|
|
142
144
|
|
|
143
145
|
return this;
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
destroy() {
|
|
147
|
-
this._menuDisposers.forEach((d) => { try { d(); } catch (_e) {} });
|
|
149
|
+
this._menuDisposers.forEach((d) => { try { d(); } catch (_e) { void _e; } });
|
|
148
150
|
this._menuDisposers = [];
|
|
149
|
-
this._disposers.forEach((d) => { try { d(); } catch (_e) {} });
|
|
151
|
+
this._disposers.forEach((d) => { try { d(); } catch (_e) { void _e; } });
|
|
150
152
|
this._disposers = [];
|
|
151
153
|
if (this.el) this.el.remove();
|
|
152
154
|
this.el = null;
|
|
@@ -379,7 +381,7 @@ export class ContextMenu {
|
|
|
379
381
|
event.preventDefault();
|
|
380
382
|
|
|
381
383
|
const winSel = globalThis.getSelection();
|
|
382
|
-
this._savedRange = (winSel
|
|
384
|
+
this._savedRange = (winSel?.rangeCount > 0) ? winSel.getRangeAt(0).cloneRange() : null;
|
|
383
385
|
this._renderItems(this._items);
|
|
384
386
|
|
|
385
387
|
// Open below the selected text so the selection stays visible.
|
|
@@ -395,7 +397,7 @@ export class ContextMenu {
|
|
|
395
397
|
// Keep X at click position (feels natural); Y just below the selection.
|
|
396
398
|
openY = selRect.bottom + 4;
|
|
397
399
|
}
|
|
398
|
-
} catch (_) {}
|
|
400
|
+
} catch (_) { void _; }
|
|
399
401
|
}
|
|
400
402
|
|
|
401
403
|
this._lastX = openX;
|
|
@@ -418,8 +420,8 @@ export class ContextMenu {
|
|
|
418
420
|
|
|
419
421
|
_reposition(x, y) {
|
|
420
422
|
if (!this.el) return;
|
|
421
|
-
const rx = x
|
|
422
|
-
const ry = y
|
|
423
|
+
const rx = x === undefined ? this._lastX : x;
|
|
424
|
+
const ry = y === undefined ? this._lastY : y;
|
|
423
425
|
const w = this.el.offsetWidth;
|
|
424
426
|
const h = this.el.offsetHeight;
|
|
425
427
|
let left = rx;
|
|
@@ -485,7 +487,7 @@ export class ContextMenu {
|
|
|
485
487
|
const editable = this.context.layoutInfo?.editable;
|
|
486
488
|
let node = range.startContainer;
|
|
487
489
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
|
488
|
-
if (!node || !editable
|
|
490
|
+
if (!node || !editable?.contains(node)) return;
|
|
489
491
|
|
|
490
492
|
// Walk up to collect explicitly-set inline properties from the nearest styled ancestor
|
|
491
493
|
const cs = globalThis.getComputedStyle(/** @type {Element} */ (node));
|
|
@@ -510,7 +512,7 @@ export class ContextMenu {
|
|
|
510
512
|
_findExplicitStyle(node, boundary, prop) {
|
|
511
513
|
let el = node;
|
|
512
514
|
while (el && el !== boundary && el !== document.body) {
|
|
513
|
-
if (el.style
|
|
515
|
+
if (el.style?.[prop]) return el.style[prop];
|
|
514
516
|
// also check font element attributes
|
|
515
517
|
if (el.nodeName === 'FONT') {
|
|
516
518
|
if (prop === 'fontFamily' && el.getAttribute('face')) return el.getAttribute('face');
|
package/src/js/module/Editor.js
CHANGED
|
@@ -62,7 +62,7 @@ export class Editor {
|
|
|
62
62
|
const onSelChange = () => {
|
|
63
63
|
if (!this.context._alive) return;
|
|
64
64
|
const sel = globalThis.getSelection();
|
|
65
|
-
if (sel
|
|
65
|
+
if (sel?.rangeCount > 0 && editable.contains(sel.anchorNode)) {
|
|
66
66
|
this.context.invoke('toolbar.refresh');
|
|
67
67
|
if (typeof this.options.onSelectionChange === 'function') {
|
|
68
68
|
this.options.onSelectionChange(this.context);
|
|
@@ -86,7 +86,7 @@ export class Editor {
|
|
|
86
86
|
// keyup : arrow-key navigation may land at <li>[0]; move to start-of-text.
|
|
87
87
|
const fixChecklistCursor = (event) => {
|
|
88
88
|
const sel = globalThis.getSelection();
|
|
89
|
-
if (!sel
|
|
89
|
+
if (!sel?.rangeCount) return;
|
|
90
90
|
const r = sel.getRangeAt(0);
|
|
91
91
|
if (!r.collapsed) return;
|
|
92
92
|
const sc = r.startContainer;
|
|
@@ -102,7 +102,7 @@ export class Editor {
|
|
|
102
102
|
|
|
103
103
|
// For mouse events: ask the browser where the pointer landed so the
|
|
104
104
|
// cursor respects the actual click position inside the text.
|
|
105
|
-
if (event
|
|
105
|
+
if (event?.type === 'mouseup') {
|
|
106
106
|
let caret = null;
|
|
107
107
|
if (document.caretRangeFromPoint) {
|
|
108
108
|
caret = document.caretRangeFromPoint(event.clientX, event.clientY);
|
|
@@ -181,7 +181,7 @@ export class Editor {
|
|
|
181
181
|
let _compositionSupSub = null;
|
|
182
182
|
const onCompositionStart = () => {
|
|
183
183
|
const sel = globalThis.getSelection();
|
|
184
|
-
if (!sel
|
|
184
|
+
if (!sel?.rangeCount) { _compositionSupSub = null; return; }
|
|
185
185
|
let node = sel.getRangeAt(0).startContainer;
|
|
186
186
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
|
187
187
|
if (node) {
|
|
@@ -196,7 +196,7 @@ export class Editor {
|
|
|
196
196
|
_compositionSupSub = null;
|
|
197
197
|
if (!tag) return;
|
|
198
198
|
const sel = globalThis.getSelection();
|
|
199
|
-
if (!sel
|
|
199
|
+
if (!sel?.rangeCount) return;
|
|
200
200
|
let node = sel.getRangeAt(0).startContainer;
|
|
201
201
|
if (node.nodeType === Node.TEXT_NODE) node = node.parentElement;
|
|
202
202
|
const el = /** @type {Element} */ (node);
|
|
@@ -557,7 +557,7 @@ export class Editor {
|
|
|
557
557
|
// Only runs when converting TO <pre> and the block has no language yet.
|
|
558
558
|
if (tagName === 'pre') {
|
|
559
559
|
const sel = globalThis.getSelection();
|
|
560
|
-
if (sel
|
|
560
|
+
if (sel?.rangeCount > 0) {
|
|
561
561
|
const container = sel.getRangeAt(0).commonAncestorContainer;
|
|
562
562
|
const pre = /** @type {Element|null} */ (
|
|
563
563
|
container.nodeType === 1
|
|
@@ -623,10 +623,7 @@ export class Editor {
|
|
|
623
623
|
if (!safeUrl) return;
|
|
624
624
|
|
|
625
625
|
const hasText = sel.toString().trim().length > 0;
|
|
626
|
-
if (
|
|
627
|
-
const displayText = this._escapeAttr(text || safeUrl);
|
|
628
|
-
Style.execCommand('insertHTML', `<a href="${this._escapeAttr(safeUrl)}"${openInNewTab ? ' target="_blank" rel="noopener noreferrer"' : ''}>${displayText}</a>`);
|
|
629
|
-
} else {
|
|
626
|
+
if (hasText) {
|
|
630
627
|
Style.execCommand('createLink', safeUrl);
|
|
631
628
|
if (openInNewTab) {
|
|
632
629
|
const link = this._getClosestAnchor();
|
|
@@ -635,6 +632,9 @@ export class Editor {
|
|
|
635
632
|
/** @type {Element} */ (link).setAttribute('rel', 'noopener noreferrer');
|
|
636
633
|
}
|
|
637
634
|
}
|
|
635
|
+
} else {
|
|
636
|
+
const displayText = this._escapeAttr(text || safeUrl);
|
|
637
|
+
Style.execCommand('insertHTML', `<a href="${this._escapeAttr(safeUrl)}"${openInNewTab ? ' target="_blank" rel="noopener noreferrer"' : ''}>${displayText}</a>`);
|
|
638
638
|
}
|
|
639
639
|
this.afterCommand();
|
|
640
640
|
}
|
|
@@ -666,7 +666,7 @@ export class EmojiDialog extends BaseDialog {
|
|
|
666
666
|
if (savedRange) savedRange.select();
|
|
667
667
|
|
|
668
668
|
const sel = globalThis.getSelection();
|
|
669
|
-
let range = sel
|
|
669
|
+
let range = sel?.rangeCount > 0 ? sel.getRangeAt(0) : null;
|
|
670
670
|
if (!range) {
|
|
671
671
|
range = document.createRange();
|
|
672
672
|
range.selectNodeContents(editable);
|