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.
Files changed (64) hide show
  1. package/README.md +72 -5
  2. package/dist/autumnnote.cjs +20 -20
  3. package/dist/autumnnote.css +1 -1
  4. package/dist/autumnnote.es.js +661 -798
  5. package/dist/autumnnote.es.js.map +1 -1
  6. package/dist/autumnnote.min.js +20 -20
  7. package/dist/autumnnote.umd.js +20 -20
  8. package/dist/autumnnote.umd.js.map +1 -1
  9. package/dist/icon-data-V0Xqv-wX.js +255 -0
  10. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  11. package/package.json +13 -4
  12. package/types/index.d.ts +8 -0
  13. package/src/js/Context.js +0 -854
  14. package/src/js/core/detectLang.js +0 -98
  15. package/src/js/core/dom.js +0 -372
  16. package/src/js/core/env.js +0 -25
  17. package/src/js/core/key.js +0 -66
  18. package/src/js/core/lists.js +0 -121
  19. package/src/js/core/markdown.js +0 -695
  20. package/src/js/core/range.js +0 -194
  21. package/src/js/core/sanitise.js +0 -231
  22. package/src/js/editing/History.js +0 -266
  23. package/src/js/editing/Style.js +0 -812
  24. package/src/js/editing/Table.js +0 -105
  25. package/src/js/editing/Typing.js +0 -397
  26. package/src/js/index.js +0 -193
  27. package/src/js/index.umd.js +0 -17
  28. package/src/js/module/AutoSaveRestore.js +0 -125
  29. package/src/js/module/BaseDialog.js +0 -133
  30. package/src/js/module/BaseMediaTooltip.js +0 -142
  31. package/src/js/module/BaseResizer.js +0 -312
  32. package/src/js/module/BubbleToolbar.js +0 -483
  33. package/src/js/module/Buttons.js +0 -399
  34. package/src/js/module/Clipboard.js +0 -579
  35. package/src/js/module/CodeTooltip.js +0 -493
  36. package/src/js/module/Codeview.js +0 -125
  37. package/src/js/module/ContextMenu.js +0 -621
  38. package/src/js/module/Editor.js +0 -747
  39. package/src/js/module/EmojiDialog.js +0 -254
  40. package/src/js/module/FindReplace.js +0 -512
  41. package/src/js/module/Fullscreen.js +0 -80
  42. package/src/js/module/IconDialog.js +0 -618
  43. package/src/js/module/ImageCropOverlay.js +0 -586
  44. package/src/js/module/ImageDialog.js +0 -193
  45. package/src/js/module/ImageResizer.js +0 -42
  46. package/src/js/module/ImageTooltip.js +0 -285
  47. package/src/js/module/LinkDialog.js +0 -145
  48. package/src/js/module/LinkTooltip.js +0 -250
  49. package/src/js/module/MarkdownShortcuts.js +0 -250
  50. package/src/js/module/Mention.js +0 -365
  51. package/src/js/module/Placeholder.js +0 -51
  52. package/src/js/module/ShortcutsDialog.js +0 -111
  53. package/src/js/module/SlashMenu.js +0 -376
  54. package/src/js/module/Statusbar.js +0 -246
  55. package/src/js/module/TableTooltip.js +0 -1521
  56. package/src/js/module/Toolbar.js +0 -750
  57. package/src/js/module/VideoDialog.js +0 -193
  58. package/src/js/module/VideoResizer.js +0 -66
  59. package/src/js/module/VideoTooltip.js +0 -248
  60. package/src/js/module/emoji-data.js +0 -496
  61. package/src/js/renderer.js +0 -120
  62. package/src/js/settings.js +0 -214
  63. package/src/styles/_variables.scss +0 -48
  64. 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
- }
@@ -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
- }
@@ -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
- };
@@ -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
- }
@@ -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
- }