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
@@ -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();
@@ -31,7 +31,7 @@ export class History {
31
31
  * @returns {{ start: number, end: number }|null}
32
32
  */
33
33
  _serializeSelection() {
34
- const sel = window.getSelection();
34
+ const sel = globalThis.getSelection();
35
35
  if (!sel || sel.rangeCount === 0) return null;
36
36
  const range = sel.getRangeAt(0);
37
37
  if (!this.editable.contains(range.startContainer)) return null;
@@ -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
  }
@@ -97,7 +97,7 @@ export class History {
97
97
  const range = document.createRange();
98
98
  range.setStart(startNode, startOff);
99
99
  range.setEnd(endNode, endOff);
100
- const sel = window.getSelection();
100
+ const sel = globalThis.getSelection();
101
101
  sel.removeAllRanges();
102
102
  sel.addRange(range);
103
103
  } catch (_) {
@@ -106,7 +106,7 @@ export class History {
106
106
  const fb = document.createRange();
107
107
  fb.setStart(this.editable, 0);
108
108
  fb.collapse(true);
109
- const s = window.getSelection();
109
+ const s = globalThis.getSelection();
110
110
  if (s) { s.removeAllRanges(); s.addRange(fb); }
111
111
  } catch (_2) { /* fully give up */ }
112
112
  }
@@ -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}__`;
@@ -181,7 +181,7 @@ export class History {
181
181
  const current = this._serialize();
182
182
  const { html: tokenized } = this._tokenizeImages(current);
183
183
  const prev = this.stack[this.stackOffset];
184
- if (prev && prev.html === tokenized) return; // No change
184
+ if (prev?.html === tokenized) return; // No change
185
185
  this._savePoint();
186
186
  }
187
187
 
@@ -40,19 +40,19 @@ export const italic = () => execCommand('italic');
40
40
  * execCommand's state detection is unreliable.
41
41
  */
42
42
  export function underline() {
43
- const sel = window.getSelection();
43
+ const sel = globalThis.getSelection();
44
44
  if (!sel || !sel.rangeCount) return;
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 = /** @type {Element|null} */ (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>).
52
52
  // Manually unwrap the <u> element.
53
53
  const parent = uEl.parentNode;
54
54
  while (uEl.firstChild) parent.insertBefore(uEl.firstChild, uEl);
55
- parent.removeChild(uEl);
55
+ uEl.remove();
56
56
  return;
57
57
  }
58
58
  execCommand('underline');
@@ -64,21 +64,21 @@ export function underline() {
64
64
  * execCommand's state detection is unreliable (mirrors underline() logic).
65
65
  */
66
66
  export function strikethrough() {
67
- const sel = window.getSelection();
67
+ const sel = globalThis.getSelection();
68
68
  if (!sel || !sel.rangeCount) return;
69
69
  // Use startContainer for consistent detection across collapsed and range
70
70
  // selections — commonAncestorContainer can miss ancestor <s>/<strike> tags
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 = /** @type {Element|null} */ (sc)?.closest('s') || /** @type {Element|null} */ (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>
78
78
  // or deeply nested inline formats). Manually unwrap the <s>/<strike>.
79
79
  const parent = sEl.parentNode;
80
80
  while (sEl.firstChild) parent.insertBefore(sEl.firstChild, sEl);
81
- parent.removeChild(sEl);
81
+ sEl.remove();
82
82
  return;
83
83
  }
84
84
  execCommand('strikeThrough');
@@ -116,10 +116,10 @@ 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
- const sel = window.getSelection();
122
+ const sel = globalThis.getSelection();
123
123
  const wasCollapsed = !sel || !sel.rangeCount || sel.getRangeAt(0).collapsed;
124
124
 
125
125
  // B-I-3/4: For a collapsed (caret) selection the browser's execCommand
@@ -158,7 +158,7 @@ export function fontSize(size, editable = document) {
158
158
  span.style.fontSize = size;
159
159
  el.parentNode.insertBefore(span, el);
160
160
  while (el.firstChild) span.appendChild(el.firstChild);
161
- el.parentNode.removeChild(el);
161
+ el.remove();
162
162
  newSpans.push(span);
163
163
  });
164
164
 
@@ -167,7 +167,7 @@ export function fontSize(size, editable = document) {
167
167
  // value until the next selectionchange event).
168
168
  if (!wasCollapsed && sel && newSpans.length > 0) {
169
169
  const first = newSpans[0];
170
- const last = newSpans[newSpans.length - 1];
170
+ const last = newSpans.at(-1);
171
171
  try {
172
172
  const nr = document.createRange();
173
173
  const startNode = first.firstChild || first;
@@ -222,13 +222,13 @@ export const indent = () => execCommand('indent');
222
222
  * (which would destroy the ul > li checklist structure).
223
223
  */
224
224
  export function outdent() {
225
- const sel = window.getSelection();
225
+ const sel = globalThis.getSelection();
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 = /** @type {Element|null} */ (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,11 +257,11 @@ 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
264
- p.innerHTML = p.innerHTML.replace(/\u200B/g, '');
264
+ p.innerHTML = p.innerHTML.replaceAll('\u200B', '');
265
265
  if (!p.hasChildNodes() || !p.textContent.trim()) {
266
266
  p.innerHTML = '';
267
267
  p.appendChild(document.createTextNode('\u00a0'));
@@ -279,8 +279,8 @@ function _checklistItemToP(checkLi) {
279
279
  checkUl.parentNode.insertBefore(p, checkUl.nextSibling);
280
280
 
281
281
  // Remove current li from checkUl; delete checkUl if now empty
282
- checkUl.removeChild(checkLi);
283
- if (checkUl.children.length === 0) checkUl.parentNode.removeChild(checkUl);
282
+ checkLi.remove();
283
+ if (checkUl.children.length === 0) checkUl.remove();
284
284
 
285
285
  // Place caret at start of the new <p>
286
286
  try {
@@ -288,7 +288,7 @@ function _checklistItemToP(checkLi) {
288
288
  const firstChild = p.firstChild;
289
289
  nr.setStart(firstChild && firstChild.nodeType === 3 ? firstChild : p, 0);
290
290
  nr.collapse(true);
291
- const s = window.getSelection();
291
+ const s = globalThis.getSelection();
292
292
  if (s) { s.removeAllRanges(); s.addRange(nr); }
293
293
  } catch {}
294
294
  }
@@ -316,7 +316,7 @@ export const insertOrderedList = () => execCommand('insertOrderedList');
316
316
  * @param {string} value - Line-height value to apply; typically a unitless multiplier (for example, "1.5").
317
317
  */
318
318
  export function lineHeight(value) {
319
- const sel = window.getSelection();
319
+ const sel = globalThis.getSelection();
320
320
  if (!sel || sel.rangeCount === 0) return;
321
321
 
322
322
  const range = sel.getRangeAt(0);
@@ -373,10 +373,10 @@ 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
- const computed = window.getComputedStyle(el);
379
+ const computed = globalThis.getComputedStyle(el);
380
380
 
381
381
  return {
382
382
  bold: document.queryCommandState('bold'),
@@ -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) {
408
- const sel = window.getSelection();
407
+ export function toggleInlineCode(_editable) {
408
+ const sel = globalThis.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 = /** @type {Element|null} */ (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.
@@ -419,17 +419,17 @@ export function toggleInlineCode(editable) {
419
419
  const prevSibling = codeEl.previousSibling;
420
420
  const movedChildren = Array.from(codeEl.childNodes);
421
421
  while (codeEl.firstChild) parent.insertBefore(codeEl.firstChild, codeEl);
422
- parent.removeChild(codeEl);
422
+ codeEl.remove();
423
423
  // Normalize only the immediate parent to merge adjacent text nodes without
424
424
  // invalidating distant selection anchors (full editable.normalize() can
425
425
  // cause selection offsets to shift, making subsequent format toggles miss).
426
- if (parent && parent.normalize) parent.normalize();
426
+ parent?.normalize();
427
427
  // Restore selection to the text that was inside the unwrapped <code>.
428
428
  if (movedChildren.length > 0) {
429
429
  try {
430
430
  // After normalize, find the merged text node that contains the content.
431
431
  const firstMoved = movedChildren[0];
432
- const lastMoved = movedChildren[movedChildren.length - 1];
432
+ const lastMoved = movedChildren.at(-1);
433
433
  const nr = document.createRange();
434
434
  // Use the (possibly merged) live node if still in the DOM.
435
435
  const anchorNode = (firstMoved.parentNode === parent) ? firstMoved : (prevSibling ? prevSibling.nextSibling : parent.firstChild);
@@ -476,11 +476,11 @@ export function toggleInlineCode(editable) {
476
476
  * @returns {boolean}
477
477
  */
478
478
  export function isInlineCode() {
479
- const sel = window.getSelection();
479
+ const sel = globalThis.getSelection();
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 = /** @type {Element|null} */ (sc)?.closest('code');
484
484
  return !!(code && !code.closest('pre'));
485
485
  }
486
486
 
@@ -503,34 +503,34 @@ export function isInlineCode() {
503
503
  * Empty or whitespace-only selections do not create a checklist.
504
504
  */
505
505
  export function toggleChecklist() {
506
- const sel = window.getSelection();
506
+ const sel = globalThis.getSelection();
507
507
  if (!sel || !sel.rangeCount) return;
508
508
  const range = sel.getRangeAt(0);
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 = /** @type {Element|null} */ (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
- p.innerHTML = p.innerHTML.replace(/\u200b/g, '');
526
+ p.innerHTML = p.innerHTML.replaceAll('\u200b', '');
527
527
  if (!p.hasChildNodes() || !p.textContent.trim()) {
528
528
  p.innerHTML = '';
529
529
  p.appendChild(document.createTextNode('\u00a0'));
530
530
  }
531
531
  ul.parentNode.insertBefore(p, ul);
532
532
  if (!firstP) firstP = p;
533
- ul.removeChild(li);
533
+ li.remove();
534
534
  });
535
535
  if (ul.children.length === 0) ul.remove();
536
536
  // Move caret to first converted paragraph
@@ -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.
@@ -561,7 +561,7 @@ export function toggleChecklist() {
561
561
  ? Array.from(block.childNodes)
562
562
  .map((n) => n.textContent)
563
563
  .join('')
564
- .replace(/\u00a0/g, ' ')
564
+ .replaceAll('\u00a0', ' ')
565
565
  : '';
566
566
 
567
567
  const ul = document.createElement('ul');
@@ -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');
@@ -656,7 +656,7 @@ export function toggleChecklist() {
656
656
  // Insert the new list before the first block, then remove all source blocks.
657
657
  const firstBlock = blocks[0];
658
658
  firstBlock.parentNode.insertBefore(newUl, firstBlock);
659
- blocks.forEach((block) => block.parentNode && block.parentNode.removeChild(block));
659
+ blocks.forEach((block) => block.remove());
660
660
 
661
661
  // Move caret to end of the last checklist item.
662
662
  if (lastTextNode) {
@@ -673,9 +673,9 @@ export function toggleChecklist() {
673
673
  * @returns {boolean}
674
674
  */
675
675
  export function isInChecklist() {
676
- const sel = window.getSelection();
676
+ const sel = globalThis.getSelection();
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 !!(/** @type {Element|null} */ (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,21 +51,20 @@ 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;
59
57
  const table = createTable(cols, rows, opts);
60
58
 
61
- const sel = window.getSelection();
59
+ const sel = globalThis.getSelection();
62
60
  if (!sel || sel.rangeCount === 0) return;
63
61
  const range = sel.getRangeAt(0);
64
62
  range.deleteContents();
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?.nodeType === 3) anchor = anchor.parentElement;
70
68
  while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) {
71
69
  anchor = anchor.parentElement;
72
70
  }
@@ -14,8 +14,8 @@ import { currentRange } from '../core/range.js';
14
14
  // at ~120+ events/sec during normal typing.
15
15
  // ---------------------------------------------------------------------------
16
16
  const _FA_PATTERN = /\bfa-/;
17
- const isFAIcon = (n) => !!(n && n.nodeName === 'I' && _FA_PATTERN.test(n.className || ''));
18
- const isZwsAnchor = (n) => !!(n && n.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
17
+ const isFAIcon = (n) => !!(n?.nodeName === 'I' && _FA_PATTERN.test(n.className || ''));
18
+ const isZwsAnchor = (n) => !!(n?.nodeType === Node.TEXT_NODE && (n.textContent === '\u200B' || n.textContent === ''));
19
19
 
20
20
  /**
21
21
  * Handles special keydown behaviour inside the editor.
@@ -26,7 +26,7 @@ const isZwsAnchor = (n) => !!(n && n.nodeType === Node.TEXT_NODE && (n.textConte
26
26
  */
27
27
  export function handleKeydown(event, editable, options = {}) {
28
28
  const moveCaret = (setFn) => {
29
- const sel = window.getSelection();
29
+ const sel = globalThis.getSelection();
30
30
  if (!sel) return false;
31
31
  const nr = document.createRange();
32
32
  setFn(nr);
@@ -40,15 +40,15 @@ export function handleKeydown(event, editable, options = {}) {
40
40
  // Backspace key — one-press deletion of a preceding FA icon (<i> element)
41
41
  // -------------------------------------------------------------------------
42
42
  if (isKey(event, key.BACKSPACE)) {
43
- const sel = window.getSelection();
44
- if (sel && sel.rangeCount > 0) {
43
+ const sel = globalThis.getSelection();
44
+ if (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();
@@ -89,7 +89,7 @@ export function handleKeydown(event, editable, options = {}) {
89
89
  // ArrowLeft / ArrowRight — one-press navigation across FA icon nodes
90
90
  // -------------------------------------------------------------------------
91
91
  if (isKey(event, key.LEFT) || isKey(event, key.RIGHT)) {
92
- const sel = window.getSelection();
92
+ const sel = globalThis.getSelection();
93
93
  if (!sel || sel.rangeCount === 0) return false;
94
94
 
95
95
  const r = sel.getRangeAt(0);
@@ -207,7 +207,7 @@ export function handleKeydown(event, editable, options = {}) {
207
207
  }
208
208
 
209
209
  // In a pre/code block, insert spaces using configured tabSize
210
- if (para && para.nodeName.toUpperCase() === 'PRE') {
210
+ if (para?.nodeName.toUpperCase() === 'PRE') {
211
211
  if (event.shiftKey) return false;
212
212
  event.preventDefault();
213
213
  execCommand('insertText', ' '.repeat(options.tabSize || 4));
@@ -241,25 +241,25 @@ 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
248
248
  // and leave an orphan <i> in the new paragraph — visually an "auto-created
249
249
  // icon". Push the cursor to just after the <i> first, then fall through so
250
250
  // the browser fires its default Enter at a safe text boundary.
251
- if (el && el.nodeName === 'I' && /\bfa-/.test(el.className || '')) {
251
+ if (el?.nodeName === 'I' && /\bfa-/.test(el.className || '')) {
252
252
  const nr = document.createRange();
253
253
  nr.setStartAfter(el);
254
254
  nr.collapse(true);
255
- const selI = window.getSelection();
255
+ const selI = globalThis.getSelection();
256
256
  if (selI) { selI.removeAllRanges(); selI.addRange(nr); }
257
257
  return false; // cursor is now outside <i> — let browser default handle Enter
258
258
  }
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?.closest('.an-video-wrapper');
263
263
  if (videoWrapper) {
264
264
  event.preventDefault();
265
265
  const p = document.createElement('p');
@@ -268,18 +268,18 @@ export function handleKeydown(event, editable, options = {}) {
268
268
  const nr = document.createRange();
269
269
  nr.setStart(p, 0);
270
270
  nr.collapse(true);
271
- const sel = window.getSelection();
271
+ const sel = globalThis.getSelection();
272
272
  sel.removeAllRanges();
273
273
  sel.addRange(nr);
274
274
  return true;
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?.closest('.an-checklist li');
279
279
  if (checkLi) {
280
280
  event.preventDefault();
281
281
  const ul = checkLi.closest('.an-checklist');
282
- const sel = window.getSelection();
282
+ const sel = globalThis.getSelection();
283
283
  let nativeRange = sel.getRangeAt(0);
284
284
 
285
285
  // Helper: get trimmed text content of a li, excluding the checkbox INPUT.
@@ -358,14 +358,14 @@ export function handleKeydown(event, editable, options = {}) {
358
358
  const para = closestPara(range.sc, editable);
359
359
 
360
360
  // Enter in a pre/code block: insert a literal newline instead of a new block
361
- if (para && para.nodeName.toUpperCase() === 'PRE') {
361
+ if (para?.nodeName.toUpperCase() === 'PRE') {
362
362
  event.preventDefault();
363
363
  execCommand('insertText', '\n');
364
364
  return true;
365
365
  }
366
366
 
367
367
  // Pressing Enter at the end of a blockquote should exit it
368
- if (para && para.nodeName.toUpperCase() === 'BLOCKQUOTE') {
368
+ if (para?.nodeName.toUpperCase() === 'BLOCKQUOTE') {
369
369
  const native = range.toNativeRange();
370
370
  native.setEnd(para, para.childNodes.length);
371
371
  if (native.toString() === '' && range.isCollapsed()) {
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)',
package/src/js/i18n/vi.js CHANGED
@@ -293,6 +293,8 @@ export const vi = {
293
293
  rowHeight: 'Chiều cao hàng',
294
294
  tableBorderWidth: 'Độ rộng viền bảng',
295
295
  deleteTable: 'Xóa bảng',
296
+ cellBackground: 'Màu Nền Ô',
297
+ noShading: 'Xóa Màu Nền',
296
298
  columnWidthPx: 'Chiều rộng cột (px)',
297
299
  rowHeightPx: 'Chiều cao hàng (px)',
298
300
  tableBorderWidthPx: 'Độ rộng viền bảng (px)',
package/src/js/index.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * const editor = AutumnNote.create('#my-editor');
10
10
  */
11
11
 
12
+ // @ts-ignore
12
13
  import '../styles/autumnnote.scss';
13
14
  import { Context, _customModules, _globalPlugins } from './Context.js';
14
15
  import { registerButton } from './module/Buttons.js';
@@ -49,7 +50,7 @@ const AutumnNote = {
49
50
  const elements = resolveElements(selector);
50
51
  const ctxs = elements.map((el) => {
51
52
  if (instances.has(el)) return instances.get(el);
52
- const ctx = new Context(el, options);
53
+ const ctx = new Context(/** @type {HTMLElement} */ (el), options);
53
54
  ctx.initialize();
54
55
  instances.set(el, ctx);
55
56
  return ctx;
@@ -159,7 +160,7 @@ function resolveElements(selector) {
159
160
  return [selector];
160
161
  }
161
162
  if (selector instanceof NodeList || Array.isArray(selector)) {
162
- return Array.from(selector);
163
+ return /** @type {Element[]} */ (Array.from(selector));
163
164
  }
164
165
  return [];
165
166
  }
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Activated when both `autoSave` and `autoSaveRestore` options are true.
6
6
  * On initialize it checks localStorage for a draft that is within the
7
- * `autoSaveRestoreTimeout` day window. If one is found a dismissible banner
7
+ * `autoSaveRestoreTimeout` day globalThis. If one is found a dismissible banner
8
8
  * is prepended to the editor container.
9
9
  */
10
10
 
@@ -118,9 +118,7 @@ export class AutoSaveRestore {
118
118
  }
119
119
 
120
120
  _removeBanner() {
121
- if (this._banner && this._banner.parentNode) {
122
- this._banner.parentNode.removeChild(this._banner);
123
- }
121
+ this._banner?.remove();
124
122
  this._banner = null;
125
123
  }
126
124
  }