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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autumnnote",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.6.1",
|
|
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
|
@@ -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) {
|
|
@@ -477,21 +477,18 @@ export class Context {
|
|
|
477
477
|
a.style.display = 'none';
|
|
478
478
|
document.body.appendChild(a);
|
|
479
479
|
a.click();
|
|
480
|
-
|
|
480
|
+
a.remove();
|
|
481
481
|
URL.revokeObjectURL(url);
|
|
482
482
|
}
|
|
483
483
|
|
|
484
484
|
/**
|
|
485
|
-
* Opens the editor content in a new
|
|
485
|
+
* Opens the editor content in a new globalThis and triggers the browser print dialog.
|
|
486
486
|
* @param {string} [title='']
|
|
487
487
|
*/
|
|
488
488
|
print(title = '') {
|
|
489
489
|
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">' +
|
|
490
|
+
const safeTitle = (title || '').replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
491
|
+
const markup = '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">' +
|
|
495
492
|
`<title>${safeTitle}</title>` +
|
|
496
493
|
'<style>' +
|
|
497
494
|
'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 +499,15 @@ export class Context {
|
|
|
502
499
|
'pre{background:#f3f4f6;padding:.75em 1em;border-radius:4px;overflow-x:auto;}' +
|
|
503
500
|
'table{border-collapse:collapse;}td,th{border:1px solid #d1d5db;padding:4px 8px;}' +
|
|
504
501
|
'</style>' +
|
|
505
|
-
`</head><body>${content}</body></html
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
w
|
|
502
|
+
`</head><body>${content}</body></html>`;
|
|
503
|
+
const blob = new Blob([markup], { type: 'text/html' });
|
|
504
|
+
const url = URL.createObjectURL(blob);
|
|
505
|
+
const w = globalThis.open(url, '_blank');
|
|
506
|
+
if (!w) { URL.revokeObjectURL(url); return; } // popup blocked by browser
|
|
507
|
+
w.addEventListener('load', () => {
|
|
508
|
+
w.print();
|
|
509
|
+
URL.revokeObjectURL(url);
|
|
510
|
+
});
|
|
509
511
|
}
|
|
510
512
|
|
|
511
513
|
/**
|
|
@@ -555,10 +557,15 @@ export class Context {
|
|
|
555
557
|
this._disposers = [];
|
|
556
558
|
|
|
557
559
|
const container = this.layoutInfo.container;
|
|
558
|
-
|
|
560
|
+
const wasDark = container?.classList.contains('an-theme-dark');
|
|
561
|
+
if (container?.parentNode) {
|
|
559
562
|
// Restore original element
|
|
560
563
|
this.targetEl.style.display = '';
|
|
561
|
-
container.
|
|
564
|
+
container.remove();
|
|
565
|
+
}
|
|
566
|
+
// If this was a dark editor and no other dark containers remain, clean up body
|
|
567
|
+
if (wasDark && !document.querySelector('.an-container.an-theme-dark')) {
|
|
568
|
+
document.body.classList.remove('an-theme-dark');
|
|
562
569
|
}
|
|
563
570
|
|
|
564
571
|
if (typeof this.options.onDestroy === 'function') {
|
|
@@ -578,7 +585,7 @@ export class Context {
|
|
|
578
585
|
*/
|
|
579
586
|
_syncToTarget() {
|
|
580
587
|
if (this.targetEl.tagName === 'TEXTAREA' || this.targetEl.tagName === 'INPUT') {
|
|
581
|
-
this.targetEl.value = this.getHTML();
|
|
588
|
+
/** @type {HTMLInputElement} */ (this.targetEl).value = this.getHTML();
|
|
582
589
|
}
|
|
583
590
|
}
|
|
584
591
|
}
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
/**
|
|
@@ -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
|
/**
|
|
@@ -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,23 +292,23 @@ 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();
|
|
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 - Number.parseFloat(box.style.left);
|
|
351
|
+
const startY = e.clientY - Number.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, globalThis.innerWidth - bw))}px`;
|
|
359
|
+
box.style.top = `${Math.max(0, Math.min(ev.clientY - startY, globalThis.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/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
|
@@ -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 langMatch = ((codeEl && codeEl.className) || '')
|
|
69
|
+
const codeEl = el.querySelector('code');
|
|
70
|
+
const langMatch = /language-(\S+)/.exec((codeEl && codeEl.className) || '');
|
|
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,16 +102,16 @@ 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
|
-
Array.from(tr.querySelectorAll('th, td')).map((c) => c.textContent.trim().
|
|
108
|
+
Array.from(tr.querySelectorAll('th, td')).map((c) => c.textContent.trim().replaceAll('|', '\\|')),
|
|
108
109
|
);
|
|
109
110
|
const cols = Math.max(...cellTexts.map((r) => r.length));
|
|
110
111
|
const padRow = (row) => { const r = [...row]; while (r.length < cols) r.push(''); return r; };
|
|
111
112
|
let md = '\n\n';
|
|
112
113
|
md += `| ${padRow(cellTexts[0]).join(' | ')} |\n`;
|
|
113
|
-
md += `| ${Array(cols).fill('---').join(' | ')} |\n`;
|
|
114
|
+
md += `| ${new Array(cols).fill('---').join(' | ')} |\n`;
|
|
114
115
|
for (let r = 1; r < cellTexts.length; r++) {
|
|
115
116
|
md += `| ${padRow(cellTexts[r]).join(' | ')} |\n`;
|
|
116
117
|
}
|
|
@@ -138,7 +139,7 @@ export function isMarkdown(text) {
|
|
|
138
139
|
* @returns {string}
|
|
139
140
|
*/
|
|
140
141
|
export function markdownToHTML(text) {
|
|
141
|
-
const lines = text.
|
|
142
|
+
const lines = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n');
|
|
142
143
|
const out = [];
|
|
143
144
|
let i = 0;
|
|
144
145
|
|
|
@@ -146,7 +147,7 @@ export function markdownToHTML(text) {
|
|
|
146
147
|
const line = lines[i];
|
|
147
148
|
|
|
148
149
|
// ---- Fenced code block ``` lang ... ``` -----------------------------------
|
|
149
|
-
const fenceMatch =
|
|
150
|
+
const fenceMatch = /^```(\S*)$/.exec(line);
|
|
150
151
|
if (fenceMatch) {
|
|
151
152
|
const lang = fenceMatch[1];
|
|
152
153
|
const codeLines = [];
|
|
@@ -169,7 +170,7 @@ export function markdownToHTML(text) {
|
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
// ---- ATX Headings # – ###### -------------------------------------------
|
|
172
|
-
const hMatch =
|
|
173
|
+
const hMatch = /^(#{1,6})\s+(.+)$/.exec(line);
|
|
173
174
|
if (hMatch) {
|
|
174
175
|
const level = hMatch[1].length;
|
|
175
176
|
out.push(`<h${level}>${_inline(hMatch[2])}</h${level}>`);
|
|
@@ -227,10 +228,10 @@ export function markdownToHTML(text) {
|
|
|
227
228
|
bodyRows.push(_parseTableRow(lines[i]));
|
|
228
229
|
i++;
|
|
229
230
|
}
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
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>` : '';
|
|
234
235
|
out.push(`<table>${thead}${tbody}</table>`);
|
|
235
236
|
continue;
|
|
236
237
|
}
|
|
@@ -297,16 +298,16 @@ function _inline(text) {
|
|
|
297
298
|
|
|
298
299
|
function _esc(v) {
|
|
299
300
|
return String(v)
|
|
300
|
-
.
|
|
301
|
-
.
|
|
302
|
-
.
|
|
301
|
+
.replaceAll('&', '&')
|
|
302
|
+
.replaceAll('<', '<')
|
|
303
|
+
.replaceAll('>', '>');
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
function _escAttr(v) {
|
|
306
307
|
return String(v)
|
|
307
|
-
.
|
|
308
|
-
.
|
|
309
|
-
.
|
|
310
|
-
.
|
|
311
|
-
.
|
|
308
|
+
.replaceAll('&', '&')
|
|
309
|
+
.replaceAll('"', '"')
|
|
310
|
+
.replaceAll("'", ''')
|
|
311
|
+
.replaceAll('<', '<')
|
|
312
|
+
.replaceAll('>', '>');
|
|
312
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());
|
|
@@ -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
|
/**
|
|
@@ -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;
|