autumnnote 1.6.0 → 1.6.2
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 +4 -4
- package/dist/autumnnote.css +0 -2
- package/dist/autumnnote.es.js +468 -552
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +461 -546
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +21 -3
- package/src/js/Context.js +21 -16
- package/src/js/core/dom.js +9 -9
- 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 +18 -18
- package/src/js/core/range.js +6 -6
- package/src/js/editing/History.js +4 -4
- package/src/js/editing/Style.js +32 -32
- package/src/js/editing/Table.js +2 -2
- package/src/js/editing/Typing.js +15 -15
- package/src/js/index.js +1 -1
- package/src/js/module/AutoSaveRestore.js +2 -4
- package/src/js/module/BaseDialog.js +126 -0
- package/src/js/module/BubbleToolbar.js +25 -25
- package/src/js/module/Buttons.js +4 -4
- package/src/js/module/Clipboard.js +12 -13
- package/src/js/module/CodeTooltip.js +14 -16
- package/src/js/module/Codeview.js +3 -5
- package/src/js/module/ContextMenu.js +27 -27
- package/src/js/module/Editor.js +17 -18
- package/src/js/module/EmojiDialog.js +5 -5
- package/src/js/module/FindReplace.js +8 -7
- package/src/js/module/IconDialog.js +13 -15
- package/src/js/module/ImageCropOverlay.js +7 -7
- package/src/js/module/ImageDialog.js +12 -82
- package/src/js/module/ImageResizer.js +4 -4
- package/src/js/module/ImageTooltip.js +14 -14
- package/src/js/module/LinkDialog.js +13 -79
- package/src/js/module/LinkTooltip.js +12 -12
- package/src/js/module/MarkdownShortcuts.js +11 -14
- package/src/js/module/Mention.js +8 -10
- package/src/js/module/Placeholder.js +1 -1
- package/src/js/module/ShortcutsDialog.js +2 -4
- package/src/js/module/Statusbar.js +3 -5
- package/src/js/module/TableTooltip.js +30 -32
- package/src/js/module/Toolbar.js +21 -21
- package/src/js/module/VideoDialog.js +17 -87
- package/src/js/module/VideoResizer.js +5 -7
- package/src/js/module/VideoTooltip.js +7 -7
- package/src/js/renderer.js +1 -1
- package/src/styles/autumnnote.scss +0 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.6.2",
|
|
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",
|
|
7
7
|
"types": "types/index.d.ts",
|
|
@@ -58,7 +58,25 @@
|
|
|
58
58
|
"dark-mode",
|
|
59
59
|
"i18n",
|
|
60
60
|
"typescript",
|
|
61
|
-
"esm"
|
|
61
|
+
"esm",
|
|
62
|
+
"javascript-wysiwyg",
|
|
63
|
+
"wysiwyg-vanilla",
|
|
64
|
+
"froala-alternative",
|
|
65
|
+
"ckeditor-alternative",
|
|
66
|
+
"zero-dependencies",
|
|
67
|
+
"react-wysiwyg",
|
|
68
|
+
"vue-wysiwyg",
|
|
69
|
+
"prosemirror-alternative",
|
|
70
|
+
"trix-alternative",
|
|
71
|
+
"slate-alternative",
|
|
72
|
+
"vue3",
|
|
73
|
+
"quill",
|
|
74
|
+
"tinymce",
|
|
75
|
+
"react",
|
|
76
|
+
"vue",
|
|
77
|
+
"plugin-api",
|
|
78
|
+
"rtl",
|
|
79
|
+
"autosave"
|
|
62
80
|
],
|
|
63
81
|
"homepage": "https://cmm-cmm.github.io/Autumn-Note/",
|
|
64
82
|
"repository": {
|
package/src/js/Context.js
CHANGED
|
@@ -159,8 +159,10 @@ export class Context {
|
|
|
159
159
|
register('mention', Mention);
|
|
160
160
|
|
|
161
161
|
// Custom modules registered via AutumnNote.registerModule()
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
if (_customModules.size > 0) {
|
|
163
|
+
for (const [name, ModuleClass] of _customModules) {
|
|
164
|
+
register(name, ModuleClass);
|
|
165
|
+
}
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
|
|
@@ -220,6 +222,7 @@ export class Context {
|
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
_applyGlobalPlugins() {
|
|
225
|
+
if (_globalPlugins.size === 0) return;
|
|
223
226
|
for (const { plugin, options } of _globalPlugins.values()) {
|
|
224
227
|
this._installPlugin(plugin, options);
|
|
225
228
|
}
|
|
@@ -477,21 +480,18 @@ export class Context {
|
|
|
477
480
|
a.style.display = 'none';
|
|
478
481
|
document.body.appendChild(a);
|
|
479
482
|
a.click();
|
|
480
|
-
|
|
483
|
+
a.remove();
|
|
481
484
|
URL.revokeObjectURL(url);
|
|
482
485
|
}
|
|
483
486
|
|
|
484
487
|
/**
|
|
485
|
-
* Opens the editor content in a new
|
|
488
|
+
* Opens the editor content in a new globalThis and triggers the browser print dialog.
|
|
486
489
|
* @param {string} [title='']
|
|
487
490
|
*/
|
|
488
491
|
print(title = '') {
|
|
489
492
|
const content = this.getHTML();
|
|
490
|
-
const safeTitle = (title || '').replace(
|
|
491
|
-
const
|
|
492
|
-
if (!w) return; // popup blocked by browser
|
|
493
|
-
w.document.write(
|
|
494
|
-
'<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">' +
|
|
493
|
+
const safeTitle = (title || '').replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
494
|
+
const markup = '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">' +
|
|
495
495
|
`<title>${safeTitle}</title>` +
|
|
496
496
|
'<style>' +
|
|
497
497
|
'body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,Arial,sans-serif;font-size:14px;line-height:1.6;padding:20mm;color:#111827;}' +
|
|
@@ -502,10 +502,15 @@ export class Context {
|
|
|
502
502
|
'pre{background:#f3f4f6;padding:.75em 1em;border-radius:4px;overflow-x:auto;}' +
|
|
503
503
|
'table{border-collapse:collapse;}td,th{border:1px solid #d1d5db;padding:4px 8px;}' +
|
|
504
504
|
'</style>' +
|
|
505
|
-
`</head><body>${content}</body></html
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
w
|
|
505
|
+
`</head><body>${content}</body></html>`;
|
|
506
|
+
const blob = new Blob([markup], { type: 'text/html' });
|
|
507
|
+
const url = URL.createObjectURL(blob);
|
|
508
|
+
const w = globalThis.open(url, '_blank');
|
|
509
|
+
if (!w) { URL.revokeObjectURL(url); return; } // popup blocked by browser
|
|
510
|
+
w.addEventListener('load', () => {
|
|
511
|
+
w.print();
|
|
512
|
+
URL.revokeObjectURL(url);
|
|
513
|
+
});
|
|
509
514
|
}
|
|
510
515
|
|
|
511
516
|
/**
|
|
@@ -555,11 +560,11 @@ export class Context {
|
|
|
555
560
|
this._disposers = [];
|
|
556
561
|
|
|
557
562
|
const container = this.layoutInfo.container;
|
|
558
|
-
const wasDark = container
|
|
559
|
-
if (container
|
|
563
|
+
const wasDark = container?.classList.contains('an-theme-dark');
|
|
564
|
+
if (container?.parentNode) {
|
|
560
565
|
// Restore original element
|
|
561
566
|
this.targetEl.style.display = '';
|
|
562
|
-
container.
|
|
567
|
+
container.remove();
|
|
563
568
|
}
|
|
564
569
|
// If this was a dark editor and no other dark containers remain, clean up body
|
|
565
570
|
if (wasDark && !document.querySelector('.an-container.an-theme-dark')) {
|
package/src/js/core/dom.js
CHANGED
|
@@ -148,7 +148,7 @@ export function createElement(tag, attrs = {}, childNodes = []) {
|
|
|
148
148
|
*/
|
|
149
149
|
export function remove(node) {
|
|
150
150
|
if (node && node.parentNode) {
|
|
151
|
-
node.
|
|
151
|
+
/** @type {ChildNode} */ (node).remove();
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -162,7 +162,7 @@ export function unwrap(node) {
|
|
|
162
162
|
while (node.firstChild) {
|
|
163
163
|
parent.insertBefore(node.firstChild, node);
|
|
164
164
|
}
|
|
165
|
-
|
|
165
|
+
/** @type {ChildNode} */ (node).remove();
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
@@ -238,7 +238,7 @@ export function placeCaret(el) {
|
|
|
238
238
|
const range = document.createRange();
|
|
239
239
|
range.selectNodeContents(el);
|
|
240
240
|
range.collapse(false);
|
|
241
|
-
const sel =
|
|
241
|
+
const sel = globalThis.getSelection();
|
|
242
242
|
if (sel) {
|
|
243
243
|
sel.removeAllRanges();
|
|
244
244
|
sel.addRange(range);
|
|
@@ -292,14 +292,14 @@ export function trapFocus(container, onEscape) {
|
|
|
292
292
|
const handler = (e) => {
|
|
293
293
|
if (e.key === 'Escape') {
|
|
294
294
|
e.stopPropagation();
|
|
295
|
-
onEscape
|
|
295
|
+
onEscape?.();
|
|
296
296
|
return;
|
|
297
297
|
}
|
|
298
298
|
if (e.key !== 'Tab') return;
|
|
299
299
|
const els = getFocusable();
|
|
300
300
|
if (!els.length) return;
|
|
301
301
|
const first = els[0];
|
|
302
|
-
const last = els
|
|
302
|
+
const last = els.at(-1);
|
|
303
303
|
if (e.shiftKey) {
|
|
304
304
|
if (document.activeElement === first) {
|
|
305
305
|
e.preventDefault();
|
|
@@ -347,16 +347,16 @@ export function makeDraggable(handle, box) {
|
|
|
347
347
|
box.dataset.anDragPinned = '1';
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
-
const startX = e.clientX - parseFloat(box.style.left);
|
|
351
|
-
const startY = e.clientY - parseFloat(box.style.top);
|
|
350
|
+
const startX = e.clientX - Number.parseFloat(box.style.left);
|
|
351
|
+
const startY = e.clientY - Number.parseFloat(box.style.top);
|
|
352
352
|
|
|
353
353
|
handle.style.cursor = 'grabbing';
|
|
354
354
|
|
|
355
355
|
const onMove = (ev) => {
|
|
356
356
|
const bw = box.offsetWidth;
|
|
357
357
|
const bh = box.offsetHeight;
|
|
358
|
-
box.style.left = `${Math.max(0, Math.min(ev.clientX - startX,
|
|
359
|
-
box.style.top = `${Math.max(0, Math.min(ev.clientY - startY,
|
|
358
|
+
box.style.left = `${Math.max(0, Math.min(ev.clientX - startX, globalThis.innerWidth - bw))}px`;
|
|
359
|
+
box.style.top = `${Math.max(0, Math.min(ev.clientY - startY, globalThis.innerHeight - bh))}px`;
|
|
360
360
|
};
|
|
361
361
|
|
|
362
362
|
const onUp = () => {
|
package/src/js/core/env.js
CHANGED
|
@@ -19,7 +19,7 @@ export const env = {
|
|
|
19
19
|
/** True if running on mobile */
|
|
20
20
|
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent),
|
|
21
21
|
/** True if touch is supported */
|
|
22
|
-
isTouch: 'ontouchstart' in
|
|
22
|
+
isTouch: 'ontouchstart' in globalThis || navigator.maxTouchPoints > 0,
|
|
23
23
|
/** Modifier key name depending on platform */
|
|
24
24
|
modifierKey: /Macintosh/.test(userAgent) ? 'metaKey' : 'ctrlKey',
|
|
25
25
|
};
|
package/src/js/core/func.js
CHANGED
|
@@ -31,7 +31,7 @@ export function debounce(fn, delay) {
|
|
|
31
31
|
/**
|
|
32
32
|
* Create a wrapper that limits how often `fn` can be invoked while ensuring the last call in a burst is executed.
|
|
33
33
|
* @param {Function} fn - Function to be throttled.
|
|
34
|
-
* @param {number} limit - Time
|
|
34
|
+
* @param {number} limit - Time globalThis in milliseconds during which at most one call is allowed.
|
|
35
35
|
* @returns {Function} A wrapper function that invokes `fn` at most once per `limit` milliseconds; calls preserve `this` and original arguments and schedule a trailing invocation for the final call in a burst.
|
|
36
36
|
*/
|
|
37
37
|
export function throttle(fn, limit) {
|
package/src/js/core/lists.js
CHANGED
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 = ((codeEl && codeEl.className) || '')
|
|
70
|
+
const langMatch = /language-(\S+)/.exec((codeEl && 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,13 +105,13 @@ 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().
|
|
108
|
+
Array.from(tr.querySelectorAll('th, td')).map((c) => c.textContent.trim().replaceAll('|', '\\|')),
|
|
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; };
|
|
112
112
|
let md = '\n\n';
|
|
113
113
|
md += `| ${padRow(cellTexts[0]).join(' | ')} |\n`;
|
|
114
|
-
md += `| ${Array(cols).fill('---').join(' | ')} |\n`;
|
|
114
|
+
md += `| ${new Array(cols).fill('---').join(' | ')} |\n`;
|
|
115
115
|
for (let r = 1; r < cellTexts.length; r++) {
|
|
116
116
|
md += `| ${padRow(cellTexts[r]).join(' | ')} |\n`;
|
|
117
117
|
}
|
|
@@ -139,7 +139,7 @@ export function isMarkdown(text) {
|
|
|
139
139
|
* @returns {string}
|
|
140
140
|
*/
|
|
141
141
|
export function markdownToHTML(text) {
|
|
142
|
-
const lines = text.
|
|
142
|
+
const lines = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n');
|
|
143
143
|
const out = [];
|
|
144
144
|
let i = 0;
|
|
145
145
|
|
|
@@ -147,7 +147,7 @@ export function markdownToHTML(text) {
|
|
|
147
147
|
const line = lines[i];
|
|
148
148
|
|
|
149
149
|
// ---- Fenced code block ``` lang ... ``` -----------------------------------
|
|
150
|
-
const fenceMatch =
|
|
150
|
+
const fenceMatch = /^```(\S*)$/.exec(line);
|
|
151
151
|
if (fenceMatch) {
|
|
152
152
|
const lang = fenceMatch[1];
|
|
153
153
|
const codeLines = [];
|
|
@@ -170,7 +170,7 @@ export function markdownToHTML(text) {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
// ---- ATX Headings # – ###### -------------------------------------------
|
|
173
|
-
const hMatch =
|
|
173
|
+
const hMatch = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
174
174
|
if (hMatch) {
|
|
175
175
|
const level = hMatch[1].length;
|
|
176
176
|
out.push(`<h${level}>${_inline(hMatch[2])}</h${level}>`);
|
|
@@ -228,10 +228,10 @@ export function markdownToHTML(text) {
|
|
|
228
228
|
bodyRows.push(_parseTableRow(lines[i]));
|
|
229
229
|
i++;
|
|
230
230
|
}
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
231
|
+
const thCells = headerCells.map((c) => `<th>${_inline(c)}</th>`).join('');
|
|
232
|
+
const thead = `<thead><tr>${thCells}</tr></thead>`;
|
|
233
|
+
const renderRow = (row) => `<tr>${row.map((c) => `<td>${_inline(c)}</td>`).join('')}</tr>`;
|
|
234
|
+
const tbody = bodyRows.length ? `<tbody>${bodyRows.map(renderRow).join('')}</tbody>` : '';
|
|
235
235
|
out.push(`<table>${thead}${tbody}</table>`);
|
|
236
236
|
continue;
|
|
237
237
|
}
|
|
@@ -298,16 +298,16 @@ function _inline(text) {
|
|
|
298
298
|
|
|
299
299
|
function _esc(v) {
|
|
300
300
|
return String(v)
|
|
301
|
-
.
|
|
302
|
-
.
|
|
303
|
-
.
|
|
301
|
+
.replaceAll('&', '&')
|
|
302
|
+
.replaceAll('<', '<')
|
|
303
|
+
.replaceAll('>', '>');
|
|
304
304
|
}
|
|
305
305
|
|
|
306
306
|
function _escAttr(v) {
|
|
307
307
|
return String(v)
|
|
308
|
-
.
|
|
309
|
-
.
|
|
310
|
-
.
|
|
311
|
-
.
|
|
312
|
-
.
|
|
308
|
+
.replaceAll('&', '&')
|
|
309
|
+
.replaceAll('"', '"')
|
|
310
|
+
.replaceAll("'", ''')
|
|
311
|
+
.replaceAll('<', '<')
|
|
312
|
+
.replaceAll('>', '>');
|
|
313
313
|
}
|
package/src/js/core/range.js
CHANGED
|
@@ -41,10 +41,10 @@ export class WrappedRange {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Select this wrapped range in the
|
|
44
|
+
* Select this wrapped range in the globalThis.
|
|
45
45
|
*/
|
|
46
46
|
select() {
|
|
47
|
-
const sel =
|
|
47
|
+
const sel = globalThis.getSelection();
|
|
48
48
|
if (!sel) return;
|
|
49
49
|
sel.removeAllRanges();
|
|
50
50
|
sel.addRange(this.toNativeRange());
|
|
@@ -116,13 +116,13 @@ export function fromNativeRange(range) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Returns a WrappedRange for the current
|
|
119
|
+
* Returns a WrappedRange for the current globalThis selection,
|
|
120
120
|
* optionally restricted to a given editable element.
|
|
121
121
|
* @param {HTMLElement} [editable]
|
|
122
122
|
* @returns {WrappedRange|null}
|
|
123
123
|
*/
|
|
124
124
|
export function currentRange(editable) {
|
|
125
|
-
const sel =
|
|
125
|
+
const sel = globalThis.getSelection();
|
|
126
126
|
if (!sel || sel.rangeCount === 0) return null;
|
|
127
127
|
const native = sel.getRangeAt(0);
|
|
128
128
|
// Optionally check that the selection is inside the editable element
|
|
@@ -161,7 +161,7 @@ export function collapsedRange(node, offset = 0) {
|
|
|
161
161
|
* @returns {boolean}
|
|
162
162
|
*/
|
|
163
163
|
export function isSelectionInside(el) {
|
|
164
|
-
const sel =
|
|
164
|
+
const sel = globalThis.getSelection();
|
|
165
165
|
if (!sel || sel.rangeCount === 0) return false;
|
|
166
166
|
return el.contains(sel.getRangeAt(0).commonAncestorContainer);
|
|
167
167
|
}
|
|
@@ -171,7 +171,7 @@ export function isSelectionInside(el) {
|
|
|
171
171
|
* @param {Function} fn
|
|
172
172
|
*/
|
|
173
173
|
export function withSavedRange(fn) {
|
|
174
|
-
const sel =
|
|
174
|
+
const sel = globalThis.getSelection();
|
|
175
175
|
if (!sel || sel.rangeCount === 0) {
|
|
176
176
|
fn(null);
|
|
177
177
|
return;
|
|
@@ -31,7 +31,7 @@ export class History {
|
|
|
31
31
|
* @returns {{ start: number, end: number }|null}
|
|
32
32
|
*/
|
|
33
33
|
_serializeSelection() {
|
|
34
|
-
const sel =
|
|
34
|
+
const sel = globalThis.getSelection();
|
|
35
35
|
if (!sel || sel.rangeCount === 0) return null;
|
|
36
36
|
const range = sel.getRangeAt(0);
|
|
37
37
|
if (!this.editable.contains(range.startContainer)) return null;
|
|
@@ -97,7 +97,7 @@ export class History {
|
|
|
97
97
|
const range = document.createRange();
|
|
98
98
|
range.setStart(startNode, startOff);
|
|
99
99
|
range.setEnd(endNode, endOff);
|
|
100
|
-
const sel =
|
|
100
|
+
const sel = globalThis.getSelection();
|
|
101
101
|
sel.removeAllRanges();
|
|
102
102
|
sel.addRange(range);
|
|
103
103
|
} catch (_) {
|
|
@@ -106,7 +106,7 @@ export class History {
|
|
|
106
106
|
const fb = document.createRange();
|
|
107
107
|
fb.setStart(this.editable, 0);
|
|
108
108
|
fb.collapse(true);
|
|
109
|
-
const s =
|
|
109
|
+
const s = globalThis.getSelection();
|
|
110
110
|
if (s) { s.removeAllRanges(); s.addRange(fb); }
|
|
111
111
|
} catch (_2) { /* fully give up */ }
|
|
112
112
|
}
|
|
@@ -181,7 +181,7 @@ export class History {
|
|
|
181
181
|
const current = this._serialize();
|
|
182
182
|
const { html: tokenized } = this._tokenizeImages(current);
|
|
183
183
|
const prev = this.stack[this.stackOffset];
|
|
184
|
-
if (prev
|
|
184
|
+
if (prev?.html === tokenized) return; // No change
|
|
185
185
|
this._savePoint();
|
|
186
186
|
}
|
|
187
187
|
|
package/src/js/editing/Style.js
CHANGED
|
@@ -40,19 +40,19 @@ export const italic = () => execCommand('italic');
|
|
|
40
40
|
* execCommand's state detection is unreliable.
|
|
41
41
|
*/
|
|
42
42
|
export function underline() {
|
|
43
|
-
const sel =
|
|
43
|
+
const sel = globalThis.getSelection();
|
|
44
44
|
if (!sel || !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
|
|
48
|
-
const uEl =
|
|
48
|
+
const uEl = /** @type {Element|null} */ (container)?.closest('u');
|
|
49
49
|
const nativeState = document.queryCommandState('underline');
|
|
50
50
|
if (uEl && !nativeState) {
|
|
51
51
|
// Browser doesn't recognise the underline state (e.g. inside <code>).
|
|
52
52
|
// Manually unwrap the <u> element.
|
|
53
53
|
const parent = uEl.parentNode;
|
|
54
54
|
while (uEl.firstChild) parent.insertBefore(uEl.firstChild, uEl);
|
|
55
|
-
|
|
55
|
+
uEl.remove();
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
execCommand('underline');
|
|
@@ -64,21 +64,21 @@ export function underline() {
|
|
|
64
64
|
* execCommand's state detection is unreliable (mirrors underline() logic).
|
|
65
65
|
*/
|
|
66
66
|
export function strikethrough() {
|
|
67
|
-
const sel =
|
|
67
|
+
const sel = globalThis.getSelection();
|
|
68
68
|
if (!sel || !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.
|
|
72
72
|
let sc = sel.getRangeAt(0).startContainer;
|
|
73
73
|
if (sc.nodeType === 3) sc = sc.parentElement;
|
|
74
|
-
const sEl =
|
|
74
|
+
const sEl = /** @type {Element|null} */ (sc)?.closest('s') || /** @type {Element|null} */ (sc)?.closest('strike');
|
|
75
75
|
const nativeState = document.queryCommandState('strikeThrough');
|
|
76
76
|
if (sEl && !nativeState) {
|
|
77
77
|
// Browser doesn’t recognise the strikethrough state (e.g. inside <code>
|
|
78
78
|
// or deeply nested inline formats). Manually unwrap the <s>/<strike>.
|
|
79
79
|
const parent = sEl.parentNode;
|
|
80
80
|
while (sEl.firstChild) parent.insertBefore(sEl.firstChild, sEl);
|
|
81
|
-
|
|
81
|
+
sEl.remove();
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
execCommand('strikeThrough');
|
|
@@ -119,7 +119,7 @@ export const fontName = (name) => execCommand('fontName', name);
|
|
|
119
119
|
* @param {HTMLElement|Document} [editable] - scoping element to avoid touching nodes outside this editor
|
|
120
120
|
*/
|
|
121
121
|
export function fontSize(size, editable = document) {
|
|
122
|
-
const sel =
|
|
122
|
+
const sel = globalThis.getSelection();
|
|
123
123
|
const wasCollapsed = !sel || !sel.rangeCount || sel.getRangeAt(0).collapsed;
|
|
124
124
|
|
|
125
125
|
// B-I-3/4: For a collapsed (caret) selection the browser's execCommand
|
|
@@ -158,7 +158,7 @@ export function fontSize(size, editable = document) {
|
|
|
158
158
|
span.style.fontSize = size;
|
|
159
159
|
el.parentNode.insertBefore(span, el);
|
|
160
160
|
while (el.firstChild) span.appendChild(el.firstChild);
|
|
161
|
-
el.
|
|
161
|
+
el.remove();
|
|
162
162
|
newSpans.push(span);
|
|
163
163
|
});
|
|
164
164
|
|
|
@@ -167,7 +167,7 @@ export function fontSize(size, editable = document) {
|
|
|
167
167
|
// value until the next selectionchange event).
|
|
168
168
|
if (!wasCollapsed && sel && newSpans.length > 0) {
|
|
169
169
|
const first = newSpans[0];
|
|
170
|
-
const last = newSpans
|
|
170
|
+
const last = newSpans.at(-1);
|
|
171
171
|
try {
|
|
172
172
|
const nr = document.createRange();
|
|
173
173
|
const startNode = first.firstChild || first;
|
|
@@ -222,11 +222,11 @@ export const indent = () => execCommand('indent');
|
|
|
222
222
|
* (which would destroy the ul > li checklist structure).
|
|
223
223
|
*/
|
|
224
224
|
export function outdent() {
|
|
225
|
-
const sel =
|
|
225
|
+
const sel = globalThis.getSelection();
|
|
226
226
|
if (sel && sel.rangeCount) {
|
|
227
227
|
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
228
228
|
if (container.nodeType === 3) container = container.parentElement;
|
|
229
|
-
const checkLi =
|
|
229
|
+
const checkLi = /** @type {Element|null} */ (container)?.closest('.an-checklist li');
|
|
230
230
|
if (checkLi) {
|
|
231
231
|
_checklistItemToP(/** @type {HTMLElement} */ (checkLi));
|
|
232
232
|
return;
|
|
@@ -261,7 +261,7 @@ function _checklistItemToP(checkLi) {
|
|
|
261
261
|
p.appendChild(child.cloneNode(true));
|
|
262
262
|
}
|
|
263
263
|
// Strip ZWS anchors left over from checklist markup
|
|
264
|
-
p.innerHTML = p.innerHTML.
|
|
264
|
+
p.innerHTML = p.innerHTML.replaceAll('\u200B', '');
|
|
265
265
|
if (!p.hasChildNodes() || !p.textContent.trim()) {
|
|
266
266
|
p.innerHTML = '';
|
|
267
267
|
p.appendChild(document.createTextNode('\u00a0'));
|
|
@@ -279,8 +279,8 @@ function _checklistItemToP(checkLi) {
|
|
|
279
279
|
checkUl.parentNode.insertBefore(p, checkUl.nextSibling);
|
|
280
280
|
|
|
281
281
|
// Remove current li from checkUl; delete checkUl if now empty
|
|
282
|
-
|
|
283
|
-
if (checkUl.children.length === 0) checkUl.
|
|
282
|
+
checkLi.remove();
|
|
283
|
+
if (checkUl.children.length === 0) checkUl.remove();
|
|
284
284
|
|
|
285
285
|
// Place caret at start of the new <p>
|
|
286
286
|
try {
|
|
@@ -288,7 +288,7 @@ function _checklistItemToP(checkLi) {
|
|
|
288
288
|
const firstChild = p.firstChild;
|
|
289
289
|
nr.setStart(firstChild && firstChild.nodeType === 3 ? firstChild : p, 0);
|
|
290
290
|
nr.collapse(true);
|
|
291
|
-
const s =
|
|
291
|
+
const s = globalThis.getSelection();
|
|
292
292
|
if (s) { s.removeAllRanges(); s.addRange(nr); }
|
|
293
293
|
} catch {}
|
|
294
294
|
}
|
|
@@ -316,7 +316,7 @@ export const insertOrderedList = () => execCommand('insertOrderedList');
|
|
|
316
316
|
* @param {string} value - Line-height value to apply; typically a unitless multiplier (for example, "1.5").
|
|
317
317
|
*/
|
|
318
318
|
export function lineHeight(value) {
|
|
319
|
-
const sel =
|
|
319
|
+
const sel = globalThis.getSelection();
|
|
320
320
|
if (!sel || sel.rangeCount === 0) return;
|
|
321
321
|
|
|
322
322
|
const range = sel.getRangeAt(0);
|
|
@@ -376,7 +376,7 @@ export function currentStyle(editable) {
|
|
|
376
376
|
const el = /** @type {Element|null} */ (isElement(container) ? container : container.parentElement);
|
|
377
377
|
if (!el) return {};
|
|
378
378
|
|
|
379
|
-
const computed =
|
|
379
|
+
const computed = globalThis.getComputedStyle(el);
|
|
380
380
|
|
|
381
381
|
return {
|
|
382
382
|
bold: document.queryCommandState('bold'),
|
|
@@ -405,12 +405,12 @@ export function currentStyle(editable) {
|
|
|
405
405
|
* @param {HTMLElement} [_editable]
|
|
406
406
|
*/
|
|
407
407
|
export function toggleInlineCode(_editable) {
|
|
408
|
-
const sel =
|
|
408
|
+
const sel = globalThis.getSelection();
|
|
409
409
|
if (!sel || !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;
|
|
413
|
-
const codeEl =
|
|
413
|
+
const codeEl = /** @type {Element|null} */ (container)?.closest('code');
|
|
414
414
|
if (codeEl && !codeEl.closest('pre')) {
|
|
415
415
|
// Unwrap — save range endpoints relative to surrounding text so we can
|
|
416
416
|
// restore the selection after normalize() merges adjacent text nodes.
|
|
@@ -419,17 +419,17 @@ export function toggleInlineCode(_editable) {
|
|
|
419
419
|
const prevSibling = codeEl.previousSibling;
|
|
420
420
|
const movedChildren = Array.from(codeEl.childNodes);
|
|
421
421
|
while (codeEl.firstChild) parent.insertBefore(codeEl.firstChild, codeEl);
|
|
422
|
-
|
|
422
|
+
codeEl.remove();
|
|
423
423
|
// Normalize only the immediate parent to merge adjacent text nodes without
|
|
424
424
|
// invalidating distant selection anchors (full editable.normalize() can
|
|
425
425
|
// cause selection offsets to shift, making subsequent format toggles miss).
|
|
426
|
-
|
|
426
|
+
parent?.normalize();
|
|
427
427
|
// Restore selection to the text that was inside the unwrapped <code>.
|
|
428
428
|
if (movedChildren.length > 0) {
|
|
429
429
|
try {
|
|
430
430
|
// After normalize, find the merged text node that contains the content.
|
|
431
431
|
const firstMoved = movedChildren[0];
|
|
432
|
-
const lastMoved = movedChildren
|
|
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
435
|
const anchorNode = (firstMoved.parentNode === parent) ? firstMoved : (prevSibling ? prevSibling.nextSibling : parent.firstChild);
|
|
@@ -476,11 +476,11 @@ export function toggleInlineCode(_editable) {
|
|
|
476
476
|
* @returns {boolean}
|
|
477
477
|
*/
|
|
478
478
|
export function isInlineCode() {
|
|
479
|
-
const sel =
|
|
479
|
+
const sel = globalThis.getSelection();
|
|
480
480
|
if (!sel || !sel.rangeCount) return false;
|
|
481
481
|
let sc = sel.getRangeAt(0).startContainer;
|
|
482
482
|
if (sc.nodeType === 3) sc = sc.parentElement;
|
|
483
|
-
const code =
|
|
483
|
+
const code = /** @type {Element|null} */ (sc)?.closest('code');
|
|
484
484
|
return !!(code && !code.closest('pre'));
|
|
485
485
|
}
|
|
486
486
|
|
|
@@ -503,13 +503,13 @@ export function isInlineCode() {
|
|
|
503
503
|
* Empty or whitespace-only selections do not create a checklist.
|
|
504
504
|
*/
|
|
505
505
|
export function toggleChecklist() {
|
|
506
|
-
const sel =
|
|
506
|
+
const sel = globalThis.getSelection();
|
|
507
507
|
if (!sel || !sel.rangeCount) return;
|
|
508
508
|
const range = sel.getRangeAt(0);
|
|
509
509
|
let container = range.commonAncestorContainer;
|
|
510
510
|
if (container.nodeType === 3) container = container.parentElement;
|
|
511
511
|
|
|
512
|
-
const ul =
|
|
512
|
+
const ul = /** @type {Element|null} */ (container)?.closest('.an-checklist');
|
|
513
513
|
if (ul) {
|
|
514
514
|
// If selection covers multiple <li>, convert them all
|
|
515
515
|
const selectedLis = Array.from(ul.querySelectorAll('li')).filter((li) =>
|
|
@@ -523,14 +523,14 @@ export function toggleChecklist() {
|
|
|
523
523
|
if (child.nodeType === 1 && /** @type {Element} */ (child).tagName === 'INPUT') continue;
|
|
524
524
|
p.appendChild(child.cloneNode(true));
|
|
525
525
|
}
|
|
526
|
-
p.innerHTML = p.innerHTML.
|
|
526
|
+
p.innerHTML = p.innerHTML.replaceAll('\u200b', '');
|
|
527
527
|
if (!p.hasChildNodes() || !p.textContent.trim()) {
|
|
528
528
|
p.innerHTML = '';
|
|
529
529
|
p.appendChild(document.createTextNode('\u00a0'));
|
|
530
530
|
}
|
|
531
531
|
ul.parentNode.insertBefore(p, ul);
|
|
532
532
|
if (!firstP) firstP = p;
|
|
533
|
-
|
|
533
|
+
li.remove();
|
|
534
534
|
});
|
|
535
535
|
if (ul.children.length === 0) ul.remove();
|
|
536
536
|
// Move caret to first converted paragraph
|
|
@@ -561,7 +561,7 @@ export function toggleChecklist() {
|
|
|
561
561
|
? Array.from(block.childNodes)
|
|
562
562
|
.map((n) => n.textContent)
|
|
563
563
|
.join('')
|
|
564
|
-
.
|
|
564
|
+
.replaceAll('\u00a0', ' ')
|
|
565
565
|
: '';
|
|
566
566
|
|
|
567
567
|
const ul = document.createElement('ul');
|
|
@@ -656,7 +656,7 @@ export function toggleChecklist() {
|
|
|
656
656
|
// Insert the new list before the first block, then remove all source blocks.
|
|
657
657
|
const firstBlock = blocks[0];
|
|
658
658
|
firstBlock.parentNode.insertBefore(newUl, firstBlock);
|
|
659
|
-
blocks.forEach((block) => block.
|
|
659
|
+
blocks.forEach((block) => block.remove());
|
|
660
660
|
|
|
661
661
|
// Move caret to end of the last checklist item.
|
|
662
662
|
if (lastTextNode) {
|
|
@@ -673,9 +673,9 @@ export function toggleChecklist() {
|
|
|
673
673
|
* @returns {boolean}
|
|
674
674
|
*/
|
|
675
675
|
export function isInChecklist() {
|
|
676
|
-
const sel =
|
|
676
|
+
const sel = globalThis.getSelection();
|
|
677
677
|
if (!sel || !sel.rangeCount) return false;
|
|
678
678
|
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
679
679
|
if (container.nodeType === 3) container = container.parentElement;
|
|
680
|
-
return !!(
|
|
680
|
+
return !!(/** @type {Element|null} */ (container)?.closest('.an-checklist li'));
|
|
681
681
|
}
|
package/src/js/editing/Table.js
CHANGED
|
@@ -56,7 +56,7 @@ export function insertTable(cols, rows, opts = {}) {
|
|
|
56
56
|
if (cols <= 0 || rows <= 0) return;
|
|
57
57
|
const table = createTable(cols, rows, opts);
|
|
58
58
|
|
|
59
|
-
const sel =
|
|
59
|
+
const sel = globalThis.getSelection();
|
|
60
60
|
if (!sel || sel.rangeCount === 0) return;
|
|
61
61
|
const range = sel.getRangeAt(0);
|
|
62
62
|
range.deleteContents();
|
|
@@ -64,7 +64,7 @@ export function insertTable(cols, rows, opts = {}) {
|
|
|
64
64
|
// Walk up to find the nearest block-level ancestor to insert after
|
|
65
65
|
const BLOCK = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI', 'PRE']);
|
|
66
66
|
let anchor = /** @type {Element|null} */ (range.startContainer);
|
|
67
|
-
if (anchor
|
|
67
|
+
if (anchor?.nodeType === 3) anchor = anchor.parentElement;
|
|
68
68
|
while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) {
|
|
69
69
|
anchor = anchor.parentElement;
|
|
70
70
|
}
|