autumnnote 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +6 -4
  2. package/dist/autumnnote.css +324 -2
  3. package/dist/autumnnote.es.js +638 -227
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.umd.js +637 -226
  6. package/dist/autumnnote.umd.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/js/Context.js +8 -3
  9. package/src/js/core/detectLang.js +98 -0
  10. package/src/js/core/dom.js +62 -6
  11. package/src/js/core/markdown.js +14 -13
  12. package/src/js/core/range.js +2 -2
  13. package/src/js/editing/History.js +6 -6
  14. package/src/js/editing/Style.js +19 -19
  15. package/src/js/editing/Table.js +4 -6
  16. package/src/js/editing/Typing.js +6 -6
  17. package/src/js/i18n/en.js +2 -0
  18. package/src/js/i18n/vi.js +2 -0
  19. package/src/js/index.js +3 -2
  20. package/src/js/module/BubbleToolbar.js +30 -13
  21. package/src/js/module/Buttons.js +16 -14
  22. package/src/js/module/Clipboard.js +5 -5
  23. package/src/js/module/CodeTooltip.js +42 -16
  24. package/src/js/module/Codeview.js +2 -2
  25. package/src/js/module/ContextMenu.js +12 -12
  26. package/src/js/module/Editor.js +64 -12
  27. package/src/js/module/EmojiDialog.js +19 -13
  28. package/src/js/module/FindReplace.js +85 -59
  29. package/src/js/module/Fullscreen.js +1 -1
  30. package/src/js/module/IconDialog.js +26 -20
  31. package/src/js/module/ImageCropOverlay.js +6 -6
  32. package/src/js/module/ImageDialog.js +18 -12
  33. package/src/js/module/ImageResizer.js +1 -1
  34. package/src/js/module/ImageTooltip.js +8 -4
  35. package/src/js/module/LinkDialog.js +18 -12
  36. package/src/js/module/LinkTooltip.js +5 -2
  37. package/src/js/module/Mention.js +3 -3
  38. package/src/js/module/Statusbar.js +2 -2
  39. package/src/js/module/TableTooltip.js +180 -18
  40. package/src/js/module/Toolbar.js +16 -15
  41. package/src/js/module/VideoDialog.js +8 -2
  42. package/src/js/module/VideoResizer.js +3 -3
  43. package/src/js/module/VideoTooltip.js +11 -6
  44. package/src/js/renderer.js +4 -2
  45. package/src/js/settings.js +53 -36
  46. package/src/styles/autumnnote.scss +332 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autumnnote",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Fast. Lightweight. Reliable. Efficient. A modern WYSIWYG editor built with vanilla JavaScript, no jQuery required.",
5
5
  "main": "dist/autumnnote.umd.js",
6
6
  "module": "dist/autumnnote.es.js",
