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.
Files changed (53) hide show
  1. package/README.md +10 -8
  2. package/dist/autumnnote.css +324 -4
  3. package/dist/autumnnote.es.js +943 -526
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +935 -519
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +21 -3
  8. package/src/js/Context.js +23 -16
  9. package/src/js/core/detectLang.js +98 -0
  10. package/src/js/core/dom.js +67 -11
  11. package/src/js/core/env.js +1 -1
  12. package/src/js/core/func.js +1 -1
  13. package/src/js/core/lists.js +1 -1
  14. package/src/js/core/markdown.js +32 -31
  15. package/src/js/core/range.js +8 -8
  16. package/src/js/editing/History.js +10 -10
  17. package/src/js/editing/Style.js +44 -44
  18. package/src/js/editing/Table.js +5 -7
  19. package/src/js/editing/Typing.js +19 -19
  20. package/src/js/i18n/en.js +2 -0
  21. package/src/js/i18n/vi.js +2 -0
  22. package/src/js/index.js +3 -2
  23. package/src/js/module/AutoSaveRestore.js +2 -4
  24. package/src/js/module/BubbleToolbar.js +51 -34
  25. package/src/js/module/Buttons.js +20 -18
  26. package/src/js/module/Clipboard.js +16 -17
  27. package/src/js/module/CodeTooltip.js +48 -24
  28. package/src/js/module/Codeview.js +5 -7
  29. package/src/js/module/ContextMenu.js +37 -37
  30. package/src/js/module/Editor.js +77 -26
  31. package/src/js/module/EmojiDialog.js +24 -18
  32. package/src/js/module/FindReplace.js +93 -66
  33. package/src/js/module/Fullscreen.js +1 -1
  34. package/src/js/module/IconDialog.js +39 -35
  35. package/src/js/module/ImageCropOverlay.js +13 -13
  36. package/src/js/module/ImageDialog.js +19 -13
  37. package/src/js/module/ImageResizer.js +5 -5
  38. package/src/js/module/ImageTooltip.js +20 -16
  39. package/src/js/module/LinkDialog.js +20 -14
  40. package/src/js/module/LinkTooltip.js +15 -12
  41. package/src/js/module/MarkdownShortcuts.js +11 -14
  42. package/src/js/module/Mention.js +11 -13
  43. package/src/js/module/Placeholder.js +1 -1
  44. package/src/js/module/ShortcutsDialog.js +2 -4
  45. package/src/js/module/Statusbar.js +5 -7
  46. package/src/js/module/TableTooltip.js +196 -36
  47. package/src/js/module/Toolbar.js +35 -34
  48. package/src/js/module/VideoDialog.js +16 -10
  49. package/src/js/module/VideoResizer.js +7 -9
  50. package/src/js/module/VideoTooltip.js +15 -10
  51. package/src/js/renderer.js +5 -3
  52. package/src/js/settings.js +53 -36
  53. package/src/styles/autumnnote.scss +332 -7
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "autumnnote",
3
- "version": "1.5.0",
4
- "description": "Fast. Lightweight. Reliable. Efficient. A modern WYSIWYG editor built with vanilla JavaScript, no jQuery required.",
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 {Function} ModuleClass
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
- document.body.removeChild(a);
480
+ a.remove();
481
481
  URL.revokeObjectURL(url);
482
482
  }
483
483
 
484
484
  /**
485
- * Opens the editor content in a new window and triggers the browser print dialog.
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(/</g, '&lt;').replace(/>/g, '&gt;');
491
- const w = window.open('', '_blank');
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
- w.document.close();
508
- w.onload = () => { w.print(); };
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
- if (container && container.parentNode) {
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.parentNode.removeChild(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
+ }
@@ -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.parentNode.removeChild(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
- parent.removeChild(node);
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() && !node.querySelector('img, video, hr, table');
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 = window.getSelection();
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 && 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[els.length - 1];
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
+ }
@@ -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 window || navigator.maxTouchPoints > 0,
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
  };
@@ -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 window in milliseconds during which at most one call is allowed.
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) {
@@ -52,7 +52,7 @@ export function tail(arr, n = 1) {
52
52
  * @returns {T[]}
53
53
  */
54
54
  export function flatten(arr) {
55
- return arr.reduce((acc, val) => acc.concat(val), []);
55
+ return arr.flat();
56
56
  }
57
57
 
58
58
  /**
@@ -37,8 +37,9 @@ function _domToMd(node, depth = 0) {
37
37
  }
38
38
  if (node.nodeType !== 1) return '';
39
39
 
40
- const tag = node.nodeName.toLowerCase();
41
- const inner = () => Array.from(node.childNodes).map(n => _domToMd(n, depth)).join('');
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 (node.closest('pre')) return inner();
65
+ if (el.closest('pre')) return inner();
65
66
  return `\`${inner()}\``;
66
67
  }
67
68
  case 'pre': {
68
- const codeEl = node.querySelector('code');
69
- const langMatch = ((codeEl && codeEl.className) || '').match(/language-(\S+)/);
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 || node).textContent || '';
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 = node.getAttribute('href') || '';
80
+ const href = el.getAttribute('href') || '';
80
81
  return `[${inner()}](${href})`;
81
82
  }
82
83
  case 'img': {
83
- const src = node.getAttribute('src') || '';
84
- const alt = node.getAttribute('alt') || '';
84
+ const src = el.getAttribute('src') || '';
85
+ const alt = el.getAttribute('alt') || '';
85
86
  return `![${alt}](${src})`;
86
87
  }
87
88
  case 'ul': {
88
- const items = Array.from(node.querySelectorAll(':scope > li'));
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(node.querySelectorAll(':scope > li'));
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(node.querySelectorAll('tr'));
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().replace(/\|/g, '\\|')),
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.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
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 = line.match(/^```(\S*)$/);
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 = line.match(/^(#{1,6})\s+(.+)$/);
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 thead = `<thead><tr>${headerCells.map((c) => `<th>${_inline(c)}</th>`).join('')}</tr></thead>`;
231
- const tbody = bodyRows.length
232
- ? `<tbody>${bodyRows.map((row) => `<tr>${row.map((c) => `<td>${_inline(c)}</td>`).join('')}</tr>`).join('')}</tbody>`
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
- .replace(/&/g, '&amp;')
301
- .replace(/</g, '&lt;')
302
- .replace(/>/g, '&gt;');
301
+ .replaceAll('&', '&amp;')
302
+ .replaceAll('<', '&lt;')
303
+ .replaceAll('>', '&gt;');
303
304
  }
304
305
 
305
306
  function _escAttr(v) {
306
307
  return String(v)
307
- .replace(/&/g, '&amp;')
308
- .replace(/"/g, '&quot;')
309
- .replace(/'/g, '&#39;')
310
- .replace(/</g, '&lt;')
311
- .replace(/>/g, '&gt;');
308
+ .replaceAll('&', '&amp;')
309
+ .replaceAll('"', '&quot;')
310
+ .replaceAll("'", '&#39;')
311
+ .replaceAll('<', '&lt;')
312
+ .replaceAll('>', '&gt;');
312
313
  }
@@ -41,10 +41,10 @@ export class WrappedRange {
41
41
  }
42
42
 
43
43
  /**
44
- * Select this wrapped range in the window.
44
+ * Select this wrapped range in the globalThis.
45
45
  */
46
46
  select() {
47
- const sel = window.getSelection();
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 window selection,
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 = window.getSelection();
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 = window.getSelection();
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 = window.getSelection();
174
+ const sel = globalThis.getSelection();
175
175
  if (!sel || sel.rangeCount === 0) {
176
176
  fn(null);
177
177
  return;