autumnnote 1.5.0 → 1.6.0
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 +6 -4
- package/dist/autumnnote.css +324 -2
- package/dist/autumnnote.es.js +638 -227
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +637 -226
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/js/Context.js +8 -3
- package/src/js/core/detectLang.js +98 -0
- package/src/js/core/dom.js +62 -6
- package/src/js/core/markdown.js +14 -13
- package/src/js/core/range.js +2 -2
- package/src/js/editing/History.js +6 -6
- package/src/js/editing/Style.js +19 -19
- package/src/js/editing/Table.js +4 -6
- package/src/js/editing/Typing.js +6 -6
- 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/BubbleToolbar.js +30 -13
- package/src/js/module/Buttons.js +16 -14
- package/src/js/module/Clipboard.js +5 -5
- package/src/js/module/CodeTooltip.js +42 -16
- package/src/js/module/Codeview.js +2 -2
- package/src/js/module/ContextMenu.js +12 -12
- package/src/js/module/Editor.js +64 -12
- package/src/js/module/EmojiDialog.js +19 -13
- package/src/js/module/FindReplace.js +85 -59
- package/src/js/module/Fullscreen.js +1 -1
- package/src/js/module/IconDialog.js +26 -20
- package/src/js/module/ImageCropOverlay.js +6 -6
- package/src/js/module/ImageDialog.js +18 -12
- package/src/js/module/ImageResizer.js +1 -1
- package/src/js/module/ImageTooltip.js +8 -4
- package/src/js/module/LinkDialog.js +18 -12
- package/src/js/module/LinkTooltip.js +5 -2
- package/src/js/module/Mention.js +3 -3
- package/src/js/module/Statusbar.js +2 -2
- package/src/js/module/TableTooltip.js +180 -18
- package/src/js/module/Toolbar.js +16 -15
- package/src/js/module/VideoDialog.js +8 -2
- package/src/js/module/VideoResizer.js +3 -3
- package/src/js/module/VideoTooltip.js +11 -6
- package/src/js/renderer.js +4 -2
- package/src/js/settings.js +53 -36
- package/src/styles/autumnnote.scss +332 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Fast. Lightweight. Reliable. Efficient. A modern WYSIWYG editor built with vanilla JavaScript, no jQuery required.",
|
|
5
5
|
"main": "dist/autumnnote.umd.js",
|
|
6
6
|
"module": "dist/autumnnote.es.js",
|
package/src/js/Context.js
CHANGED
|
@@ -59,7 +59,7 @@ export class Context {
|
|
|
59
59
|
this.locale = resolveLocale(this.options.lang);
|
|
60
60
|
|
|
61
61
|
/** @type {{ container: HTMLElement, editable: HTMLElement, toolbar?: HTMLElement, statusbar?: HTMLElement }} */
|
|
62
|
-
this.layoutInfo = {};
|
|
62
|
+
this.layoutInfo = /** @type {any} */ ({});
|
|
63
63
|
|
|
64
64
|
/** @type {Map<string, Function[]>} */
|
|
65
65
|
this._listeners = new Map();
|
|
@@ -167,7 +167,7 @@ export class Context {
|
|
|
167
167
|
/**
|
|
168
168
|
* Registers and initialises a custom module on this instance.
|
|
169
169
|
* @param {string} name
|
|
170
|
-
* @param {
|
|
170
|
+
* @param {new (ctx: this) => any} ModuleClass
|
|
171
171
|
* @returns {this}
|
|
172
172
|
*/
|
|
173
173
|
registerModule(name, ModuleClass) {
|
|
@@ -555,11 +555,16 @@ export class Context {
|
|
|
555
555
|
this._disposers = [];
|
|
556
556
|
|
|
557
557
|
const container = this.layoutInfo.container;
|
|
558
|
+
const wasDark = container && container.classList.contains('an-theme-dark');
|
|
558
559
|
if (container && container.parentNode) {
|
|
559
560
|
// Restore original element
|
|
560
561
|
this.targetEl.style.display = '';
|
|
561
562
|
container.parentNode.removeChild(container);
|
|
562
563
|
}
|
|
564
|
+
// If this was a dark editor and no other dark containers remain, clean up body
|
|
565
|
+
if (wasDark && !document.querySelector('.an-container.an-theme-dark')) {
|
|
566
|
+
document.body.classList.remove('an-theme-dark');
|
|
567
|
+
}
|
|
563
568
|
|
|
564
569
|
if (typeof this.options.onDestroy === 'function') {
|
|
565
570
|
this.options.onDestroy(this);
|
|
@@ -578,7 +583,7 @@ export class Context {
|
|
|
578
583
|
*/
|
|
579
584
|
_syncToTarget() {
|
|
580
585
|
if (this.targetEl.tagName === 'TEXTAREA' || this.targetEl.tagName === 'INPUT') {
|
|
581
|
-
this.targetEl.value = this.getHTML();
|
|
586
|
+
/** @type {HTMLInputElement} */ (this.targetEl).value = this.getHTML();
|
|
582
587
|
}
|
|
583
588
|
}
|
|
584
589
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detectLang.js — Heuristic programming-language detection for code snippets.
|
|
3
|
+
*
|
|
4
|
+
* Returns a Prism.js language identifier or null when no language can be
|
|
5
|
+
* determined with reasonable confidence.
|
|
6
|
+
*
|
|
7
|
+
* Detection order (conflicts in parentheses):
|
|
8
|
+
* TypeScript → Rust → PHP → Java → Kotlin → Swift → Go
|
|
9
|
+
* → JavaScript → HTML → CSS → JSON → SQL → Python → Ruby
|
|
10
|
+
* → Bash → C++ → C# → C → XML
|
|
11
|
+
*
|
|
12
|
+
* @param {string} code
|
|
13
|
+
* @returns {string|null}
|
|
14
|
+
*/
|
|
15
|
+
export function detectLang(code) {
|
|
16
|
+
if (!code || !code.trim()) return null;
|
|
17
|
+
const s = code.trim();
|
|
18
|
+
|
|
19
|
+
// ── TypeScript ─────────────────────────────────────────────────────────────
|
|
20
|
+
// Must be first — TS is a JS superset; its markers are specific.
|
|
21
|
+
if (/(:\s*(string|number|boolean|void|never|any|unknown)\b|interface\s+\w+\s*\{|type\s+\w+\s*[=<(]|<\w+>\s*[;,)]|readonly\s+\w|enum\s+\w+\s*\{|\?\s*:\s*\w|as\s+\w+\s*[;,)\]])/.test(s)) return 'typescript';
|
|
22
|
+
|
|
23
|
+
// ── Rust ────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Before JavaScript — both have `let`. `!` macros and `pub fn` are unique.
|
|
25
|
+
if (/\bprintln!\s*\(|\bprint!\s*\(|\bfn\s+\w+\s*(<[^>]*>)?\s*\(|\blet\s+mut\s|\bpub\s+fn\s|\buse\s+std::|\bimpl\s+\w+|\bOption<|\bResult<\w+/.test(s)) return 'rust';
|
|
26
|
+
|
|
27
|
+
// ── PHP ─────────────────────────────────────────────────────────────────────
|
|
28
|
+
// Before Bash — both have `echo`. `<?php` and `$var` are unique to PHP.
|
|
29
|
+
if (/(<\?php\b|<\?=|\becho\s+.*\$\w|\$this->|\$\w+\s*=\s*\w|\bforeach\s*\(\s*\$|Illuminate\\)/.test(s)) return 'php';
|
|
30
|
+
|
|
31
|
+
// ── Java ────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Before Kotlin — `System.out.println()` would otherwise match Kotlin's
|
|
33
|
+
// `println\s*\(` pattern.
|
|
34
|
+
if (/\bpublic\s+(class|static|void|int|String)\s+\w|System\.out\.(print|println)\s*\(|@(Override|Autowired|Component|Service|Controller)\b|import\s+java\.(util|io|lang|net)\.|throws\s+\w+Exception/.test(s)) return 'java';
|
|
35
|
+
|
|
36
|
+
// ── Kotlin ──────────────────────────────────────────────────────────────────
|
|
37
|
+
// Kotlin uses `val` (immutable) unlike Swift/JS which use `let`.
|
|
38
|
+
// `fun`, `data class`, `companion object`, `println()` are Kotlin-specific.
|
|
39
|
+
if (/\bfun\s+\w+\s*\(|\bdata\s+class\s+\w+|\bcompanion\s+object\b|\bval\s+\w+\s*:\s*\w|\bprintln\s*\(/.test(s)) return 'kotlin';
|
|
40
|
+
|
|
41
|
+
// ── Swift ───────────────────────────────────────────────────────────────────
|
|
42
|
+
// Swift uses `let` for constants (Kotlin uses `val`). `guard let`, `protocol`,
|
|
43
|
+
// `extension`, `func ... -> ReturnType`, and `let x: UppercaseType` are signals.
|
|
44
|
+
if (/\bguard\s+(let|var)\b|\bprotocol\s+\w+\s*\{|\bextension\s+\w+|\bfunc\s+\w+[^(]*\([^)]*\)\s*->\s*\w|\blet\s+\w+\s*:\s*[A-Z]\w*|\bSwiftUI\b/.test(s)) return 'swift';
|
|
45
|
+
|
|
46
|
+
// ── Go ──────────────────────────────────────────────────────────────────────
|
|
47
|
+
// `package`, `:=` short assignment, `fmt.Print*`, `chan`, `goroutine`.
|
|
48
|
+
if (/\bpackage\s+\w+\b|\bfmt\.(Print|Println|Sprintf|Errorf|Fprintf)\s*\(|:=\s*\w|\bgoroutine\b|\bchan\s+\w|\bgo\s+func\b/.test(s)) return 'go';
|
|
49
|
+
|
|
50
|
+
// ── JavaScript ──────────────────────────────────────────────────────────────
|
|
51
|
+
// Before HTML to handle JSX (`<div/>` would otherwise trigger HTML).
|
|
52
|
+
// `var\s+\w+\s*=` (not just `var\s+\w`) to avoid matching Swift `var x: Int`.
|
|
53
|
+
if (/\b(const\s+\w|let\s+\w+\s*=|var\s+\w+\s*=|function\s+\w|\=>\s*[{(]|import\s+.*\bfrom\b\s*['"]|require\s*\(|console\.(log|error|warn|info)|document\.\w|window\.\w|async\s+function|\bPromise\b|React\.|useState\s*\(|\.then\s*\()/.test(s)) return 'javascript';
|
|
54
|
+
|
|
55
|
+
// ── HTML ────────────────────────────────────────────────────────────────────
|
|
56
|
+
if (/^<!DOCTYPE html/i.test(s) || /<(html|head|body|div|section|article|nav|p|a|img|ul|ol|li|table|form|input|button|script|style)\b[^>]*>/i.test(s)) return 'html';
|
|
57
|
+
|
|
58
|
+
// ── SCSS ─────────────────────────────────────────────────────────────────────
|
|
59
|
+
// Before CSS — SCSS is a superset. Unique markers: `//` line comments,
|
|
60
|
+
// `&` nesting, `$variable`, `@mixin/@include/@extend`, `#{interpolation}`.
|
|
61
|
+
if (/(^|\n)\s*(\/\/\s+\S|&[:.[\w]|\$\w+\s*:|@(mixin|include|extend|each|if|for|use|forward)\b|#\{)/.test(s) && /[\w#.*&[\]:(),>+~ -]+\s*\{/.test(s)) return 'scss';
|
|
62
|
+
|
|
63
|
+
// ── CSS ──────────────────────────────────────────────────────────────────────
|
|
64
|
+
if (/(^|\n)\s*[\w#.*:[\]&, +-]+\s*\{[^}]*[\w-]+\s*:[^{}:;]+[;}\n]/m.test(s) && !/<\w|function\s|def\s|:\s*(string|number)/.test(s)) return 'css';
|
|
65
|
+
|
|
66
|
+
// ── JSON ────────────────────────────────────────────────────────────────────
|
|
67
|
+
if (/^\s*[{[]/.test(s) && /"\w[\w\s-]*"\s*:/.test(s) && !/\bfunction\b|\bdef\b/.test(s)) return 'json';
|
|
68
|
+
|
|
69
|
+
// ── SQL ─────────────────────────────────────────────────────────────────────
|
|
70
|
+
if (/(^|\n)\s*(SELECT\s|INSERT\s+INTO|UPDATE\s+\w|DELETE\s+FROM|CREATE\s+(TABLE|DATABASE|INDEX|VIEW)|DROP\s+(TABLE|DATABASE)|ALTER\s+TABLE|WITH\s+\w+\s+AS\s*\()/im.test(s)) return 'sql';
|
|
71
|
+
|
|
72
|
+
// ── Python ──────────────────────────────────────────────────────────────────
|
|
73
|
+
// `def` requires trailing `:` (Python rule); `class` checked at line start.
|
|
74
|
+
if (/\bdef\s+\w+\s*\([^)]*\)\s*:|(^|\n)\s*class\s+\w+.*:\s*$|(^|\n)\s*import\s+\w|(^|\n)\s*from\s+\w+\s+import\s+|\bprint\s*\(|if\s+__name__\s*==\s*['"]__main__['"]/m.test(s)) return 'python';
|
|
75
|
+
|
|
76
|
+
// ── Ruby ────────────────────────────────────────────────────────────────────
|
|
77
|
+
// `.each do |x|`, `attr_*`, `puts` with value, multi-line `def...end`.
|
|
78
|
+
if (/\bputs\s+\S|\battr_(accessor|reader|writer)\s|\.each\s+do\s*\|\w+\s*\||\bdo\s*\|\w+\s*\|.*\bend\b|\bdef\s+\w+[^:]*\n[\s\S]*?\bend\b/.test(s)) return 'ruby';
|
|
79
|
+
|
|
80
|
+
// ── Bash / Shell ─────────────────────────────────────────────────────────────
|
|
81
|
+
if (/^#!.*\/(ba|z|da|fi|k)?sh\b/m.test(s) || /\b(echo\s+["']|grep\s+|awk\s+|sed\s+['"\\/-]|chmod\s+|sudo\s+|apt(-get)?\s+install|brew\s+install|npm\s+(install|run|start|build)|pip\s+(install|3\s)|docker\s+(run|build|compose)|kubectl\s+|git\s+(clone|add|commit|push|pull|checkout))\b/.test(s)) return 'bash';
|
|
82
|
+
|
|
83
|
+
// ── C++ ─────────────────────────────────────────────────────────────────────
|
|
84
|
+
// Before C and C# — `cout<<`, `using namespace std`, `std::`, `template<>`.
|
|
85
|
+
if (/\bcout\s*<<|\bcin\s*>>|using\s+namespace\s+std\b|std::\w|\btemplate\s*<\w|\b#include\s*<(iostream|vector|map|set|algorithm|string|memory)>/.test(s)) return 'cpp';
|
|
86
|
+
|
|
87
|
+
// ── C# ──────────────────────────────────────────────────────────────────────
|
|
88
|
+
// After C++ to avoid matching C++'s `using namespace std`.
|
|
89
|
+
if (/\busing\s+System\b|Console\.(Write|WriteLine)\s*\(|\bget;\s*set;|\basync\s+Task[<\s]|IEnumerable<|\bLINQ\b|\.Select\s*\(|\.Where\s*\(/.test(s)) return 'csharp';
|
|
90
|
+
|
|
91
|
+
// ── C ───────────────────────────────────────────────────────────────────────
|
|
92
|
+
if (/\b#include\s*<(stdio|stdlib|string|math|time|ctype)\.h>|\bprintf\s*\(|\bscanf\s*\(|int\s+main\s*\(\s*(void|int\s+argc)|\bmalloc\s*\(|\bfree\s*\(/.test(s) && !/namespace|cout|cin|std::/.test(s)) return 'c';
|
|
93
|
+
|
|
94
|
+
// ── XML ─────────────────────────────────────────────────────────────────────
|
|
95
|
+
if (/^<\?xml\s/i.test(s) || /xmlns:|<\/[\w:]+>/.test(s)) return 'xml';
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
package/src/js/core/dom.js
CHANGED
|
@@ -29,7 +29,7 @@ export const isInline = (node) =>
|
|
|
29
29
|
isElement(node) &&
|
|
30
30
|
/^(a|abbr|acronym|b|bdo|big|br|button|cite|code|dfn|em|i|img|input|kbd|label|map|object|output|q|s|samp|select|small|span|strong|sub|sup|textarea|time|tt|u|var)$/i.test(node.nodeName);
|
|
31
31
|
/** @param {Node} node */
|
|
32
|
-
export const isEditable = (node) => isElement(node) && node.isContentEditable;
|
|
32
|
+
export const isEditable = (node) => isElement(node) && /** @type {HTMLElement} */ (node).isContentEditable;
|
|
33
33
|
/** @param {Node} node */
|
|
34
34
|
export const isAnchor = (node) => isElement(node) && node.nodeName.toUpperCase() === 'A';
|
|
35
35
|
/** @param {Node} node */
|
|
@@ -100,7 +100,7 @@ export function prevElement(node) {
|
|
|
100
100
|
while (sibling && !isElement(sibling)) {
|
|
101
101
|
sibling = sibling.previousSibling;
|
|
102
102
|
}
|
|
103
|
-
return sibling;
|
|
103
|
+
return /** @type {Element|null} */ (sibling);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
@@ -113,7 +113,7 @@ export function nextElement(node) {
|
|
|
113
113
|
while (sibling && !isElement(sibling)) {
|
|
114
114
|
sibling = sibling.nextSibling;
|
|
115
115
|
}
|
|
116
|
-
return sibling;
|
|
116
|
+
return /** @type {Element|null} */ (sibling);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// ---------------------------------------------------------------------------
|
|
@@ -214,7 +214,7 @@ export function isEmpty(node) {
|
|
|
214
214
|
if (isText(node)) return !node.nodeValue;
|
|
215
215
|
if (isVoid(node)) return false;
|
|
216
216
|
if (node.childNodes.length === 1 && node.firstChild?.nodeName === 'BR') return true;
|
|
217
|
-
return !node.textContent.trim() &&
|
|
217
|
+
return !node.textContent.trim() && !/** @type {Element} */ (node).querySelector('img, video, hr, table');
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
/**
|
|
@@ -303,12 +303,12 @@ export function trapFocus(container, onEscape) {
|
|
|
303
303
|
if (e.shiftKey) {
|
|
304
304
|
if (document.activeElement === first) {
|
|
305
305
|
e.preventDefault();
|
|
306
|
-
last.focus();
|
|
306
|
+
/** @type {HTMLElement} */ (last).focus();
|
|
307
307
|
}
|
|
308
308
|
} else {
|
|
309
309
|
if (document.activeElement === last) {
|
|
310
310
|
e.preventDefault();
|
|
311
|
-
first.focus();
|
|
311
|
+
/** @type {HTMLElement} */ (first).focus();
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
};
|
|
@@ -316,3 +316,59 @@ export function trapFocus(container, onEscape) {
|
|
|
316
316
|
document.addEventListener('keydown', handler);
|
|
317
317
|
return () => document.removeEventListener('keydown', handler);
|
|
318
318
|
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Makes a dialog box draggable by its handle element.
|
|
322
|
+
* On first drag the box is pinned to its current viewport coordinates via
|
|
323
|
+
* `position:fixed`, freeing it from the parent flex container's centering.
|
|
324
|
+
* The position is clamped to the visible viewport.
|
|
325
|
+
*
|
|
326
|
+
* @param {HTMLElement} handle Element the user grabs (title bar / header)
|
|
327
|
+
* @param {HTMLElement} box Element that actually moves
|
|
328
|
+
* @returns {Function} Cleanup function (removes the mousedown listener)
|
|
329
|
+
*/
|
|
330
|
+
export function makeDraggable(handle, box) {
|
|
331
|
+
handle.style.cursor = 'grab';
|
|
332
|
+
|
|
333
|
+
const onMousedown = (e) => {
|
|
334
|
+
if (e.button !== 0) return;
|
|
335
|
+
// Don't start drag when clicking on interactive children of the handle
|
|
336
|
+
if (/** @type {Element} */ (e.target).closest('button, input, select, textarea, a')) return;
|
|
337
|
+
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
|
|
340
|
+
// First drag: snapshot position and pin to viewport with position:fixed
|
|
341
|
+
if (!box.dataset.anDragPinned) {
|
|
342
|
+
const r = box.getBoundingClientRect();
|
|
343
|
+
box.style.position = 'fixed';
|
|
344
|
+
box.style.margin = '0';
|
|
345
|
+
box.style.left = `${r.left}px`;
|
|
346
|
+
box.style.top = `${r.top}px`;
|
|
347
|
+
box.dataset.anDragPinned = '1';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const startX = e.clientX - parseFloat(box.style.left);
|
|
351
|
+
const startY = e.clientY - parseFloat(box.style.top);
|
|
352
|
+
|
|
353
|
+
handle.style.cursor = 'grabbing';
|
|
354
|
+
|
|
355
|
+
const onMove = (ev) => {
|
|
356
|
+
const bw = box.offsetWidth;
|
|
357
|
+
const bh = box.offsetHeight;
|
|
358
|
+
box.style.left = `${Math.max(0, Math.min(ev.clientX - startX, window.innerWidth - bw))}px`;
|
|
359
|
+
box.style.top = `${Math.max(0, Math.min(ev.clientY - startY, window.innerHeight - bh))}px`;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const onUp = () => {
|
|
363
|
+
handle.style.cursor = 'grab';
|
|
364
|
+
document.removeEventListener('mousemove', onMove);
|
|
365
|
+
document.removeEventListener('mouseup', onUp);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
document.addEventListener('mousemove', onMove);
|
|
369
|
+
document.addEventListener('mouseup', onUp);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
handle.addEventListener('mousedown', onMousedown);
|
|
373
|
+
return () => handle.removeEventListener('mousedown', onMousedown);
|
|
374
|
+
}
|
package/src/js/core/markdown.js
CHANGED
|
@@ -37,8 +37,9 @@ function _domToMd(node, depth = 0) {
|
|
|
37
37
|
}
|
|
38
38
|
if (node.nodeType !== 1) return '';
|
|
39
39
|
|
|
40
|
-
const
|
|
41
|
-
const
|
|
40
|
+
const el = /** @type {Element} */ (node);
|
|
41
|
+
const tag = el.nodeName.toLowerCase();
|
|
42
|
+
const inner = () => Array.from(el.childNodes).map(n => _domToMd(n, depth)).join('');
|
|
42
43
|
|
|
43
44
|
switch (tag) {
|
|
44
45
|
case 'p':
|
|
@@ -57,18 +58,18 @@ function _domToMd(node, depth = 0) {
|
|
|
57
58
|
case 'del':
|
|
58
59
|
case 's':
|
|
59
60
|
case 'strike': return `~~${inner()}~~`;
|
|
60
|
-
case 'sup': return `^${inner()}
|
|
61
|
-
case 'sub': return `~${inner()}
|
|
61
|
+
case 'sup': return `^${inner()}^`;
|
|
62
|
+
case 'sub': return `~${inner()}~`;
|
|
62
63
|
case 'code': {
|
|
63
64
|
// Inside <pre> we emit raw text; outside we wrap in backticks
|
|
64
|
-
if (
|
|
65
|
+
if (el.closest('pre')) return inner();
|
|
65
66
|
return `\`${inner()}\``;
|
|
66
67
|
}
|
|
67
68
|
case 'pre': {
|
|
68
|
-
const codeEl =
|
|
69
|
+
const codeEl = el.querySelector('code');
|
|
69
70
|
const langMatch = ((codeEl && codeEl.className) || '').match(/language-(\S+)/);
|
|
70
71
|
const lang = langMatch ? langMatch[1] : '';
|
|
71
|
-
const content = (codeEl ||
|
|
72
|
+
const content = (codeEl || el).textContent || '';
|
|
72
73
|
return `\n\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`;
|
|
73
74
|
}
|
|
74
75
|
case 'blockquote': {
|
|
@@ -76,23 +77,23 @@ function _domToMd(node, depth = 0) {
|
|
|
76
77
|
return `\n\n${lines.map((l) => `> ${l}`).join('\n')}\n\n`;
|
|
77
78
|
}
|
|
78
79
|
case 'a': {
|
|
79
|
-
const href =
|
|
80
|
+
const href = el.getAttribute('href') || '';
|
|
80
81
|
return `[${inner()}](${href})`;
|
|
81
82
|
}
|
|
82
83
|
case 'img': {
|
|
83
|
-
const src =
|
|
84
|
-
const alt =
|
|
84
|
+
const src = el.getAttribute('src') || '';
|
|
85
|
+
const alt = el.getAttribute('alt') || '';
|
|
85
86
|
return ``;
|
|
86
87
|
}
|
|
87
88
|
case 'ul': {
|
|
88
|
-
const items = Array.from(
|
|
89
|
+
const items = Array.from(el.querySelectorAll(':scope > li'));
|
|
89
90
|
if (!items.length) return inner();
|
|
90
91
|
const indent = ' '.repeat(depth);
|
|
91
92
|
const lines = items.map((li) => `${indent}- ${_domToMd(li, depth + 1).trim()}`).join('\n');
|
|
92
93
|
return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
|
|
93
94
|
}
|
|
94
95
|
case 'ol': {
|
|
95
|
-
const items = Array.from(
|
|
96
|
+
const items = Array.from(el.querySelectorAll(':scope > li'));
|
|
96
97
|
if (!items.length) return inner();
|
|
97
98
|
const indent = ' '.repeat(depth);
|
|
98
99
|
const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join('\n');
|
|
@@ -101,7 +102,7 @@ function _domToMd(node, depth = 0) {
|
|
|
101
102
|
case 'li': return inner();
|
|
102
103
|
case 'hr': return '\n\n---\n\n';
|
|
103
104
|
case 'table': {
|
|
104
|
-
const rows = Array.from(
|
|
105
|
+
const rows = Array.from(el.querySelectorAll('tr'));
|
|
105
106
|
if (!rows.length) return inner();
|
|
106
107
|
const cellTexts = rows.map((tr) =>
|
|
107
108
|
Array.from(tr.querySelectorAll('th, td')).map((c) => c.textContent.trim().replace(/\|/g, '\\|')),
|
package/src/js/core/range.js
CHANGED
|
@@ -57,7 +57,7 @@ export class WrappedRange {
|
|
|
57
57
|
commonAncestor() {
|
|
58
58
|
const native = this.toNativeRange();
|
|
59
59
|
const ancestor = native.commonAncestorContainer;
|
|
60
|
-
return isElement(ancestor) ? ancestor : ancestor.parentElement;
|
|
60
|
+
return /** @type {Element|null} */ (isElement(ancestor) ? ancestor : ancestor.parentElement);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
@@ -66,7 +66,7 @@ export class WrappedRange {
|
|
|
66
66
|
* @returns {Element|null}
|
|
67
67
|
*/
|
|
68
68
|
blockNode(editable) {
|
|
69
|
-
return closest(this.sc, (n) => isElement(n) && n !== editable, editable);
|
|
69
|
+
return /** @type {Element|null} */ (closest(this.sc, (n) => isElement(n) && n !== editable, editable));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
@@ -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();
|
|
@@ -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
|
}
|
|
@@ -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}__`;
|
package/src/js/editing/Style.js
CHANGED
|
@@ -45,7 +45,7 @@ export function underline() {
|
|
|
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 = container &&
|
|
48
|
+
const uEl = container && /** @type {Element} */ (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>).
|
|
@@ -71,7 +71,7 @@ export function strikethrough() {
|
|
|
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 = sc &&
|
|
74
|
+
const sEl = sc && (/** @type {Element} */ (sc).closest('s') || /** @type {Element} */ (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>
|
|
@@ -116,7 +116,7 @@ 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
122
|
const sel = window.getSelection();
|
|
@@ -226,9 +226,9 @@ export function outdent() {
|
|
|
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 = container &&
|
|
229
|
+
const checkLi = container && /** @type {Element} */ (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,7 +257,7 @@ 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
|
|
@@ -373,7 +373,7 @@ 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
379
|
const computed = window.getComputedStyle(el);
|
|
@@ -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(
|
|
407
|
+
export function toggleInlineCode(_editable) {
|
|
408
408
|
const sel = window.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 = container &&
|
|
413
|
+
const codeEl = container && /** @type {Element} */ (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.
|
|
@@ -480,7 +480,7 @@ export function isInlineCode() {
|
|
|
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 = sc &&
|
|
483
|
+
const code = sc && /** @type {Element} */ (sc).closest('code');
|
|
484
484
|
return !!(code && !code.closest('pre'));
|
|
485
485
|
}
|
|
486
486
|
|
|
@@ -509,18 +509,18 @@ export function toggleChecklist() {
|
|
|
509
509
|
let container = range.commonAncestorContainer;
|
|
510
510
|
if (container.nodeType === 3) container = container.parentElement;
|
|
511
511
|
|
|
512
|
-
const ul = container
|
|
512
|
+
const ul = container && /** @type {Element} */ (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
526
|
p.innerHTML = p.innerHTML.replace(/\u200b/g, '');
|
|
@@ -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.
|
|
@@ -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');
|
|
@@ -677,5 +677,5 @@ export function isInChecklist() {
|
|
|
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 !!(container &&
|
|
680
|
+
return !!(container && /** @type {Element} */ (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,7 +51,6 @@ 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;
|
|
@@ -65,8 +63,8 @@ export function insertTable(cols, rows, opts = {}) {
|
|
|
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.nodeType === 3) anchor = anchor.parentElement;
|
|
66
|
+
let anchor = /** @type {Element|null} */ (range.startContainer);
|
|
67
|
+
if (anchor && 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
|
@@ -44,11 +44,11 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
44
44
|
if (sel && 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();
|
|
@@ -241,7 +241,7 @@ 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
|
|
@@ -259,7 +259,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
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 && el.closest
|
|
262
|
+
const videoWrapper = el && el.closest('.an-video-wrapper');
|
|
263
263
|
if (videoWrapper) {
|
|
264
264
|
event.preventDefault();
|
|
265
265
|
const p = document.createElement('p');
|
|
@@ -275,7 +275,7 @@ export function handleKeydown(event, editable, options = {}) {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// Checklist — Enter creates new item; empty item exits the list
|
|
278
|
-
const checkLi = el && el.closest
|
|
278
|
+
const checkLi = el && el.closest('.an-checklist li');
|
|
279
279
|
if (checkLi) {
|
|
280
280
|
event.preventDefault();
|
|
281
281
|
const ul = checkLi.closest('.an-checklist');
|
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)',
|