autumnnote 2.0.0 → 2.2.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 +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +661 -798
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +13 -4
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -25
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -231
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -312
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1521
- package/src/js/module/Toolbar.js +0 -750
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- package/src/styles/autumnnote.scss +0 -2866
|
@@ -1,98 +0,0 @@
|
|
|
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?.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
DELETED
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dom.js - DOM manipulation utilities
|
|
3
|
-
* Inspired by Summernote's dom.js — rewritten for vanilla JS without jQuery
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Node type helpers
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
export const ELEMENT_NODE = 1;
|
|
11
|
-
export const TEXT_NODE = 3;
|
|
12
|
-
|
|
13
|
-
/** @param {Node} node */
|
|
14
|
-
export const isElement = (node) => node?.nodeType === ELEMENT_NODE;
|
|
15
|
-
/** @param {Node} node */
|
|
16
|
-
export const isText = (node) => node?.nodeType === TEXT_NODE;
|
|
17
|
-
/** @param {Node} node */
|
|
18
|
-
export const isVoid = (node) => isElement(node) && /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(node.nodeName);
|
|
19
|
-
/** @param {Node} node */
|
|
20
|
-
export const isPara = (node) => isElement(node) && /^(p|div|li|h[1-6]|blockquote|td|th|pre)$/i.test(node.nodeName);
|
|
21
|
-
/** @param {Node} node */
|
|
22
|
-
export const isLi = (node) => isElement(node) && /^(li)$/i.test(node.nodeName);
|
|
23
|
-
/** @param {Node} node */
|
|
24
|
-
export const isList = (node) => isElement(node) && /^(ul|ol)$/i.test(node.nodeName);
|
|
25
|
-
/** @param {Node} node */
|
|
26
|
-
export const isTable = (node) => isElement(node) && node.nodeName.toUpperCase() === 'TABLE';
|
|
27
|
-
/** @param {Node} node */
|
|
28
|
-
export const isInline = (node) =>
|
|
29
|
-
isElement(node) &&
|
|
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
|
-
/** @param {Node} node */
|
|
32
|
-
export const isEditable = (node) => isElement(node) && /** @type {HTMLElement} */ (node).isContentEditable;
|
|
33
|
-
/** @param {Node} node */
|
|
34
|
-
export const isAnchor = (node) => isElement(node) && node.nodeName.toUpperCase() === 'A';
|
|
35
|
-
/** @param {Node} node */
|
|
36
|
-
export const isImage = (node) => isElement(node) && node.nodeName.toUpperCase() === 'IMG';
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
// Tree traversal
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Walk up the DOM tree from node, returning the first element matching predicate (inclusive).
|
|
44
|
-
* @param {Node} node
|
|
45
|
-
* @param {(node: Node) => boolean} predicate
|
|
46
|
-
* @param {Node} [stopAt] - stop traversal at this ancestor (exclusive)
|
|
47
|
-
* @returns {Node|null}
|
|
48
|
-
*/
|
|
49
|
-
export function closest(node, predicate, stopAt) {
|
|
50
|
-
let cur = node;
|
|
51
|
-
while (cur && cur !== stopAt) {
|
|
52
|
-
if (predicate(cur)) return cur;
|
|
53
|
-
cur = cur.parentNode;
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Returns the nearest ancestor that is a paragraph-like block.
|
|
60
|
-
* @param {Node} node
|
|
61
|
-
* @param {Node} [editable]
|
|
62
|
-
* @returns {Node|null}
|
|
63
|
-
*/
|
|
64
|
-
export function closestPara(node, editable) {
|
|
65
|
-
return closest(node, isPara, editable);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Returns all ancestors of node up to (but not including) stopAt.
|
|
70
|
-
* @param {Node} node
|
|
71
|
-
* @param {Node} [stopAt]
|
|
72
|
-
* @returns {Node[]}
|
|
73
|
-
*/
|
|
74
|
-
export function ancestors(node, stopAt) {
|
|
75
|
-
const result = [];
|
|
76
|
-
let cur = node.parentNode;
|
|
77
|
-
while (cur && cur !== stopAt) {
|
|
78
|
-
result.push(cur);
|
|
79
|
-
cur = cur.parentNode;
|
|
80
|
-
}
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Returns all children of node as an Array.
|
|
86
|
-
* @param {Node} node
|
|
87
|
-
* @returns {Node[]}
|
|
88
|
-
*/
|
|
89
|
-
export function children(node) {
|
|
90
|
-
return Array.from(node.childNodes);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Returns the previous sibling element (skipping text/comment nodes).
|
|
95
|
-
* @param {Node} node
|
|
96
|
-
* @returns {Element|null}
|
|
97
|
-
*/
|
|
98
|
-
export function prevElement(node) {
|
|
99
|
-
let sibling = node.previousSibling;
|
|
100
|
-
while (sibling && !isElement(sibling)) {
|
|
101
|
-
sibling = sibling.previousSibling;
|
|
102
|
-
}
|
|
103
|
-
return /** @type {Element|null} */ (sibling);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Returns the next sibling element.
|
|
108
|
-
* @param {Node} node
|
|
109
|
-
* @returns {Element|null}
|
|
110
|
-
*/
|
|
111
|
-
export function nextElement(node) {
|
|
112
|
-
let sibling = node.nextSibling;
|
|
113
|
-
while (sibling && !isElement(sibling)) {
|
|
114
|
-
sibling = sibling.nextSibling;
|
|
115
|
-
}
|
|
116
|
-
return /** @type {Element|null} */ (sibling);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
// DOM mutation helpers
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Creates an element with optional attributes and children.
|
|
125
|
-
* @param {string} tag
|
|
126
|
-
* @param {Record<string, string>} [attrs]
|
|
127
|
-
* @param {(Node|string)[]} [childNodes]
|
|
128
|
-
* @returns {HTMLElement}
|
|
129
|
-
*/
|
|
130
|
-
export function createElement(tag, attrs = {}, childNodes = []) {
|
|
131
|
-
const el = document.createElement(tag);
|
|
132
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
133
|
-
el.setAttribute(k, v);
|
|
134
|
-
}
|
|
135
|
-
for (const child of childNodes) {
|
|
136
|
-
if (typeof child === 'string') {
|
|
137
|
-
el.appendChild(document.createTextNode(child));
|
|
138
|
-
} else {
|
|
139
|
-
el.appendChild(child);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return el;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Removes a node from its parent.
|
|
147
|
-
* @param {Node} node
|
|
148
|
-
*/
|
|
149
|
-
export function remove(node) {
|
|
150
|
-
if (node?.parentNode) {
|
|
151
|
-
/** @type {ChildNode} */ (node).remove();
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Unwraps a node — replaces the node with its children.
|
|
157
|
-
* @param {Node} node
|
|
158
|
-
*/
|
|
159
|
-
export function unwrap(node) {
|
|
160
|
-
const parent = node.parentNode;
|
|
161
|
-
if (!parent) return;
|
|
162
|
-
while (node.firstChild) {
|
|
163
|
-
parent.insertBefore(node.firstChild, node);
|
|
164
|
-
}
|
|
165
|
-
/** @type {ChildNode} */ (node).remove();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Wraps a node with a wrapper element.
|
|
170
|
-
* @param {Node} node
|
|
171
|
-
* @param {HTMLElement} wrapper
|
|
172
|
-
* @returns {HTMLElement} the wrapper
|
|
173
|
-
*/
|
|
174
|
-
export function wrap(node, wrapper) {
|
|
175
|
-
node.parentNode.insertBefore(wrapper, node);
|
|
176
|
-
wrapper.appendChild(node);
|
|
177
|
-
return wrapper;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Insert node after reference node.
|
|
182
|
-
* @param {Node} newNode
|
|
183
|
-
* @param {Node} refNode
|
|
184
|
-
*/
|
|
185
|
-
export function insertAfter(newNode, refNode) {
|
|
186
|
-
if (refNode.nextSibling) {
|
|
187
|
-
refNode.parentNode.insertBefore(newNode, refNode.nextSibling);
|
|
188
|
-
} else {
|
|
189
|
-
refNode.parentNode.appendChild(newNode);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
// Content helpers
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Returns the text content of a node (safe).
|
|
199
|
-
* @param {Node} node
|
|
200
|
-
* @returns {string}
|
|
201
|
-
*/
|
|
202
|
-
export function nodeValue(node) {
|
|
203
|
-
return isText(node) ? node.nodeValue : node.textContent || '';
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Determine whether a DOM node contains no visible content.
|
|
208
|
-
*
|
|
209
|
-
* Text nodes are considered empty when their `nodeValue` is empty. Void elements (e.g., `img`, `br`, `input`) are considered non-empty. An element with exactly one `<br>` child is treated as empty. For other elements, emptiness means trimmed `textContent` is empty and there are no descendant `img`, `video`, `hr`, or `table` elements.
|
|
210
|
-
* @param {Node} node - Node to inspect for visible content.
|
|
211
|
-
* @returns {boolean} `true` if the node has no visible content, `false` otherwise.
|
|
212
|
-
*/
|
|
213
|
-
export function isEmpty(node) {
|
|
214
|
-
if (isText(node)) return !node.nodeValue;
|
|
215
|
-
if (isVoid(node)) return false;
|
|
216
|
-
if (node.childNodes.length === 1 && node.firstChild?.nodeName === 'BR') return true;
|
|
217
|
-
return !node.textContent.trim() && !/** @type {Element} */ (node).querySelector('img, video, hr, table');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Returns the outerHTML of an element.
|
|
222
|
-
* @param {Element} el
|
|
223
|
-
* @returns {string}
|
|
224
|
-
*/
|
|
225
|
-
export function outerHtml(el) {
|
|
226
|
-
return el.outerHTML;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
// Selection / editing helpers
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Places the caret at the end of a contenteditable element.
|
|
235
|
-
* @param {HTMLElement} el
|
|
236
|
-
*/
|
|
237
|
-
export function placeCaret(el) {
|
|
238
|
-
const range = document.createRange();
|
|
239
|
-
range.selectNodeContents(el);
|
|
240
|
-
range.collapse(false);
|
|
241
|
-
const sel = globalThis.getSelection();
|
|
242
|
-
if (sel) {
|
|
243
|
-
sel.removeAllRanges();
|
|
244
|
-
sel.addRange(range);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Returns true if the node is inside a contenteditable root.
|
|
250
|
-
* @param {Node} node
|
|
251
|
-
* @returns {boolean}
|
|
252
|
-
*/
|
|
253
|
-
export function isInsideEditable(node) {
|
|
254
|
-
return !!closest(node, isEditable);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// Event helpers
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Adds an event listener and returns a disposer function.
|
|
263
|
-
* @param {EventTarget} target
|
|
264
|
-
* @param {string} type
|
|
265
|
-
* @param {EventListener} handler
|
|
266
|
-
* @param {AddEventListenerOptions} [options]
|
|
267
|
-
* @returns {() => void} disposer
|
|
268
|
-
*/
|
|
269
|
-
export function on(target, type, handler, options) {
|
|
270
|
-
target.addEventListener(type, handler, options);
|
|
271
|
-
return () => target.removeEventListener(type, handler, options);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Installs a keyboard focus trap inside a dialog container.
|
|
276
|
-
* - Tab / Shift+Tab cycles focus within the container's focusable children.
|
|
277
|
-
* - Escape calls `onEscape` and removes the trap listener.
|
|
278
|
-
*
|
|
279
|
-
* Returns a disposer function that removes the listener (call on dialog close).
|
|
280
|
-
*
|
|
281
|
-
* @param {HTMLElement} container - the dialog element to trap focus inside
|
|
282
|
-
* @param {() => void} onEscape - called when Escape is pressed
|
|
283
|
-
* @returns {() => void} disposer
|
|
284
|
-
*/
|
|
285
|
-
export function trapFocus(container, onEscape) {
|
|
286
|
-
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
287
|
-
|
|
288
|
-
const getFocusable = () => Array.from(container.querySelectorAll(FOCUSABLE)).filter(
|
|
289
|
-
(el) => !el.closest('[style*="display: none"]') && !el.closest('[style*="display:none"]'),
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
const handler = (e) => {
|
|
293
|
-
if (e.key === 'Escape') {
|
|
294
|
-
e.stopPropagation();
|
|
295
|
-
onEscape?.();
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
if (e.key !== 'Tab') return;
|
|
299
|
-
const els = getFocusable();
|
|
300
|
-
if (!els.length) return;
|
|
301
|
-
const first = els[0];
|
|
302
|
-
const last = els.at(-1);
|
|
303
|
-
if (e.shiftKey) {
|
|
304
|
-
if (document.activeElement === first) {
|
|
305
|
-
e.preventDefault();
|
|
306
|
-
/** @type {HTMLElement} */ (last).focus();
|
|
307
|
-
}
|
|
308
|
-
} else if (document.activeElement === last) {
|
|
309
|
-
e.preventDefault();
|
|
310
|
-
/** @type {HTMLElement} */ (first).focus();
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
document.addEventListener('keydown', handler);
|
|
315
|
-
return () => document.removeEventListener('keydown', handler);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Makes a dialog box draggable by its handle element.
|
|
320
|
-
* On first drag the box is pinned to its current viewport coordinates via
|
|
321
|
-
* `position:fixed`, freeing it from the parent flex container's centering.
|
|
322
|
-
* The position is clamped to the visible viewport.
|
|
323
|
-
*
|
|
324
|
-
* @param {HTMLElement} handle Element the user grabs (title bar / header)
|
|
325
|
-
* @param {HTMLElement} box Element that actually moves
|
|
326
|
-
* @returns {Function} Cleanup function (removes the mousedown listener)
|
|
327
|
-
*/
|
|
328
|
-
export function makeDraggable(handle, box) {
|
|
329
|
-
handle.style.cursor = 'grab';
|
|
330
|
-
|
|
331
|
-
const onMousedown = (e) => {
|
|
332
|
-
if (e.button !== 0) return;
|
|
333
|
-
// Don't start drag when clicking on interactive children of the handle
|
|
334
|
-
if (/** @type {Element} */ (e.target).closest('button, input, select, textarea, a')) return;
|
|
335
|
-
|
|
336
|
-
e.preventDefault();
|
|
337
|
-
|
|
338
|
-
// First drag: snapshot position and pin to viewport with position:fixed
|
|
339
|
-
if (!box.dataset.anDragPinned) {
|
|
340
|
-
const r = box.getBoundingClientRect();
|
|
341
|
-
box.style.position = 'fixed';
|
|
342
|
-
box.style.margin = '0';
|
|
343
|
-
box.style.left = `${r.left}px`;
|
|
344
|
-
box.style.top = `${r.top}px`;
|
|
345
|
-
box.dataset.anDragPinned = '1';
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const startX = e.clientX - Number.parseFloat(box.style.left);
|
|
349
|
-
const startY = e.clientY - Number.parseFloat(box.style.top);
|
|
350
|
-
|
|
351
|
-
handle.style.cursor = 'grabbing';
|
|
352
|
-
|
|
353
|
-
const onMove = (ev) => {
|
|
354
|
-
const bw = box.offsetWidth;
|
|
355
|
-
const bh = box.offsetHeight;
|
|
356
|
-
box.style.left = `${Math.max(0, Math.min(ev.clientX - startX, globalThis.innerWidth - bw))}px`;
|
|
357
|
-
box.style.top = `${Math.max(0, Math.min(ev.clientY - startY, globalThis.innerHeight - bh))}px`;
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const onUp = () => {
|
|
361
|
-
handle.style.cursor = 'grab';
|
|
362
|
-
document.removeEventListener('mousemove', onMove);
|
|
363
|
-
document.removeEventListener('mouseup', onUp);
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
document.addEventListener('mousemove', onMove);
|
|
367
|
-
document.addEventListener('mouseup', onUp);
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
handle.addEventListener('mousedown', onMousedown);
|
|
371
|
-
return () => handle.removeEventListener('mousedown', onMousedown);
|
|
372
|
-
}
|
package/src/js/core/env.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* env.js - Environment / browser detection
|
|
3
|
-
* Inspired by Summernote's env.js
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const userAgent = navigator.userAgent;
|
|
7
|
-
|
|
8
|
-
export const env = {
|
|
9
|
-
/** True if browser is Chrome */
|
|
10
|
-
isChrome: /Chrome\//.test(userAgent),
|
|
11
|
-
/** True if browser is Firefox */
|
|
12
|
-
isFF: /Firefox\//.test(userAgent),
|
|
13
|
-
/** True if browser is Safari (not Chrome) */
|
|
14
|
-
isSafari: /^((?!chrome|android).)*safari/i.test(userAgent),
|
|
15
|
-
/** True if browser is Edge (Chromium) */
|
|
16
|
-
isEdge: /Edg\//.test(userAgent),
|
|
17
|
-
/** True if running on macOS */
|
|
18
|
-
isMac: /Macintosh/.test(userAgent),
|
|
19
|
-
/** True if running on mobile */
|
|
20
|
-
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent),
|
|
21
|
-
/** True if touch is supported */
|
|
22
|
-
isTouch: 'ontouchstart' in globalThis || navigator.maxTouchPoints > 0,
|
|
23
|
-
/** Modifier key name depending on platform */
|
|
24
|
-
modifierKey: /Macintosh/.test(userAgent) ? 'metaKey' : 'ctrlKey',
|
|
25
|
-
};
|
package/src/js/core/key.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* key.js - Keyboard key code constants
|
|
3
|
-
* Inspired by Summernote's key.js
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const key = {
|
|
7
|
-
BACKSPACE: 'Backspace',
|
|
8
|
-
TAB: 'Tab',
|
|
9
|
-
ENTER: 'Enter',
|
|
10
|
-
ESCAPE: 'Escape',
|
|
11
|
-
SPACE: ' ',
|
|
12
|
-
PAGE_UP: 'PageUp',
|
|
13
|
-
PAGE_DOWN: 'PageDown',
|
|
14
|
-
END: 'End',
|
|
15
|
-
HOME: 'Home',
|
|
16
|
-
LEFT: 'ArrowLeft',
|
|
17
|
-
UP: 'ArrowUp',
|
|
18
|
-
RIGHT: 'ArrowRight',
|
|
19
|
-
DOWN: 'ArrowDown',
|
|
20
|
-
DELETE: 'Delete',
|
|
21
|
-
// Numbers
|
|
22
|
-
NUM0: '0',
|
|
23
|
-
NUM1: '1',
|
|
24
|
-
NUM2: '2',
|
|
25
|
-
NUM3: '3',
|
|
26
|
-
NUM4: '4',
|
|
27
|
-
NUM5: '5',
|
|
28
|
-
NUM6: '6',
|
|
29
|
-
NUM7: '7',
|
|
30
|
-
NUM8: '8',
|
|
31
|
-
// Letters
|
|
32
|
-
B: 'b',
|
|
33
|
-
E: 'e',
|
|
34
|
-
I: 'i',
|
|
35
|
-
J: 'j',
|
|
36
|
-
K: 'k',
|
|
37
|
-
L: 'l',
|
|
38
|
-
R: 'r',
|
|
39
|
-
S: 's',
|
|
40
|
-
U: 'u',
|
|
41
|
-
V: 'v',
|
|
42
|
-
Y: 'y',
|
|
43
|
-
Z: 'z',
|
|
44
|
-
SLASH: '/',
|
|
45
|
-
PERIOD: '.',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Returns true if the event matches the given key
|
|
50
|
-
* @param {KeyboardEvent} event
|
|
51
|
-
* @param {string} keyName - one of key.*
|
|
52
|
-
* @returns {boolean}
|
|
53
|
-
*/
|
|
54
|
-
export function isKey(event, keyName) {
|
|
55
|
-
return event.key === keyName || event.key === keyName.toUpperCase();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Returns true if the event is a modifier key press (Ctrl/Cmd + key)
|
|
60
|
-
* @param {KeyboardEvent} event
|
|
61
|
-
* @param {string} keyName
|
|
62
|
-
* @returns {boolean}
|
|
63
|
-
*/
|
|
64
|
-
export function isModifier(event, keyName) {
|
|
65
|
-
return (event.ctrlKey || event.metaKey) && isKey(event, keyName);
|
|
66
|
-
}
|
package/src/js/core/lists.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lists.js - Array/list utility helpers
|
|
3
|
-
* Inspired by Summernote's lists.js
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Returns the last element of an array.
|
|
8
|
-
* @template T
|
|
9
|
-
* @param {T[]} arr
|
|
10
|
-
* @returns {T|undefined}
|
|
11
|
-
*/
|
|
12
|
-
export function last(arr) {
|
|
13
|
-
return arr[arr.length - 1];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Returns the first element of an array.
|
|
18
|
-
* @template T
|
|
19
|
-
* @param {T[]} arr
|
|
20
|
-
* @returns {T|undefined}
|
|
21
|
-
*/
|
|
22
|
-
export function first(arr) {
|
|
23
|
-
return arr[0];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Returns a new array without the last n items.
|
|
28
|
-
* @template T
|
|
29
|
-
* @param {T[]} arr
|
|
30
|
-
* @param {number} [n=1]
|
|
31
|
-
* @returns {T[]}
|
|
32
|
-
*/
|
|
33
|
-
export function initial(arr, n = 1) {
|
|
34
|
-
return arr.slice(0, arr.length - n);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Returns a new array without the first n items.
|
|
39
|
-
* @template T
|
|
40
|
-
* @param {T[]} arr
|
|
41
|
-
* @param {number} [n=1]
|
|
42
|
-
* @returns {T[]}
|
|
43
|
-
*/
|
|
44
|
-
export function tail(arr, n = 1) {
|
|
45
|
-
return arr.slice(n);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Returns a flattened (one level) array.
|
|
50
|
-
* @template T
|
|
51
|
-
* @param {T[][]} arr
|
|
52
|
-
* @returns {T[]}
|
|
53
|
-
*/
|
|
54
|
-
export function flatten(arr) {
|
|
55
|
-
return arr.flat();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Returns unique elements of an array (using Set).
|
|
60
|
-
* @template T
|
|
61
|
-
* @param {T[]} arr
|
|
62
|
-
* @returns {T[]}
|
|
63
|
-
*/
|
|
64
|
-
export function unique(arr) {
|
|
65
|
-
return [...new Set(arr)];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Splits an array into chunks of size n.
|
|
70
|
-
* @template T
|
|
71
|
-
* @param {T[]} arr
|
|
72
|
-
* @param {number} n
|
|
73
|
-
* @returns {T[][]}
|
|
74
|
-
*/
|
|
75
|
-
export function chunk(arr, n) {
|
|
76
|
-
const result = [];
|
|
77
|
-
for (let i = 0; i < arr.length; i += n) {
|
|
78
|
-
result.push(arr.slice(i, i + n));
|
|
79
|
-
}
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Groups array elements by a key function.
|
|
85
|
-
* @template T
|
|
86
|
-
* @param {T[]} arr
|
|
87
|
-
* @param {(item: T) => string} keyFn
|
|
88
|
-
* @returns {Record<string, T[]>}
|
|
89
|
-
*/
|
|
90
|
-
export function groupBy(arr, keyFn) {
|
|
91
|
-
return arr.reduce((groups, item) => {
|
|
92
|
-
const key = keyFn(item);
|
|
93
|
-
if (!groups[key]) {
|
|
94
|
-
groups[key] = [];
|
|
95
|
-
}
|
|
96
|
-
groups[key].push(item);
|
|
97
|
-
return groups;
|
|
98
|
-
}, {});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Returns true if all elements satisfy the predicate.
|
|
103
|
-
* @template T
|
|
104
|
-
* @param {T[]} arr
|
|
105
|
-
* @param {(item: T) => boolean} predicate
|
|
106
|
-
* @returns {boolean}
|
|
107
|
-
*/
|
|
108
|
-
export function all(arr, predicate) {
|
|
109
|
-
return arr.every(predicate);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Returns true if any element satisfies the predicate.
|
|
114
|
-
* @template T
|
|
115
|
-
* @param {T[]} arr
|
|
116
|
-
* @param {(item: T) => boolean} predicate
|
|
117
|
-
* @returns {boolean}
|
|
118
|
-
*/
|
|
119
|
-
export function any(arr, predicate) {
|
|
120
|
-
return arr.some(predicate);
|
|
121
|
-
}
|