package/src/js/Context.js CHANGED
@@ -59,7 +59,7 @@ export class Context {
59
59
  this.locale = resolveLocale(this.options.lang);
60
60
 
61
61
  /** @type {{ container: HTMLElement, editable: HTMLElement, toolbar?: HTMLElement, statusbar?: HTMLElement }} */
62
- this.layoutInfo = {};
62
+ this.layoutInfo = /** @type {any} */ ({});
63
63
 
64
64
  /** @type {Map<string, Function[]>} */
65
65
  this._listeners = new Map();
@@ -167,7 +167,7 @@ export class Context {
167
167
  /**
168
168
  * Registers and initialises a custom module on this instance.
169
169
  * @param {string} name
170
- * @param {Function} ModuleClass
170
+ * @param {new (ctx: this) => any} ModuleClass
171
171
  * @returns {this}
172
172
  */
173
173
  registerModule(name, ModuleClass) {
@@ -555,11 +555,16 @@ export class Context {
555
555
  this._disposers = [];
556
556
 
557
557
  const container = this.layoutInfo.container;
558
+ const wasDark = container && container.classList.contains('an-theme-dark');
558
559
  if (container && container.parentNode) {
559
560
  // Restore original element
560
561
  this.targetEl.style.display = '';
561
562
  container.parentNode.removeChild(container);
562
563
  }
564
+ // If this was a dark editor and no other dark containers remain, clean up body
565
+ if (wasDark && !document.querySelector('.an-container.an-theme-dark')) {
566
+ document.body.classList.remove('an-theme-dark');
567
+ }
563
568
 
564
569
  if (typeof this.options.onDestroy === 'function') {
565
570
  this.options.onDestroy(this);
@@ -578,7 +583,7 @@ export class Context {
578
583
  */
579
584
  _syncToTarget() {
580
585
  if (this.targetEl.tagName === 'TEXTAREA' || this.targetEl.tagName === 'INPUT') {
581
- this.targetEl.value = this.getHTML();
586
+ /** @type {HTMLInputElement} */ (this.targetEl).value = this.getHTML();
582
587
  }
583
588
  }
584
589
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * detectLang.js — Heuristic programming-language detection for code snippets.
3
+ *
4
+ * Returns a Prism.js language identifier or null when no language can be
5
+ * determined with reasonable confidence.
6
+ *
7
+ * Detection order (conflicts in parentheses):
8
+ * TypeScript → Rust → PHP → Java → Kotlin → Swift → Go
9
+ * → JavaScript → HTML → CSS → JSON → SQL → Python → Ruby
10
+ * → Bash → C++ → C# → C → XML
11
+ *
12
+ * @param {string} code
13
+ * @returns {string|null}
14
+ */
15
+ export function detectLang(code) {
16
+ if (!code || !code.trim()) return null;
17
+ const s = code.trim();
18
+
19
+ // ── TypeScript ─────────────────────────────────────────────────────────────
20
+ // Must be first — TS is a JS superset; its markers are specific.
21
+ if (/(:\s*(string|number|boolean|void|never|any|unknown)\b|interface\s+\w+\s*\{|type\s+\w+\s*[=<(]|<\w+>\s*[;,)]|readonly\s+\w|enum\s+\w+\s*\{|\?\s*:\s*\w|as\s+\w+\s*[;,)\]])/.test(s)) return 'typescript';
22
+
23
+ // ── Rust ────────────────────────────────────────────────────────────────────
24
+ // Before JavaScript — both have `let`. `!` macros and `pub fn` are unique.
25
+ if (/\bprintln!\s*\(|\bprint!\s*\(|\bfn\s+\w+\s*(<[^>]*>)?\s*\(|\blet\s+mut\s|\bpub\s+fn\s|\buse\s+std::|\bimpl\s+\w+|\bOption<|\bResult<\w+/.test(s)) return 'rust';
26
+
27
+ // ── PHP ─────────────────────────────────────────────────────────────────────
28
+ // Before Bash — both have `echo`. `<?php` and `$var` are unique to PHP.
29
+ if (/(<\?php\b|<\?=|\becho\s+.*\$\w|\$this->|\$\w+\s*=\s*\w|\bforeach\s*\(\s*\$|Illuminate\\)/.test(s)) return 'php';
30
+
31
+ // ── Java ────────────────────────────────────────────────────────────────────
32
+ // Before Kotlin — `System.out.println()` would otherwise match Kotlin's
33
+ // `println\s*\(` pattern.
34
+ if (/\bpublic\s+(class|static|void|int|String)\s+\w|System\.out\.(print|println)\s*\(|@(Override|Autowired|Component|Service|Controller)\b|import\s+java\.(util|io|lang|net)\.|throws\s+\w+Exception/.test(s)) return 'java';
35
+
36
+ // ── Kotlin ──────────────────────────────────────────────────────────────────
37
+ // Kotlin uses `val` (immutable) unlike Swift/JS which use `let`.
38
+ // `fun`, `data class`, `companion object`, `println()` are Kotlin-specific.
39
+ if (/\bfun\s+\w+\s*\(|\bdata\s+class\s+\w+|\bcompanion\s+object\b|\bval\s+\w+\s*:\s*\w|\bprintln\s*\(/.test(s)) return 'kotlin';
40
+
41
+ // ── Swift ───────────────────────────────────────────────────────────────────
42
+ // Swift uses `let` for constants (Kotlin uses `val`). `guard let`, `protocol`,
43
+ // `extension`, `func ... -> ReturnType`, and `let x: UppercaseType` are signals.
44
+ if (/\bguard\s+(let|var)\b|\bprotocol\s+\w+\s*\{|\bextension\s+\w+|\bfunc\s+\w+[^(]*\([^)]*\)\s*->\s*\w|\blet\s+\w+\s*:\s*[A-Z]\w*|\bSwiftUI\b/.test(s)) return 'swift';
45
+
46
+ // ── Go ──────────────────────────────────────────────────────────────────────
47
+ // `package`, `:=` short assignment, `fmt.Print*`, `chan`, `goroutine`.
48
+ if (/\bpackage\s+\w+\b|\bfmt\.(Print|Println|Sprintf|Errorf|Fprintf)\s*\(|:=\s*\w|\bgoroutine\b|\bchan\s+\w|\bgo\s+func\b/.test(s)) return 'go';
49
+
50
+ // ── JavaScript ──────────────────────────────────────────────────────────────
51
+ // Before HTML to handle JSX (`<div/>` would otherwise trigger HTML).
52
+ // `var\s+\w+\s*=` (not just `var\s+\w`) to avoid matching Swift `var x: Int`.
53
+ if (/\b(const\s+\w|let\s+\w+\s*=|var\s+\w+\s*=|function\s+\w|\=>\s*[{(]|import\s+.*\bfrom\b\s*['"]|require\s*\(|console\.(log|error|warn|info)|document\.\w|window\.\w|async\s+function|\bPromise\b|React\.|useState\s*\(|\.then\s*\()/.test(s)) return 'javascript';
54
+
55
+ // ── HTML ────────────────────────────────────────────────────────────────────
56
+ if (/^<!DOCTYPE html/i.test(s) || /<(html|head|body|div|section|article|nav|p|a|img|ul|ol|li|table|form|input|button|script|style)\b[^>]*>/i.test(s)) return 'html';
57
+
58
+ // ── SCSS ─────────────────────────────────────────────────────────────────────
59
+ // Before CSS — SCSS is a superset. Unique markers: `//` line comments,
60
+ // `&` nesting, `$variable`, `@mixin/@include/@extend`, `#{interpolation}`.
61
+ if (/(^|\n)\s*(\/\/\s+\S|&[:.[\w]|\$\w+\s*:|@(mixin|include|extend|each|if|for|use|forward)\b|#\{)/.test(s) && /[\w#.*&[\]:(),>+~ -]+\s*\{/.test(s)) return 'scss';
62
+
63
+ // ── CSS ──────────────────────────────────────────────────────────────────────
64
+ if (/(^|\n)\s*[\w#.*:[\]&, +-]+\s*\{[^}]*[\w-]+\s*:[^{}:;]+[;}\n]/m.test(s) && !/<\w|function\s|def\s|:\s*(string|number)/.test(s)) return 'css';
65
+
66
+ // ── JSON ────────────────────────────────────────────────────────────────────
67
+ if (/^\s*[{[]/.test(s) && /"\w[\w\s-]*"\s*:/.test(s) && !/\bfunction\b|\bdef\b/.test(s)) return 'json';
68
+
69
+ // ── SQL ─────────────────────────────────────────────────────────────────────
70
+ if (/(^|\n)\s*(SELECT\s|INSERT\s+INTO|UPDATE\s+\w|DELETE\s+FROM|CREATE\s+(TABLE|DATABASE|INDEX|VIEW)|DROP\s+(TABLE|DATABASE)|ALTER\s+TABLE|WITH\s+\w+\s+AS\s*\()/im.test(s)) return 'sql';
71
+
72
+ // ── Python ──────────────────────────────────────────────────────────────────
73
+ // `def` requires trailing `:` (Python rule); `class` checked at line start.
74
+ if (/\bdef\s+\w+\s*\([^)]*\)\s*:|(^|\n)\s*class\s+\w+.*:\s*$|(^|\n)\s*import\s+\w|(^|\n)\s*from\s+\w+\s+import\s+|\bprint\s*\(|if\s+__name__\s*==\s*['"]__main__['"]/m.test(s)) return 'python';
75
+
76
+ // ── Ruby ────────────────────────────────────────────────────────────────────
77
+ // `.each do |x|`, `attr_*`, `puts` with value, multi-line `def...end`.
78
+ if (/\bputs\s+\S|\battr_(accessor|reader|writer)\s|\.each\s+do\s*\|\w+\s*\||\bdo\s*\|\w+\s*\|.*\bend\b|\bdef\s+\w+[^:]*\n[\s\S]*?\bend\b/.test(s)) return 'ruby';
79
+
80
+ // ── Bash / Shell ─────────────────────────────────────────────────────────────
81
+ if (/^#!.*\/(ba|z|da|fi|k)?sh\b/m.test(s) || /\b(echo\s+["']|grep\s+|awk\s+|sed\s+['"\\/-]|chmod\s+|sudo\s+|apt(-get)?\s+install|brew\s+install|npm\s+(install|run|start|build)|pip\s+(install|3\s)|docker\s+(run|build|compose)|kubectl\s+|git\s+(clone|add|commit|push|pull|checkout))\b/.test(s)) return 'bash';
82
+
83
+ // ── C++ ─────────────────────────────────────────────────────────────────────
84
+ // Before C and C# — `cout<<`, `using namespace std`, `std::`, `template<>`.
85
+ if (/\bcout\s*<<|\bcin\s*>>|using\s+namespace\s+std\b|std::\w|\btemplate\s*<\w|\b#include\s*<(iostream|vector|map|set|algorithm|string|memory)>/.test(s)) return 'cpp';
86
+
87
+ // ── C# ──────────────────────────────────────────────────────────────────────
88
+ // After C++ to avoid matching C++'s `using namespace std`.
89
+ if (/\busing\s+System\b|Console\.(Write|WriteLine)\s*\(|\bget;\s*set;|\basync\s+Task[<\s]|IEnumerable<|\bLINQ\b|\.Select\s*\(|\.Where\s*\(/.test(s)) return 'csharp';
90
+
91
+ // ── C ───────────────────────────────────────────────────────────────────────
92
+ if (/\b#include\s*<(stdio|stdlib|string|math|time|ctype)\.h>|\bprintf\s*\(|\bscanf\s*\(|int\s+main\s*\(\s*(void|int\s+argc)|\bmalloc\s*\(|\bfree\s*\(/.test(s) && !/namespace|cout|cin|std::/.test(s)) return 'c';
93
+
94
+ // ── XML ─────────────────────────────────────────────────────────────────────
95
+ if (/^<\?xml\s/i.test(s) || /xmlns:|<\/[\w:]+>/.test(s)) return 'xml';
96
+
97
+ return null;
98
+ }
@@ -29,7 +29,7 @@ export const isInline = (node) =>
29
29
  isElement(node) &&
30
30
  /^(a|abbr|acronym|b|bdo|big|br|button|cite|code|dfn|em|i|img|input|kbd|label|map|object|output|q|s|samp|select|small|span|strong|sub|sup|textarea|time|tt|u|var)$/i.test(node.nodeName);
31
31
  /** @param {Node} node */
32
- export const isEditable = (node) => isElement(node) && node.isContentEditable;
32
+ export const isEditable = (node) => isElement(node) && /** @type {HTMLElement} */ (node).isContentEditable;
33
33
  /** @param {Node} node */
34
34
  export const isAnchor = (node) => isElement(node) && node.nodeName.toUpperCase() === 'A';
35
35
  /** @param {Node} node */
@@ -100,7 +100,7 @@ export function prevElement(node) {
100
100
  while (sibling && !isElement(sibling)) {
101
101
  sibling = sibling.previousSibling;
102
102
  }
103
- return sibling;
103
+ return /** @type {Element|null} */ (sibling);
104
104
  }
105
105
 
106
106
  /**
@@ -113,7 +113,7 @@ export function nextElement(node) {
113
113
  while (sibling && !isElement(sibling)) {
114
114
  sibling = sibling.nextSibling;
115
115
  }
116
- return sibling;
116
+ return /** @type {Element|null} */ (sibling);
117
117
  }
118
118
 
119
119
  // ---------------------------------------------------------------------------
@@ -214,7 +214,7 @@ export function isEmpty(node) {
214
214
  if (isText(node)) return !node.nodeValue;
215
215
  if (isVoid(node)) return false;
216
216
  if (node.childNodes.length === 1 && node.firstChild?.nodeName === 'BR') return true;
217
- return !node.textContent.trim() && !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
  /**
@@ -303,12 +303,12 @@ export function trapFocus(container, onEscape) {
303
303
  if (e.shiftKey) {
304
304
  if (document.activeElement === first) {
305
305
  e.preventDefault();
306
- last.focus();
306
+ /** @type {HTMLElement} */ (last).focus();
307
307
  }
308
308
  } else {
309
309
  if (document.activeElement === last) {
310
310
  e.preventDefault();
311
- first.focus();
311
+ /** @type {HTMLElement} */ (first).focus();
312
312
  }
313
313
  }
314
314
  };
@@ -316,3 +316,59 @@ export function trapFocus(container, onEscape) {
316
316
  document.addEventListener('keydown', handler);
317
317
  return () => document.removeEventListener('keydown', handler);
318
318
  }
319
+
320
+ /**
321
+ * Makes a dialog box draggable by its handle element.
322
+ * On first drag the box is pinned to its current viewport coordinates via
323
+ * `position:fixed`, freeing it from the parent flex container's centering.
324
+ * The position is clamped to the visible viewport.
325
+ *
326
+ * @param {HTMLElement} handle Element the user grabs (title bar / header)
327
+ * @param {HTMLElement} box Element that actually moves
328
+ * @returns {Function} Cleanup function (removes the mousedown listener)
329
+ */
330
+ export function makeDraggable(handle, box) {
331
+ handle.style.cursor = 'grab';
332
+
333
+ const onMousedown = (e) => {
334
+ if (e.button !== 0) return;
335
+ // Don't start drag when clicking on interactive children of the handle
336
+ if (/** @type {Element} */ (e.target).closest('button, input, select, textarea, a')) return;
337
+
338
+ e.preventDefault();
339
+
340
+ // First drag: snapshot position and pin to viewport with position:fixed
341
+ if (!box.dataset.anDragPinned) {
342
+ const r = box.getBoundingClientRect();
343
+ box.style.position = 'fixed';
344
+ box.style.margin = '0';
345
+ box.style.left = `${r.left}px`;
346
+ box.style.top = `${r.top}px`;
347
+ box.dataset.anDragPinned = '1';
348
+ }
349
+
350
+ const startX = e.clientX - parseFloat(box.style.left);
351
+ const startY = e.clientY - parseFloat(box.style.top);
352
+
353
+ handle.style.cursor = 'grabbing';
354
+
355
+ const onMove = (ev) => {
356
+ const bw = box.offsetWidth;
357
+ const bh = box.offsetHeight;
358
+ box.style.left = `${Math.max(0, Math.min(ev.clientX - startX, window.innerWidth - bw))}px`;
359
+ box.style.top = `${Math.max(0, Math.min(ev.clientY - startY, window.innerHeight - bh))}px`;
360
+ };
361
+
362
+ const onUp = () => {
363
+ handle.style.cursor = 'grab';
364
+ document.removeEventListener('mousemove', onMove);
365
+ document.removeEventListener('mouseup', onUp);
366
+ };
367
+
368
+ document.addEventListener('mousemove', onMove);
369
+ document.addEventListener('mouseup', onUp);
370
+ };
371
+
372
+ handle.addEventListener('mousedown', onMousedown);
373
+ return () => handle.removeEventListener('mousedown', onMousedown);
374
+ }
@@ -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 codeEl = el.querySelector('code');
69
70
  const langMatch = ((codeEl && codeEl.className) || '').match(/language-(\S+)/);
70
71
  const lang = langMatch ? langMatch[1] : '';
71
- const content = (codeEl || 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,7 +102,7 @@ function _domToMd(node, depth = 0) {
101
102
  case 'li': return inner();
102
103
  case 'hr': return '\n\n---\n\n';
103
104
  case 'table': {
104
- const rows = Array.from(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
108
  Array.from(tr.querySelectorAll('th, td')).map((c) => c.textContent.trim().replace(/\|/g, '\\|')),
@@ -57,7 +57,7 @@ export class WrappedRange {
57
57
  commonAncestor() {
58
58
  const native = this.toNativeRange();
59
59
  const ancestor = native.commonAncestorContainer;
60
- return isElement(ancestor) ? ancestor : ancestor.parentElement;
60
+ return /** @type {Element|null} */ (isElement(ancestor) ? ancestor : ancestor.parentElement);
61
61
  }
62
62
 
63
63
  /**
@@ -66,7 +66,7 @@ export class WrappedRange {
66
66
  * @returns {Element|null}
67
67
  */
68
68
  blockNode(editable) {
69
- return closest(this.sc, (n) => isElement(n) && n !== editable, editable);
69
+ return /** @type {Element|null} */ (closest(this.sc, (n) => isElement(n) && n !== editable, editable));
70
70
  }
71
71
 
72
72
  /**
@@ -11,7 +11,7 @@ export class History {
11
11
  constructor(editable, limit = 100) {
12
12
  this.editable = editable;
13
13
  this._limit = limit;
14
- /** @type {Array<{html: string, range: {sc: string, so: number, ec: string, eo: number}|null}>} */
14
+ /** @type {Array<{html: string, images?: Record<string,string>, sel: {start: number, end: number}|null}>} */
15
15
  this.stack = [];
16
16
  this.stackOffset = -1;
17
17
  this._savePoint();
@@ -54,7 +54,7 @@ export class History {
54
54
  let cur;
55
55
  while ((cur = walker.nextNode())) {
56
56
  if (cur === node) return count + offset;
57
- count += cur.length;
57
+ count += /** @type {Text} */ (cur).length;
58
58
  }
59
59
  return 0;
60
60
  }
@@ -71,7 +71,7 @@ export class History {
71
71
  const walker = document.createTreeWalker(this.editable, NodeFilter.SHOW_TEXT, null);
72
72
  let cur;
73
73
  while ((cur = walker.nextNode())) {
74
- const len = cur.length;
74
+ const len = /** @type {Text} */ (cur).length;
75
75
  if (!startNode && count + len >= saved.start) {
76
76
  startNode = cur;
77
77
  startOff = saved.start - count;
@@ -88,7 +88,7 @@ export class History {
88
88
  const lastWalker = document.createTreeWalker(this.editable, NodeFilter.SHOW_TEXT, null);
89
89
  let lastNode = null;
90
90
  while ((lastNode = lastWalker.nextNode())) { startNode = lastNode; }
91
- startOff = startNode ? startNode.length : 0;
91
+ startOff = startNode ? /** @type {Text} */ (startNode).length : 0;
92
92
  endNode = startNode;
93
93
  endOff = startOff;
94
94
  }
@@ -148,8 +148,8 @@ export class History {
148
148
  */
149
149
  _tokenizeImages(html) {
150
150
  // Fast-path: skip regex entirely when there are no data URIs (common case)
151
- if (!html.includes('data:')) return { html, images: {} };
152
- const images = {};
151
+ if (!html.includes('data:')) return { html, images: /** @type {Record<string,string>} */ ({}) };
152
+ const images = /** @type {Record<string,string>} */ ({});
153
153
  let index = 0;
154
154
  const tokenized = html.replace(/data:[^;]+;base64,[^"' >]*/g, (match) => {
155
155
  const token = `__asn_img_${index}__`;
@@ -45,7 +45,7 @@ export function underline() {
45
45
  let container = sel.getRangeAt(0).commonAncestorContainer;
46
46
  if (container.nodeType === 3) container = container.parentElement;
47
47
  // Check if we're inside a <u> (DOM truth), to guard against unreliable queryCommandState
48
- const uEl = container && container.closest && container.closest('u');
48
+ const uEl = container && /** @type {Element} */ (container).closest('u');
49
49
  const nativeState = document.queryCommandState('underline');
50
50
  if (uEl && !nativeState) {
51
51
  // Browser doesn't recognise the underline state (e.g. inside <code>).
@@ -71,7 +71,7 @@ export function strikethrough() {
71
71
  // when the selection spans across nested inline elements.
72
72
  let sc = sel.getRangeAt(0).startContainer;
73
73
  if (sc.nodeType === 3) sc = sc.parentElement;
74
- const sEl = sc && sc.closest && (sc.closest('s') || sc.closest('strike'));
74
+ const sEl = sc && (/** @type {Element} */ (sc).closest('s') || /** @type {Element} */ (sc).closest('strike'));
75
75
  const nativeState = document.queryCommandState('strikeThrough');
76
76
  if (sEl && !nativeState) {
77
77
  // Browser doesn’t recognise the strikethrough state (e.g. inside <code>
@@ -116,7 +116,7 @@ export const fontName = (name) => execCommand('fontName', name);
116
116
  * Sets the font size (in pt or with unit) for the selection.
117
117
  * Uses a span-based approach to set px sizes precisely.
118
118
  * @param {string} size - e.g. '14px'
119
- * @param {HTMLElement} [editable] - scoping element to avoid touching nodes outside this editor
119
+ * @param {HTMLElement|Document} [editable] - scoping element to avoid touching nodes outside this editor
120
120
  */
121
121
  export function fontSize(size, editable = document) {
122
122
  const sel = window.getSelection();
@@ -226,9 +226,9 @@ export function outdent() {
226
226
  if (sel && sel.rangeCount) {
227
227
  let container = sel.getRangeAt(0).commonAncestorContainer;
228
228
  if (container.nodeType === 3) container = container.parentElement;
229
- const checkLi = container && container.closest && container.closest('.an-checklist li');
229
+ const checkLi = container && /** @type {Element} */ (container).closest('.an-checklist li');
230
230
  if (checkLi) {
231
- _checklistItemToP(checkLi);
231
+ _checklistItemToP(/** @type {HTMLElement} */ (checkLi));
232
232
  return;
233
233
  }
234
234
  }
@@ -257,7 +257,7 @@ function _checklistItemToP(checkLi) {
257
257
  // Build <p> preserving inline formatting (bold/italic/links) from the item's content
258
258
  const p = document.createElement('p');
259
259
  for (const child of checkLi.childNodes) {
260
- if (child.nodeType === 1 && child.tagName === 'INPUT') continue;
260
+ if (child.nodeType === 1 && /** @type {Element} */ (child).tagName === 'INPUT') continue;
261
261
  p.appendChild(child.cloneNode(true));
262
262
  }
263
263
  // Strip ZWS anchors left over from checklist markup
@@ -373,7 +373,7 @@ export function currentStyle(editable) {
373
373
  ? range.sc
374
374
  : range.commonAncestor();
375
375
 
376
- const el = isElement(container) ? container : container.parentElement;
376
+ const el = /** @type {Element|null} */ (isElement(container) ? container : container.parentElement);
377
377
  if (!el) return {};
378
378
 
379
379
  const computed = window.getComputedStyle(el);
@@ -402,15 +402,15 @@ export function currentStyle(editable) {
402
402
  /**
403
403
  * Wraps the selection in an inline <code> element, or unwraps it if the
404
404
  * cursor is already inside a <code> that is not inside a <pre>.
405
- * @param {HTMLElement} [editable]
405
+ * @param {HTMLElement} [_editable]
406
406
  */
407
- export function toggleInlineCode(editable) {
407
+ export function toggleInlineCode(_editable) {
408
408
  const sel = window.getSelection();
409
409
  if (!sel || !sel.rangeCount) return;
410
410
  const range = sel.getRangeAt(0);
411
411
  let container = range.commonAncestorContainer;
412
412
  if (container.nodeType === 3) container = container.parentElement;
413
- const codeEl = container && container.closest ? container.closest('code') : null;
413
+ const codeEl = container && /** @type {Element} */ (container).closest('code');
414
414
  if (codeEl && !codeEl.closest('pre')) {
415
415
  // Unwrap — save range endpoints relative to surrounding text so we can
416
416
  // restore the selection after normalize() merges adjacent text nodes.
@@ -480,7 +480,7 @@ export function isInlineCode() {
480
480
  if (!sel || !sel.rangeCount) return false;
481
481
  let sc = sel.getRangeAt(0).startContainer;
482
482
  if (sc.nodeType === 3) sc = sc.parentElement;
483
- const code = sc && sc.closest ? sc.closest('code') : null;
483
+ const code = sc && /** @type {Element} */ (sc).closest('code');
484
484
  return !!(code && !code.closest('pre'));
485
485
  }
486
486
 
@@ -509,18 +509,18 @@ export function toggleChecklist() {
509
509
  let container = range.commonAncestorContainer;
510
510
  if (container.nodeType === 3) container = container.parentElement;
511
511
 
512
- const ul = container.closest && container.closest('.an-checklist');
512
+ const ul = container && /** @type {Element} */ (container).closest('.an-checklist');
513
513
  if (ul) {
514
514
  // If selection covers multiple <li>, convert them all
515
515
  const selectedLis = Array.from(ul.querySelectorAll('li')).filter((li) =>
516
516
  sel.containsNode(li, true),
517
517
  );
518
518
  if (selectedLis.length > 0) {
519
- let firstP = null;
519
+ /** @type {HTMLElement|null} */ let firstP = null;
520
520
  selectedLis.forEach((li) => {
521
521
  const p = document.createElement('p');
522
522
  for (const child of li.childNodes) {
523
- if (child.nodeType === 1 && child.tagName === 'INPUT') continue;
523
+ if (child.nodeType === 1 && /** @type {Element} */ (child).tagName === 'INPUT') continue;
524
524
  p.appendChild(child.cloneNode(true));
525
525
  }
526
526
  p.innerHTML = p.innerHTML.replace(/\u200b/g, '');
@@ -551,9 +551,9 @@ export function toggleChecklist() {
551
551
  // Find the nearest block-level ancestor (p, div, li, h1-h6, blockquote, etc.)
552
552
  // and convert it into a single checklist item.
553
553
  const BLOCK_TAGS = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI']);
554
- let block = container;
554
+ let block = /** @type {Element|null} */ (container);
555
555
  while (block && block.parentNode && !BLOCK_TAGS.has(block.tagName)) {
556
- block = block.parentNode;
556
+ block = /** @type {Element|null} */ (block.parentNode);
557
557
  }
558
558
  // Fallback: if no block element found (e.g. cursor directly in editable root), use the
559
559
  // insertion approach with a zero-width-space item so the cursor ends up inside.
@@ -619,7 +619,7 @@ export function toggleChecklist() {
619
619
  let node;
620
620
  while ((node = iter.nextNode())) {
621
621
  if (!range.intersectsNode(node)) continue;
622
- let block = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
622
+ let block = /** @type {Element|null} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
623
623
  while (block && !BLOCK_TAGS_MULTI.has(block.tagName)) {
624
624
  block = block.parentElement;
625
625
  }
@@ -634,7 +634,7 @@ export function toggleChecklist() {
634
634
  // Build checklist and replace collected blocks.
635
635
  const newUl = document.createElement('ul');
636
636
  newUl.className = 'an-checklist';
637
- let lastTextNode = null;
637
+ /** @type {Text|null} */ let lastTextNode = null;
638
638
  blocks.forEach((block) => {
639
639
  const li = document.createElement('li');
640
640
  const cb = document.createElement('input');
@@ -677,5 +677,5 @@ export function isInChecklist() {
677
677
  if (!sel || !sel.rangeCount) return false;
678
678
  let container = sel.getRangeAt(0).commonAncestorContainer;
679
679
  if (container.nodeType === 3) container = container.parentElement;
680
- return !!(container && container.closest && container.closest('.an-checklist li'));
680
+ return !!(container && /** @type {Element} */ (container).closest('.an-checklist li'));
681
681
  }
@@ -13,8 +13,7 @@ import { createElement } from '../core/dom.js';
13
13
  * Build an HTML table with the given number of columns and rows, optionally including a header row.
14
14
  * @param {number} cols - Number of columns in each row.
15
15
  * @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
16
- * @param {{ headerRow?: boolean }} [opts] - Options object.
17
- * @param {boolean} [opts.headerRow=false] - When true and `rows > 0`, creates a header row (`<thead>`) plus body rows for the remainder.
16
+ * @param {{ headerRow?: boolean }} [opts] - Options: `headerRow` creates a `<thead>` when true.
18
17
  * @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
19
18
  */
20
19
  export function createTable(cols, rows, opts = {}) {
@@ -44,7 +43,7 @@ export function createTable(cols, rows, opts = {}) {
44
43
  }
45
44
  tbody.appendChild(tr);
46
45
  }
47
- return table;
46
+ return /** @type {HTMLTableElement} */ (table);
48
47
  }
49
48
 
50
49
  /**
@@ -52,7 +51,6 @@ export function createTable(cols, rows, opts = {}) {
52
51
  * @param {number} cols - Number of columns for the new table.
53
52
  * @param {number} rows - Number of rows for the new table.
54
53
  * @param {{ headerRow?: boolean }} [opts] - Options for table creation.
55
- * @param {boolean} [opts.headerRow=false] - If true, include a header row as the first row.
56
54
  */
57
55
  export function insertTable(cols, rows, opts = {}) {
58
56
  if (cols <= 0 || rows <= 0) return;
@@ -65,8 +63,8 @@ export function insertTable(cols, rows, opts = {}) {
65
63
 
66
64
  // Walk up to find the nearest block-level ancestor to insert after
67
65
  const BLOCK = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'BLOCKQUOTE', 'LI', 'PRE']);
68
- let anchor = range.startContainer;
69
- if (anchor.nodeType === 3) anchor = anchor.parentElement;
66
+ let anchor = /** @type {Element|null} */ (range.startContainer);
67
+ if (anchor && anchor.nodeType === 3) anchor = anchor.parentElement;
70
68
  while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) {
71
69
  anchor = anchor.parentElement;
72
70
  }
@@ -44,11 +44,11 @@ export function handleKeydown(event, editable, options = {}) {
44
44
  if (sel && sel.rangeCount > 0) {
45
45
  const r = sel.getRangeAt(0);
46
46
  if (r.collapsed && r.startContainer.nodeType === Node.TEXT_NODE) {
47
- const textNode = r.startContainer;
47
+ const textNode = /** @type {ChildNode} */ (r.startContainer);
48
48
  // Case A: cursor at offset 0, preceding sibling is an FA icon
49
49
  if (r.startOffset === 0 && isFAIcon(textNode.previousSibling)) {
50
50
  event.preventDefault();
51
- textNode.previousSibling.remove();
51
+ /** @type {ChildNode} */ (textNode.previousSibling).remove();
52
52
  return true;
53
53
  }
54
54
 
@@ -59,7 +59,7 @@ export function handleKeydown(event, editable, options = {}) {
59
59
  isFAIcon(textNode.previousSibling)) {
60
60
  event.preventDefault();
61
61
  const parent = textNode.parentNode;
62
- const icon = textNode.previousSibling;
62
+ const icon = /** @type {ChildNode} */ (textNode.previousSibling);
63
63
  const prevNode = icon.previousSibling; // node before the icon (e.g. ZWS of prior icon)
64
64
  icon.remove();
65
65
  textNode.remove();
@@ -241,7 +241,7 @@ export function handleKeydown(event, editable, options = {}) {
241
241
 
242
242
  // Hoist sc/el once so all guards below can reuse them.
243
243
  const sc = range.sc;
244
- const el = sc.nodeType === 3 ? sc.parentElement : sc;
244
+ const el = /** @type {Element|null} */ (sc.nodeType === 3 ? sc.parentElement : sc);
245
245
 
246
246
  // Guard: if the cursor is inside a <i> FA icon element (zero text children,
247
247
  // rendered entirely by CSS ::before), pressing Enter would split the block
@@ -259,7 +259,7 @@ export function handleKeydown(event, editable, options = {}) {
259
259
 
260
260
  // Video wrapper — Enter should create a new paragraph after the wrapper,
261
261
  // not split the wrapper's container and produce an empty video clone.
262
- const videoWrapper = el && el.closest && el.closest('.an-video-wrapper');
262
+ const videoWrapper = el && el.closest('.an-video-wrapper');
263
263
  if (videoWrapper) {
264
264
  event.preventDefault();
265
265
  const p = document.createElement('p');
@@ -275,7 +275,7 @@ export function handleKeydown(event, editable, options = {}) {
275
275
  }
276
276
 
277
277
  // Checklist — Enter creates new item; empty item exits the list
278
- const checkLi = el && el.closest && el.closest('.an-checklist li');
278
+ const checkLi = el && el.closest('.an-checklist li');
279
279
  if (checkLi) {
280
280
  event.preventDefault();
281
281
  const ul = checkLi.closest('.an-checklist');
package/src/js/i18n/en.js CHANGED
@@ -298,6 +298,8 @@ export const en = {
298
298
  rowHeight: 'Row Height',
299
299
  tableBorderWidth: 'Table Border Width',
300
300
  deleteTable: 'Delete Table',
301
+ cellBackground: 'Cell Background',
302
+ noShading: 'No Shading',
301
303
  columnWidthPx: 'Column Width (px)',
302
304
  rowHeightPx: 'Row Height (px)',
303
305
  tableBorderWidthPx: 'Table Border Width (px)',