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
|
@@ -11,7 +11,7 @@ export class History {
|
|
|
11
11
|
constructor(editable, limit = 100) {
|
|
12
12
|
this.editable = editable;
|
|
13
13
|
this._limit = limit;
|
|
14
|
-
/** @type {Array<{html: string,
|
|
14
|
+
/** @type {Array<{html: string, images?: Record<string,string>, sel: {start: number, end: number}|null}>} */
|
|
15
15
|
this.stack = [];
|
|
16
16
|
this.stackOffset = -1;
|
|
17
17
|
this._savePoint();
|
|
@@ -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;
|
|
@@ -54,7 +54,7 @@ export class History {
|
|
|
54
54
|
let cur;
|
|
55
55
|
while ((cur = walker.nextNode())) {
|
|
56
56
|
if (cur === node) return count + offset;
|
|
57
|
-
count += cur.length;
|
|
57
|
+
count += /** @type {Text} */ (cur).length;
|
|
58
58
|
}
|
|
59
59
|
return 0;
|
|
60
60
|
}
|
|
@@ -71,7 +71,7 @@ export class History {
|
|
|
71
71
|
const walker = document.createTreeWalker(this.editable, NodeFilter.SHOW_TEXT, null);
|
|
72
72
|
let cur;
|
|
73
73
|
while ((cur = walker.nextNode())) {
|
|
74
|
-
const len = cur.length;
|
|
74
|
+
const len = /** @type {Text} */ (cur).length;
|
|
75
75
|
if (!startNode && count + len >= saved.start) {
|
|
76
76
|
startNode = cur;
|
|
77
77
|
startOff = saved.start - count;
|
|
@@ -88,7 +88,7 @@ export class History {
|
|
|
88
88
|
const lastWalker = document.createTreeWalker(this.editable, NodeFilter.SHOW_TEXT, null);
|
|
89
89
|
let lastNode = null;
|
|
90
90
|
while ((lastNode = lastWalker.nextNode())) { startNode = lastNode; }
|
|
91
|
-
startOff = startNode ? startNode.length : 0;
|
|
91
|
+
startOff = startNode ? /** @type {Text} */ (startNode).length : 0;
|
|
92
92
|
endNode = startNode;
|
|
93
93
|
endOff = startOff;
|
|
94
94
|
}
|
|
@@ -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
|
}
|
|
@@ -148,8 +148,8 @@ export class History {
|
|
|
148
148
|
*/
|
|
149
149
|
_tokenizeImages(html) {
|
|
150
150
|
// Fast-path: skip regex entirely when there are no data URIs (common case)
|
|
151
|
-
if (!html.includes('data:')) return { html, images: {} };
|
|
152
|
-
const images = {};
|
|
151
|
+
if (!html.includes('data:')) return { html, images: /** @type {Record<string,string>} */ ({}) };
|
|
152
|
+
const images = /** @type {Record<string,string>} */ ({});
|
|
153
153
|
let index = 0;
|
|
154
154
|
const tokenized = html.replace(/data:[^;]+;base64,[^"' >]*/g, (match) => {
|
|
155
155
|
const token = `__asn_img_${index}__`;
|
|
@@ -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');
|
|
@@ -116,10 +116,10 @@ export const fontName = (name) => execCommand('fontName', name);
|
|
|
116
116
|
* Sets the font size (in pt or with unit) for the selection.
|
|
117
117
|
* Uses a span-based approach to set px sizes precisely.
|
|
118
118
|
* @param {string} size - e.g. '14px'
|
|
119
|
-
* @param {HTMLElement} [editable] - scoping element to avoid touching nodes outside this editor
|
|
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,13 +222,13 @@ 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
|
-
_checklistItemToP(checkLi);
|
|
231
|
+
_checklistItemToP(/** @type {HTMLElement} */ (checkLi));
|
|
232
232
|
return;
|
|
233
233
|
}
|
|
234
234
|
}
|
|
@@ -257,11 +257,11 @@ function _checklistItemToP(checkLi) {
|
|
|
257
257
|
// Build <p> preserving inline formatting (bold/italic/links) from the item's content
|
|
258
258
|
const p = document.createElement('p');
|
|
259
259
|
for (const child of checkLi.childNodes) {
|
|
260
|
-
if (child.nodeType === 1 && child.tagName === 'INPUT') continue;
|
|
260
|
+
if (child.nodeType === 1 && /** @type {Element} */ (child).tagName === 'INPUT') continue;
|
|
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);
|
|
@@ -373,10 +373,10 @@ export function currentStyle(editable) {
|
|
|
373
373
|
? range.sc
|
|
374
374
|
: range.commonAncestor();
|
|
375
375
|
|
|
376
|
-
const el = isElement(container) ? container : container.parentElement;
|
|
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'),
|
|
@@ -402,15 +402,15 @@ export function currentStyle(editable) {
|
|
|
402
402
|
/**
|
|
403
403
|
* Wraps the selection in an inline <code> element, or unwraps it if the
|
|
404
404
|
* cursor is already inside a <code> that is not inside a <pre>.
|
|
405
|
-
* @param {HTMLElement} [
|
|
405
|
+
* @param {HTMLElement} [_editable]
|
|
406
406
|
*/
|
|
407
|
-
export function toggleInlineCode(
|
|
408
|
-
const sel =
|
|
407
|
+
export function toggleInlineCode(_editable) {
|
|
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,34 +503,34 @@ 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) =>
|
|
516
516
|
sel.containsNode(li, true),
|
|
517
517
|
);
|
|
518
518
|
if (selectedLis.length > 0) {
|
|
519
|
-
let firstP = null;
|
|
519
|
+
/** @type {HTMLElement|null} */ let firstP = null;
|
|
520
520
|
selectedLis.forEach((li) => {
|
|
521
521
|
const p = document.createElement('p');
|
|
522
522
|
for (const child of li.childNodes) {
|
|
523
|
-
if (child.nodeType === 1 && child.tagName === 'INPUT') continue;
|
|
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
|
|
@@ -551,9 +551,9 @@ export function toggleChecklist() {
|
|
|
551
551
|
// Find the nearest block-level ancestor (p, div, li, h1-h6, blockquote, etc.)
|
|
552
552
|
// and convert it into a single checklist item.
|
|
553
553
|
const BLOCK_TAGS = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI']);
|
|
554
|
-
let block = container;
|
|
554
|
+
let block = /** @type {Element|null} */ (container);
|
|
555
555
|
while (block && block.parentNode && !BLOCK_TAGS.has(block.tagName)) {
|
|
556
|
-
block = block.parentNode;
|
|
556
|
+
block = /** @type {Element|null} */ (block.parentNode);
|
|
557
557
|
}
|
|
558
558
|
// Fallback: if no block element found (e.g. cursor directly in editable root), use the
|
|
559
559
|
// insertion approach with a zero-width-space item so the cursor ends up inside.
|
|
@@ -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');
|
|
@@ -619,7 +619,7 @@ export function toggleChecklist() {
|
|
|
619
619
|
let node;
|
|
620
620
|
while ((node = iter.nextNode())) {
|
|
621
621
|
if (!range.intersectsNode(node)) continue;
|
|
622
|
-
let block = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
|
|
622
|
+
let block = /** @type {Element|null} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
|
|
623
623
|
while (block && !BLOCK_TAGS_MULTI.has(block.tagName)) {
|
|
624
624
|
block = block.parentElement;
|
|
625
625
|
}
|
|
@@ -634,7 +634,7 @@ export function toggleChecklist() {
|
|
|
634
634
|
// Build checklist and replace collected blocks.
|
|
635
635
|
const newUl = document.createElement('ul');
|
|
636
636
|
newUl.className = 'an-checklist';
|
|
637
|
-
let lastTextNode = null;
|
|
637
|
+
/** @type {Text|null} */ let lastTextNode = null;
|
|
638
638
|
blocks.forEach((block) => {
|
|
639
639
|
const li = document.createElement('li');
|
|
640
640
|
const cb = document.createElement('input');
|
|
@@ -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
|
@@ -13,8 +13,7 @@ import { createElement } from '../core/dom.js';
|
|
|
13
13
|
* Build an HTML table with the given number of columns and rows, optionally including a header row.
|
|
14
14
|
* @param {number} cols - Number of columns in each row.
|
|
15
15
|
* @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
|
|
16
|
-
* @param {{ headerRow?: boolean }} [opts] - Options
|
|
17
|
-
* @param {boolean} [opts.headerRow=false] - When true and `rows > 0`, creates a header row (`<thead>`) plus body rows for the remainder.
|
|
16
|
+
* @param {{ headerRow?: boolean }} [opts] - Options: `headerRow` creates a `<thead>` when true.
|
|
18
17
|
* @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
|
|
19
18
|
*/
|
|
20
19
|
export function createTable(cols, rows, opts = {}) {
|
|
@@ -44,7 +43,7 @@ export function createTable(cols, rows, opts = {}) {
|
|
|
44
43
|
}
|
|
45
44
|
tbody.appendChild(tr);
|
|
46
45
|
}
|
|
47
|
-
return table;
|
|
46
|
+
return /** @type {HTMLTableElement} */ (table);
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
/**
|
|
@@ -52,21 +51,20 @@ export function createTable(cols, rows, opts = {}) {
|
|
|
52
51
|
* @param {number} cols - Number of columns for the new table.
|
|
53
52
|
* @param {number} rows - Number of rows for the new table.
|
|
54
53
|
* @param {{ headerRow?: boolean }} [opts] - Options for table creation.
|
|
55
|
-
* @param {boolean} [opts.headerRow=false] - If true, include a header row as the first row.
|
|
56
54
|
*/
|
|
57
55
|
export function insertTable(cols, rows, opts = {}) {
|
|
58
56
|
if (cols <= 0 || rows <= 0) return;
|
|
59
57
|
const table = createTable(cols, rows, opts);
|
|
60
58
|
|
|
61
|
-
const sel =
|
|
59
|
+
const sel = globalThis.getSelection();
|
|
62
60
|
if (!sel || sel.rangeCount === 0) return;
|
|
63
61
|
const range = sel.getRangeAt(0);
|
|
64
62
|
range.deleteContents();
|
|
65
63
|
|
|
66
64
|
// Walk up to find the nearest block-level ancestor to insert after
|
|
67
65
|
const BLOCK = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI', 'PRE']);
|
|
68
|
-
let anchor = range.startContainer;
|
|
69
|
-
if (anchor
|
|
66
|
+
let anchor = /** @type {Element|null} */ (range.startContainer);
|
|
67
|
+
if (anchor?.nodeType === 3) anchor = anchor.parentElement;
|
|
70
68
|
while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) {
|
|
71
69
|
anchor = anchor.parentElement;
|
|
72
70
|
}
|
package/src/js/editing/Typing.js
CHANGED
|
@@ -14,8 +14,8 @@ import { currentRange } from '../core/range.js';
|
|
|
14
14
|
// at ~120+ events/sec during normal typing.
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
const _FA_PATTERN = /\bfa-/;
|
|
17
|
-
const isFAIcon = (n) => !!(n
|
|
18
|
-
const isZwsAnchor = (n) => !!(n
|
|
17
|
+
const isFAIcon = (n) => !!(n?.nodeName === 'I' && _FA_PATTERN.test(n.className || ''));
|
|
18
|
+
const isZwsAnchor = (n) => !!(n?.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Handles special keydown behaviour inside the editor.
|
|
@@ -26,7 +26,7 @@ const isZwsAnchor = (n) => !!(n && n.nodeType === Node.TEXT_NODE && (n.textConte
|
|
|
26
26
|
*/
|
|
27
27
|
export function handleKeydown(event, editable, options = {}) {
|
|
28
28
|
const moveCaret = (setFn) => {
|
|
29
|
-
const sel =
|
|
29
|
+
const sel = globalThis.getSelection();
|
|
30
30
|
if (!sel) return false;
|
|
31
31
|
const nr = document.createRange();
|
|
32
32
|
setFn(nr);
|
|
@@ -40,15 +40,15 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
40
40
|
// Backspace key — one-press deletion of a preceding FA icon (<i> element)
|
|
41
41
|
// -------------------------------------------------------------------------
|
|
42
42
|
if (isKey(event, key.BACKSPACE)) {
|
|
43
|
-
const sel =
|
|
44
|
-
if (sel
|
|
43
|
+
const sel = globalThis.getSelection();
|
|
44
|
+
if (sel?.rangeCount > 0) {
|
|
45
45
|
const r = sel.getRangeAt(0);
|
|
46
46
|
if (r.collapsed && r.startContainer.nodeType === Node.TEXT_NODE) {
|
|
47
|
-
const textNode = r.startContainer;
|
|
47
|
+
const textNode = /** @type {ChildNode} */ (r.startContainer);
|
|
48
48
|
// Case A: cursor at offset 0, preceding sibling is an FA icon
|
|
49
49
|
if (r.startOffset === 0 && isFAIcon(textNode.previousSibling)) {
|
|
50
50
|
event.preventDefault();
|
|
51
|
-
textNode.previousSibling.remove();
|
|
51
|
+
/** @type {ChildNode} */ (textNode.previousSibling).remove();
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -59,7 +59,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
59
59
|
isFAIcon(textNode.previousSibling)) {
|
|
60
60
|
event.preventDefault();
|
|
61
61
|
const parent = textNode.parentNode;
|
|
62
|
-
const icon = textNode.previousSibling;
|
|
62
|
+
const icon = /** @type {ChildNode} */ (textNode.previousSibling);
|
|
63
63
|
const prevNode = icon.previousSibling; // node before the icon (e.g. ZWS of prior icon)
|
|
64
64
|
icon.remove();
|
|
65
65
|
textNode.remove();
|
|
@@ -89,7 +89,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
89
89
|
// ArrowLeft / ArrowRight — one-press navigation across FA icon nodes
|
|
90
90
|
// -------------------------------------------------------------------------
|
|
91
91
|
if (isKey(event, key.LEFT) || isKey(event, key.RIGHT)) {
|
|
92
|
-
const sel =
|
|
92
|
+
const sel = globalThis.getSelection();
|
|
93
93
|
if (!sel || sel.rangeCount === 0) return false;
|
|
94
94
|
|
|
95
95
|
const r = sel.getRangeAt(0);
|
|
@@ -207,7 +207,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
// In a pre/code block, insert spaces using configured tabSize
|
|
210
|
-
if (para
|
|
210
|
+
if (para?.nodeName.toUpperCase() === 'PRE') {
|
|
211
211
|
if (event.shiftKey) return false;
|
|
212
212
|
event.preventDefault();
|
|
213
213
|
execCommand('insertText', ' '.repeat(options.tabSize || 4));
|
|
@@ -241,25 +241,25 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
241
241
|
|
|
242
242
|
// Hoist sc/el once so all guards below can reuse them.
|
|
243
243
|
const sc = range.sc;
|
|
244
|
-
const el = sc.nodeType === 3 ? sc.parentElement : sc;
|
|
244
|
+
const el = /** @type {Element|null} */ (sc.nodeType === 3 ? sc.parentElement : sc);
|
|
245
245
|
|
|
246
246
|
// Guard: if the cursor is inside a <i> FA icon element (zero text children,
|
|
247
247
|
// rendered entirely by CSS ::before), pressing Enter would split the block
|
|
248
248
|
// and leave an orphan <i> in the new paragraph — visually an "auto-created
|
|
249
249
|
// icon". Push the cursor to just after the <i> first, then fall through so
|
|
250
250
|
// the browser fires its default Enter at a safe text boundary.
|
|
251
|
-
if (el
|
|
251
|
+
if (el?.nodeName === 'I' && /\bfa-/.test(el.className || '')) {
|
|
252
252
|
const nr = document.createRange();
|
|
253
253
|
nr.setStartAfter(el);
|
|
254
254
|
nr.collapse(true);
|
|
255
|
-
const selI =
|
|
255
|
+
const selI = globalThis.getSelection();
|
|
256
256
|
if (selI) { selI.removeAllRanges(); selI.addRange(nr); }
|
|
257
257
|
return false; // cursor is now outside <i> — let browser default handle Enter
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
// Video wrapper — Enter should create a new paragraph after the wrapper,
|
|
261
261
|
// not split the wrapper's container and produce an empty video clone.
|
|
262
|
-
const videoWrapper = el
|
|
262
|
+
const videoWrapper = el?.closest('.an-video-wrapper');
|
|
263
263
|
if (videoWrapper) {
|
|
264
264
|
event.preventDefault();
|
|
265
265
|
const p = document.createElement('p');
|
|
@@ -268,18 +268,18 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
268
268
|
const nr = document.createRange();
|
|
269
269
|
nr.setStart(p, 0);
|
|
270
270
|
nr.collapse(true);
|
|
271
|
-
const sel =
|
|
271
|
+
const sel = globalThis.getSelection();
|
|
272
272
|
sel.removeAllRanges();
|
|
273
273
|
sel.addRange(nr);
|
|
274
274
|
return true;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// Checklist — Enter creates new item; empty item exits the list
|
|
278
|
-
const checkLi = el
|
|
278
|
+
const checkLi = el?.closest('.an-checklist li');
|
|
279
279
|
if (checkLi) {
|
|
280
280
|
event.preventDefault();
|
|
281
281
|
const ul = checkLi.closest('.an-checklist');
|
|
282
|
-
const sel =
|
|
282
|
+
const sel = globalThis.getSelection();
|
|
283
283
|
let nativeRange = sel.getRangeAt(0);
|
|
284
284
|
|
|
285
285
|
// Helper: get trimmed text content of a li, excluding the checkbox INPUT.
|
|
@@ -358,14 +358,14 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
358
358
|
const para = closestPara(range.sc, editable);
|
|
359
359
|
|
|
360
360
|
// Enter in a pre/code block: insert a literal newline instead of a new block
|
|
361
|
-
if (para
|
|
361
|
+
if (para?.nodeName.toUpperCase() === 'PRE') {
|
|
362
362
|
event.preventDefault();
|
|
363
363
|
execCommand('insertText', '\n');
|
|
364
364
|
return true;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
// Pressing Enter at the end of a blockquote should exit it
|
|
368
|
-
if (para
|
|
368
|
+
if (para?.nodeName.toUpperCase() === 'BLOCKQUOTE') {
|
|
369
369
|
const native = range.toNativeRange();
|
|
370
370
|
native.setEnd(para, para.childNodes.length);
|
|
371
371
|
if (native.toString() === '' && range.isCollapsed()) {
|
package/src/js/i18n/en.js
CHANGED
|
@@ -298,6 +298,8 @@ export const en = {
|
|
|
298
298
|
rowHeight: 'Row Height',
|
|
299
299
|
tableBorderWidth: 'Table Border Width',
|
|
300
300
|
deleteTable: 'Delete Table',
|
|
301
|
+
cellBackground: 'Cell Background',
|
|
302
|
+
noShading: 'No Shading',
|
|
301
303
|
columnWidthPx: 'Column Width (px)',
|
|
302
304
|
rowHeightPx: 'Row Height (px)',
|
|
303
305
|
tableBorderWidthPx: 'Table Border Width (px)',
|
package/src/js/i18n/vi.js
CHANGED
|
@@ -293,6 +293,8 @@ export const vi = {
|
|
|
293
293
|
rowHeight: 'Chiều cao hàng',
|
|
294
294
|
tableBorderWidth: 'Độ rộng viền bảng',
|
|
295
295
|
deleteTable: 'Xóa bảng',
|
|
296
|
+
cellBackground: 'Màu Nền Ô',
|
|
297
|
+
noShading: 'Xóa Màu Nền',
|
|
296
298
|
columnWidthPx: 'Chiều rộng cột (px)',
|
|
297
299
|
rowHeightPx: 'Chiều cao hàng (px)',
|
|
298
300
|
tableBorderWidthPx: 'Độ rộng viền bảng (px)',
|
package/src/js/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* const editor = AutumnNote.create('#my-editor');
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
// @ts-ignore
|
|
12
13
|
import '../styles/autumnnote.scss';
|
|
13
14
|
import { Context, _customModules, _globalPlugins } from './Context.js';
|
|
14
15
|
import { registerButton } from './module/Buttons.js';
|
|
@@ -49,7 +50,7 @@ const AutumnNote = {
|
|
|
49
50
|
const elements = resolveElements(selector);
|
|
50
51
|
const ctxs = elements.map((el) => {
|
|
51
52
|
if (instances.has(el)) return instances.get(el);
|
|
52
|
-
const ctx = new Context(el, options);
|
|
53
|
+
const ctx = new Context(/** @type {HTMLElement} */ (el), options);
|
|
53
54
|
ctx.initialize();
|
|
54
55
|
instances.set(el, ctx);
|
|
55
56
|
return ctx;
|
|
@@ -159,7 +160,7 @@ function resolveElements(selector) {
|
|
|
159
160
|
return [selector];
|
|
160
161
|
}
|
|
161
162
|
if (selector instanceof NodeList || Array.isArray(selector)) {
|
|
162
|
-
return Array.from(selector);
|
|
163
|
+
return /** @type {Element[]} */ (Array.from(selector));
|
|
163
164
|
}
|
|
164
165
|
return [];
|
|
165
166
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Activated when both `autoSave` and `autoSaveRestore` options are true.
|
|
6
6
|
* On initialize it checks localStorage for a draft that is within the
|
|
7
|
-
* `autoSaveRestoreTimeout` day
|
|
7
|
+
* `autoSaveRestoreTimeout` day globalThis. If one is found a dismissible banner
|
|
8
8
|
* is prepended to the editor container.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -118,9 +118,7 @@ export class AutoSaveRestore {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
_removeBanner() {
|
|
121
|
-
|
|
122
|
-
this._banner.parentNode.removeChild(this._banner);
|
|
123
|
-
}
|
|
121
|
+
this._banner?.remove();
|
|
124
122
|
this._banner = null;
|
|
125
123
|
}
|
|
126
124
|
}
|