autumnnote 1.2.1 → 1.3.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.
@@ -428,9 +428,15 @@
428
428
  execCommand("outdent");
429
429
  }
430
430
  /**
431
- * G.5 helper: splits a checklist at checkLi, converts it to a <p>,
432
- * and keeps items before/after as separate checklists.
433
- * @param {HTMLElement} checkLi
431
+ * Convert a checklist <li> into a paragraph and move any following items into a new checklist.
432
+ *
433
+ * Preserves inline markup from the converted item, strips zero-width space anchors,
434
+ * and replaces empty content with a non‑breaking space. If there are list items
435
+ * after the converted item they are moved into a new <ul class="an-checklist">
436
+ * inserted immediately after the original list. The original <li> is removed and
437
+ * the original list is removed if it becomes empty. Attempts to place the caret
438
+ * at the start of the newly created <p>.
439
+ * @param {HTMLElement} checkLi - The checklist `<li>` element to convert to a `<p>`.
434
440
  */
435
441
  function _checklistItemToP(checkLi) {
436
442
  const checkUl = checkLi.closest(".an-checklist");
@@ -439,7 +445,15 @@
439
445
  const liIndex = allLis.indexOf(checkLi);
440
446
  const afterLis = allLis.slice(liIndex + 1);
441
447
  const p = document.createElement("p");
442
- p.textContent = Array.from(checkLi.childNodes).filter((n) => !(n.nodeType === 1 && n.tagName === "INPUT")).map((n) => n.textContent).join("").replace(/\u200B/g, "").trim() || "\xA0";
448
+ for (const child of checkLi.childNodes) {
449
+ if (child.nodeType === 1 && child.tagName === "INPUT") continue;
450
+ p.appendChild(child.cloneNode(true));
451
+ }
452
+ p.innerHTML = p.innerHTML.replace(/\u200B/g, "");
453
+ if (!p.hasChildNodes() || !p.textContent.trim()) {
454
+ p.innerHTML = "";
455
+ p.appendChild(document.createTextNode("\xA0"));
456
+ }
443
457
  if (afterLis.length > 0) {
444
458
  const newUl = document.createElement("ul");
445
459
  newUl.className = "an-checklist";
@@ -470,9 +484,12 @@
470
484
  */
471
485
  var insertOrderedList = () => execCommand("insertOrderedList");
472
486
  /**
473
- * Applies a line-height value to every block-level element that intersects
474
- * the current selection.
475
- * @param {string} value - unitless multiplier, e.g. '1.5'
487
+ * Set the line-height on every block-level element that intersects the current selection.
488
+ *
489
+ * If the selection is collapsed, the nearest enclosing block element receives the style.
490
+ * For a non-collapsed selection, all unique block ancestors of text nodes that intersect the range are updated;
491
+ * if none are found, the nearest block ancestor of the range's common ancestor is updated.
492
+ * @param {string} value - Line-height value to apply; typically a unitless multiplier (for example, "1.5").
476
493
  */
477
494
  function lineHeight(value) {
478
495
  const sel = window.getSelection();
@@ -507,9 +524,9 @@
507
524
  return;
508
525
  }
509
526
  const blocks = /* @__PURE__ */ new Set();
510
- const iter = document.createNodeIterator(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, null);
527
+ const iter = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, { acceptNode: (node) => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP });
511
528
  let textNode;
512
- while (textNode = iter.nextNode()) if (range.intersectsNode(textNode)) {
529
+ while (textNode = iter.nextNode()) {
513
530
  const block = nearestBlock(textNode);
514
531
  if (block) blocks.add(block);
515
532
  }
@@ -591,9 +608,18 @@
591
608
  return !!(code && !code.closest("pre"));
592
609
  }
593
610
  /**
594
- * Toggles a task-list at the cursor.
595
- * If inside a checklist <li>, converts it (and any other selected items) back to <p> elements.
596
- * Otherwise inserts a new <ul class="an-checklist"> with one item per selected line.
611
+ * Toggle a checklist at the current selection or caret.
612
+ *
613
+ * When the selection is inside an existing checklist `<ul class="an-checklist">`,
614
+ * converts the selected `<li>` items back into `<p>` paragraphs and places the caret
615
+ * at the start of the first converted paragraph. Otherwise creates a checklist:
616
+ * - If the selection is collapsed, converts the nearest block-level ancestor (or inserts
617
+ * a single checklist item at the editable root) into a checklist with one item containing
618
+ * that block's text and places the caret inside the new item.
619
+ * - If the selection is a range, converts each intersecting block element into one checklist
620
+ * item (preserving textual content) and places the caret at the end of the last item.
621
+ *
622
+ * Empty or whitespace-only selections do not create a checklist.
597
623
  */
598
624
  function toggleChecklist() {
599
625
  const sel = window.getSelection();
@@ -652,8 +678,9 @@
652
678
  ul.appendChild(li);
653
679
  if (block && BLOCK_TAGS.has(block.tagName)) block.parentNode.replaceChild(ul, block);
654
680
  else {
655
- document.execCommand("insertHTML", false, ul.outerHTML);
656
- return;
681
+ const nativeRange = sel.getRangeAt(0);
682
+ nativeRange.deleteContents();
683
+ nativeRange.insertNode(ul);
657
684
  }
658
685
  const textNode = li.lastChild;
659
686
  const nr = document.createRange();
@@ -1332,6 +1359,7 @@
1332
1359
  replaceAriaLabel: "Replace with",
1333
1360
  replaceBtn: "Replace",
1334
1361
  replaceAllBtn: "Replace All",
1362
+ noResults: "No results",
1335
1363
  close: "×"
1336
1364
  },
1337
1365
  shortcutsDialog: {
@@ -1684,6 +1712,7 @@
1684
1712
  replaceAriaLabel: "Thay thế bằng",
1685
1713
  replaceBtn: "Thay thế",
1686
1714
  replaceAllBtn: "Thay thế tất cả",
1715
+ noResults: "Không có kết quả",
1687
1716
  close: "×"
1688
1717
  },
1689
1718
  shortcutsDialog: {
@@ -2013,6 +2042,7 @@
2013
2042
  replacePlaceholder: "置換後…",
2014
2043
  replaceAriaLabel: "置換後のテキスト",
2015
2044
  replaceBtn: "置換",
2045
+ noResults: "結果なし",
2016
2046
  replaceAllBtn: "すべて置換",
2017
2047
  close: "×"
2018
2048
  },
@@ -2343,6 +2373,7 @@
2343
2373
  replacePlaceholder: "替换为…",
2344
2374
  replaceAriaLabel: "替换为",
2345
2375
  replaceBtn: "替换",
2376
+ noResults: "没有结果",
2346
2377
  replaceAllBtn: "全部替换",
2347
2378
  close: "×"
2348
2379
  },
@@ -2673,6 +2704,7 @@
2673
2704
  replacePlaceholder: "Remplacer par…",
2674
2705
  replaceAriaLabel: "Remplacer par",
2675
2706
  replaceBtn: "Remplacer",
2707
+ noResults: "Aucun résultat",
2676
2708
  replaceAllBtn: "Tout remplacer",
2677
2709
  close: "×"
2678
2710
  },
@@ -3003,6 +3035,7 @@
3003
3035
  replacePlaceholder: "Ersetzen durch…",
3004
3036
  replaceAriaLabel: "Ersetzen durch",
3005
3037
  replaceBtn: "Ersetzen",
3038
+ noResults: "Keine Ergebnisse",
3006
3039
  replaceAllBtn: "Alle ersetzen",
3007
3040
  close: "×"
3008
3041
  },
@@ -3333,6 +3366,7 @@
3333
3366
  replacePlaceholder: "Reemplazar con…",
3334
3367
  replaceAriaLabel: "Reemplazar con",
3335
3368
  replaceBtn: "Reemplazar",
3369
+ noResults: "Sin resultados",
3336
3370
  replaceAllBtn: "Reemplazar todo",
3337
3371
  close: "×"
3338
3372
  },
@@ -3663,6 +3697,7 @@
3663
3697
  replacePlaceholder: "바꿀 내용…",
3664
3698
  replaceAriaLabel: "바꿀 내용",
3665
3699
  replaceBtn: "바꾸기",
3700
+ noResults: "결과 없음",
3666
3701
  replaceAllBtn: "모두 바꾸기",
3667
3702
  close: "×"
3668
3703
  },
@@ -3883,8 +3918,10 @@
3883
3918
  "object",
3884
3919
  "embed",
3885
3920
  "form",
3886
- "button"
3921
+ "base"
3887
3922
  ];
3923
+ /** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
3924
+ var UNWRAP_TAGS = new Set(["button"]);
3888
3925
  /** Attributes whose values must be sanitised as URLs. */
3889
3926
  var URL_ATTRS = [
3890
3927
  "href",
@@ -3902,60 +3939,61 @@
3902
3939
  "player.vimeo.com"
3903
3940
  ]);
3904
3941
  /**
3905
- * Sanitises an HTML string by removing dangerous elements and attributes.
3906
- * Uses DOMParser so the sanitisation follows normal browser parsing rules —
3907
- * no regex shortcuts that can be bypassed by encoding tricks.
3942
+ * Produce a sanitized HTML string with dangerous elements and attributes removed.
3908
3943
  *
3909
- * - Strips PROHIBITED_TAGS (script, style, iframe, object, embed, form, button)
3910
- * - Allows input[type="checkbox"] only inside ul.an-checklist li; removes all other <input>
3911
- * - Removes all on* event-handler attributes
3912
- * - Rejects javascript: and vbscript: URLs in URL attributes
3913
- * - Rejects data: URIs everywhere except img[src] (base64 uploads)
3944
+ * Removes disallowed tags and wrappers, strips event-handler attributes, rejects
3945
+ * `javascript:`/`vbscript:` URLs and most `data:` URIs, restricts iframe `src`
3946
+ * to trusted hosts when enabled, and permits only checklist checkboxes as inputs.
3914
3947
  *
3915
- * @param {string} html
3916
- * @returns {string}
3948
+ * @param {string} html - HTML fragment to sanitize.
3949
+ * @param {Object} [options]
3950
+ * @param {boolean} [options.allowIframes=false] - If true, `iframe` elements are not removed but their `src` is restricted to trusted hosts and `srcdoc` is removed.
3951
+ * @returns {string} The sanitized HTML fragment.
3917
3952
  */
3918
3953
  function sanitiseHTML(html, { allowIframes = false } = {}) {
3919
3954
  const doc = new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html");
3920
- (allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS).forEach((tag) => {
3921
- doc.querySelectorAll(tag).forEach((el) => el.remove());
3922
- });
3923
- doc.querySelectorAll("*").forEach((el) => {
3924
- Array.from(el.attributes).forEach((attr) => {
3955
+ const allElements = Array.from(doc.querySelectorAll("*"));
3956
+ const prohibited = new Set(allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS);
3957
+ for (const el of allElements) {
3958
+ const tag = el.tagName.toLowerCase();
3959
+ if (UNWRAP_TAGS.has(tag)) {
3960
+ el.replaceWith(...el.childNodes);
3961
+ continue;
3962
+ }
3963
+ if (prohibited.has(tag)) {
3964
+ el.remove();
3965
+ continue;
3966
+ }
3967
+ for (const attr of Array.from(el.attributes)) {
3925
3968
  if (attr.name.startsWith("on")) {
3926
3969
  el.removeAttribute(attr.name);
3927
- return;
3970
+ continue;
3928
3971
  }
3929
3972
  if (URL_ATTRS.includes(attr.name)) {
3930
3973
  const val = attr.value.trim();
3931
3974
  if (/^(javascript|vbscript):/i.test(val)) {
3932
3975
  el.removeAttribute(attr.name);
3933
- return;
3976
+ continue;
3934
3977
  }
3935
3978
  if (/^data:/i.test(val) && !(attr.name === "src" && el.tagName === "IMG")) el.removeAttribute(attr.name);
3936
3979
  }
3937
3980
  if (el.tagName === "IFRAME") {
3938
3981
  if (attr.name === "srcdoc") {
3939
3982
  el.removeAttribute(attr.name);
3940
- return;
3941
- }
3942
- if (attr.name === "src") {
3943
- if (!isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
3944
- return;
3983
+ continue;
3945
3984
  }
3985
+ if (attr.name === "src" && !isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
3946
3986
  }
3947
- });
3948
- });
3949
- doc.querySelectorAll("input").forEach((el) => {
3950
- if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
3951
- else Array.from(el.attributes).forEach((attr) => {
3952
- if (![
3987
+ }
3988
+ if (tag === "input") {
3989
+ if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
3990
+ else for (const attr of Array.from(el.attributes)) if (![
3953
3991
  "type",
3954
3992
  "checked",
3955
3993
  "contenteditable"
3956
3994
  ].includes(attr.name)) el.removeAttribute(attr.name);
3957
- });
3958
- });
3995
+ }
3996
+ }
3959
3997
  return doc.body.innerHTML;
3960
3998
  }
3961
3999
  /**
@@ -4156,7 +4194,18 @@
4156
4194
  const sel = window.getSelection();
4157
4195
  sel.removeAllRanges();
4158
4196
  sel.addRange(range);
4159
- } catch (_) {}
4197
+ } catch (_) {
4198
+ try {
4199
+ const fb = document.createRange();
4200
+ fb.setStart(this.editable, 0);
4201
+ fb.collapse(true);
4202
+ const s = window.getSelection();
4203
+ if (s) {
4204
+ s.removeAllRanges();
4205
+ s.addRange(fb);
4206
+ }
4207
+ } catch (_2) {}
4208
+ }
4160
4209
  }
4161
4210
  _savePoint() {
4162
4211
  if (this.stackOffset < this.stack.length - 1) this.stack = this.stack.slice(0, this.stackOffset + 1);
@@ -4183,6 +4232,10 @@
4183
4232
  * @returns {{ html: string, images: Object<string,string> }}
4184
4233
  */
4185
4234
  _tokenizeImages(html) {
4235
+ if (!html.includes("data:")) return {
4236
+ html,
4237
+ images: {}
4238
+ };
4186
4239
  const images = {};
4187
4240
  let index = 0;
4188
4241
  return {
@@ -4254,11 +4307,12 @@
4254
4307
  * Inspired by Summernote's table handling
4255
4308
  */
4256
4309
  /**
4257
- * Creates a table element with the specified dimensions.
4258
- * @param {number} cols
4259
- * @param {number} rows
4260
- * @param {{ headerRow?: boolean }} [opts]
4261
- * @returns {HTMLTableElement}
4310
+ * Build an HTML table with the given number of columns and rows, optionally including a header row.
4311
+ * @param {number} cols - Number of columns in each row.
4312
+ * @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
4313
+ * @param {{ headerRow?: boolean }} [opts] - Options object.
4314
+ * @param {boolean} [opts.headerRow=false] - When true and `rows > 0`, creates a header row (`<thead>`) plus body rows for the remainder.
4315
+ * @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
4262
4316
  */
4263
4317
  function createTable(cols, rows, opts = {}) {
4264
4318
  const { headerRow = false } = opts;
@@ -4267,7 +4321,7 @@
4267
4321
  const thead = createElement("thead");
4268
4322
  const tr = createElement("tr");
4269
4323
  for (let c = 0; c < cols; c++) {
4270
- const th = createElement("th", {}, ["\xA0"]);
4324
+ const th = createElement("th", {}, [document.createElement("br")]);
4271
4325
  tr.appendChild(th);
4272
4326
  }
4273
4327
  thead.appendChild(tr);
@@ -4279,7 +4333,7 @@
4279
4333
  for (let r = 0; r < bodyRows; r++) {
4280
4334
  const tr = createElement("tr");
4281
4335
  for (let c = 0; c < cols; c++) {
4282
- const td = createElement("td", {}, ["\xA0"]);
4336
+ const td = createElement("td", {}, [document.createElement("br")]);
4283
4337
  tr.appendChild(td);
4284
4338
  }
4285
4339
  tbody.appendChild(tr);
@@ -4287,13 +4341,52 @@
4287
4341
  return table;
4288
4342
  }
4289
4343
  /**
4290
- * Inserts a table at the current cursor position.
4291
- * @param {number} cols
4292
- * @param {number} rows
4293
- * @param {{ headerRow?: boolean }} [opts]
4344
+ * Insert a table at the current selection and place the caret into its first cell.
4345
+ * @param {number} cols - Number of columns for the new table.
4346
+ * @param {number} rows - Number of rows for the new table.
4347
+ * @param {{ headerRow?: boolean }} [opts] - Options for table creation.
4348
+ * @param {boolean} [opts.headerRow=false] - If true, include a header row as the first row.
4294
4349
  */
4295
4350
  function insertTable(cols, rows, opts = {}) {
4296
- execCommand("insertHTML", createTable(cols, rows, opts).outerHTML);
4351
+ if (cols <= 0 || rows <= 0) return;
4352
+ const table = createTable(cols, rows, opts);
4353
+ const sel = window.getSelection();
4354
+ if (!sel || sel.rangeCount === 0) return;
4355
+ const range = sel.getRangeAt(0);
4356
+ range.deleteContents();
4357
+ const BLOCK = new Set([
4358
+ "P",
4359
+ "DIV",
4360
+ "H1",
4361
+ "H2",
4362
+ "H3",
4363
+ "H4",
4364
+ "H5",
4365
+ "H6",
4366
+ "BLOCKQUOTE",
4367
+ "LI",
4368
+ "PRE"
4369
+ ]);
4370
+ let anchor = range.startContainer;
4371
+ if (anchor.nodeType === 3) anchor = anchor.parentElement;
4372
+ while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) anchor = anchor.parentElement;
4373
+ if (anchor && BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentNode) {
4374
+ anchor.after(table);
4375
+ if (!table.nextElementSibling) {
4376
+ const p = document.createElement("p");
4377
+ p.appendChild(document.createElement("br"));
4378
+ table.after(p);
4379
+ }
4380
+ if (!anchor.textContent.trim() && !anchor.querySelector("img, video, table")) anchor.remove();
4381
+ } else range.insertNode(table);
4382
+ const firstCell = table.querySelector("td, th");
4383
+ if (firstCell) {
4384
+ const nr = document.createRange();
4385
+ nr.setStart(firstCell, 0);
4386
+ nr.collapse(true);
4387
+ sel.removeAllRanges();
4388
+ sel.addRange(nr);
4389
+ }
4297
4390
  }
4298
4391
  //#endregion
4299
4392
  //#region src/js/core/key.js
@@ -4508,7 +4601,7 @@
4508
4601
  if (para && para.nodeName.toUpperCase() === "PRE") {
4509
4602
  if (event.shiftKey) return false;
4510
4603
  event.preventDefault();
4511
- execCommand("insertText", " ");
4604
+ execCommand("insertText", " ".repeat(options.tabSize || 4));
4512
4605
  return true;
4513
4606
  }
4514
4607
  if (options.tabSize) {
@@ -4601,6 +4694,11 @@
4601
4694
  return true;
4602
4695
  }
4603
4696
  const para = closestPara(range.sc, editable);
4697
+ if (para && para.nodeName.toUpperCase() === "PRE") {
4698
+ event.preventDefault();
4699
+ execCommand("insertText", "\n");
4700
+ return true;
4701
+ }
4604
4702
  if (para && para.nodeName.toUpperCase() === "BLOCKQUOTE") {
4605
4703
  const native = range.toNativeRange();
4606
4704
  native.setEnd(para, para.childNodes.length);
@@ -4634,11 +4732,22 @@
4634
4732
  function htmlToMarkdown(html) {
4635
4733
  return _domToMd(new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html").body).replace(/\n{3,}/g, "\n\n").trim();
4636
4734
  }
4637
- function _domToMd(node) {
4735
+ /**
4736
+ * Convert a DOM node subtree into Markdown.
4737
+ *
4738
+ * Recursively produces a Markdown string representing the given DOM node and its descendants,
4739
+ * handling common HTML constructs such as paragraphs, headings, lists (with nested indentation),
4740
+ * blockquotes, fenced and inline code, links, images, tables, horizontal rules, and basic inline emphasis.
4741
+ *
4742
+ * @param {Node} node - The DOM node to convert.
4743
+ * @param {number} [depth=0] - Current nesting depth used to indent nested list items.
4744
+ * @returns {string} The Markdown representation of the node subtree.
4745
+ */
4746
+ function _domToMd(node, depth = 0) {
4638
4747
  if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
4639
4748
  if (node.nodeType !== 1) return "";
4640
4749
  const tag = node.nodeName.toLowerCase();
4641
- const inner = () => Array.from(node.childNodes).map(_domToMd).join("");
4750
+ const inner = () => Array.from(node.childNodes).map((n) => _domToMd(n, depth)).join("");
4642
4751
  switch (tag) {
4643
4752
  case "p":
4644
4753
  case "div": return `\n\n${inner()}\n\n`;
@@ -4678,12 +4787,16 @@
4678
4787
  case "ul": {
4679
4788
  const items = Array.from(node.querySelectorAll(":scope > li"));
4680
4789
  if (!items.length) return inner();
4681
- return `\n\n${items.map((li) => `- ${_domToMd(li).trim()}`).join("\n")}\n\n`;
4790
+ const indent = " ".repeat(depth);
4791
+ const lines = items.map((li) => `${indent}- ${_domToMd(li, depth + 1).trim()}`).join("\n");
4792
+ return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
4682
4793
  }
4683
4794
  case "ol": {
4684
4795
  const items = Array.from(node.querySelectorAll(":scope > li"));
4685
4796
  if (!items.length) return inner();
4686
- return `\n\n${items.map((li, i) => `${i + 1}. ${_domToMd(li).trim()}`).join("\n")}\n\n`;
4797
+ const indent = " ".repeat(depth);
4798
+ const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
4799
+ return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
4687
4800
  }
4688
4801
  case "li": return inner();
4689
4802
  case "hr": return "\n\n---\n\n";
@@ -4707,12 +4820,15 @@
4707
4820
  }
4708
4821
  }
4709
4822
  /**
4710
- * Returns true if the text contains recognisable Markdown patterns.
4711
- * @param {string} text
4712
- * @returns {boolean}
4823
+ * Detects whether a string likely contains Markdown syntax.
4824
+ *
4825
+ * Checks for common Markdown constructs such as ATX headings, unordered or
4826
+ * ordered list items, blockquotes, fenced code blocks, and bold emphasis.
4827
+ * @param {string} text - Input text to inspect for Markdown patterns.
4828
+ * @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
4713
4829
  */
4714
4830
  function isMarkdown(text) {
4715
- return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|\*{2}.+?\*{2}|^```/m.test(text);
4831
+ return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|^```|^\*{2}.+?\*{2}/m.test(text);
4716
4832
  }
4717
4833
  /**
4718
4834
  * Converts a Markdown string to an HTML string.
@@ -5475,6 +5591,8 @@
5475
5591
  this._disposers = [];
5476
5592
  /** @type {Array<() => void>} closers for all open color picker popups */
5477
5593
  this._colorPickerClosers = [];
5594
+ /** @type {number|null} rAF handle for debounced refresh */
5595
+ this._refreshRaf = null;
5478
5596
  }
5479
5597
  initialize() {
5480
5598
  this.el = createElement("div", { class: "an-toolbar" });
@@ -5484,6 +5602,8 @@
5484
5602
  return this;
5485
5603
  }
5486
5604
  destroy() {
5605
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
5606
+ this._refreshRaf = null;
5487
5607
  this._disposers.forEach((d) => d());
5488
5608
  this._disposers = [];
5489
5609
  if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
@@ -5879,6 +5999,13 @@
5879
5999
  return _faPageLevelReady;
5880
6000
  }
5881
6001
  refresh() {
6002
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
6003
+ this._refreshRaf = requestAnimationFrame(() => {
6004
+ this._refreshRaf = null;
6005
+ this._doRefresh();
6006
+ });
6007
+ }
6008
+ _doRefresh() {
5882
6009
  if (!this.el) return;
5883
6010
  const btnMap = this._btnMap || /* @__PURE__ */ new Map();
5884
6011
  this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
@@ -6056,7 +6183,7 @@
6056
6183
  }
6057
6184
  update() {
6058
6185
  if (!this._wordCountEl || !this._charCountEl) return;
6059
- const text = this.context.layoutInfo.editable.innerText || "";
6186
+ const text = this.context.layoutInfo.editable.textContent || "";
6060
6187
  const words = _countWords(text);
6061
6188
  const chars = text.replace(/\n/g, "").length;
6062
6189
  const maxWords = this.options.maxWords || 0;
@@ -6251,7 +6378,7 @@
6251
6378
  event.preventDefault();
6252
6379
  const raw = clipboardData.getData("text/html");
6253
6380
  const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
6254
- const isSocialContent = /\bdata-testid\b/.test(raw) || /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
6381
+ const isSocialContent = /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
6255
6382
  let html = raw;
6256
6383
  if (isWordContent) html = this._cleanWordHtml(html);
6257
6384
  else if (isSocialContent) html = this._cleanSocialHtml(html);
@@ -6353,7 +6480,7 @@
6353
6480
  */
6354
6481
  _dataUrlToBlob(dataUrl) {
6355
6482
  const [header, b64] = dataUrl.split(",");
6356
- const mime = header.match(/:(.*?);/)[1];
6483
+ const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
6357
6484
  const binary = atob(b64);
6358
6485
  const arr = new Uint8Array(binary.length);
6359
6486
  for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
@@ -7359,6 +7486,7 @@
7359
7486
  _select(img) {
7360
7487
  if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
7361
7488
  this._activeImg = img;
7489
+ this._lastOverlayPos = null;
7362
7490
  img.classList.add("an-image-selected");
7363
7491
  this._updateOverlayPosition();
7364
7492
  this._overlay.style.display = "block";
@@ -7382,6 +7510,14 @@
7382
7510
  const offsetParent = this._overlay.offsetParent || this._container;
7383
7511
  const containerRect = offsetParent.getBoundingClientRect();
7384
7512
  const rect = this._activeImg.getBoundingClientRect();
7513
+ const p = this._lastOverlayPos;
7514
+ if (p && p.l === rect.left && p.t === rect.top && p.w === rect.width && p.h === rect.height) return;
7515
+ this._lastOverlayPos = {
7516
+ l: rect.left,
7517
+ t: rect.top,
7518
+ w: rect.width,
7519
+ h: rect.height
7520
+ };
7385
7521
  const left = rect.left - containerRect.left + offsetParent.scrollLeft;
7386
7522
  const top = rect.top - containerRect.top + offsetParent.scrollTop;
7387
7523
  this._overlay.style.left = `${left}px`;
@@ -13080,7 +13216,7 @@
13080
13216
  cells.forEach((cell) => {
13081
13217
  cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
13082
13218
  });
13083
- labelEl.textContent = rows && cols ? `${cols} × ${rows}` : this.context.locale.contextMenu.table || "Insert Table";
13219
+ labelEl.textContent = rows && cols ? `${rows} × ${cols}` : this.context.locale.contextMenu.table || "Insert Table";
13084
13220
  };
13085
13221
  panel.appendChild(gridEl);
13086
13222
  panel.appendChild(labelEl);
@@ -13720,7 +13856,6 @@
13720
13856
  if (!editable) return;
13721
13857
  const rawMatches = this._findRawMatches(query, editable);
13722
13858
  if (rawMatches.length === 0) return;
13723
- this._currentIndex = 0;
13724
13859
  for (let i = rawMatches.length - 1; i >= 0; i--) {
13725
13860
  const { node, start, end } = rawMatches[i];
13726
13861
  try {
@@ -13737,6 +13872,7 @@
13737
13872
  }
13738
13873
  this._matches.reverse();
13739
13874
  this._matches = this._matches.filter((m) => m.mark);
13875
+ this._currentIndex = 0;
13740
13876
  if (this._matches.length === 0) return;
13741
13877
  if (this._matches[0] && this._matches[0].mark) {
13742
13878
  this._matches[0].mark.className = "an-highlight an-highlight-current";
@@ -13762,12 +13898,13 @@
13762
13898
  this._lastCaseSensitive = this._caseSensitive;
13763
13899
  }
13764
13900
  const re = this._queryRegex;
13901
+ const MAX_RESULTS = 500;
13765
13902
  const walker = document.createTreeWalker(root, 4);
13766
13903
  let node;
13767
- while (node = walker.nextNode()) {
13904
+ while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
13768
13905
  re.lastIndex = 0;
13769
13906
  let m;
13770
- while ((m = re.exec(node.textContent)) !== null) results.push({
13907
+ while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
13771
13908
  node,
13772
13909
  start: m.index,
13773
13910
  end: m.index + m[0].length
@@ -13854,7 +13991,7 @@
13854
13991
  const total = this._matches.length;
13855
13992
  if (total === 0) {
13856
13993
  const query = this._findInput ? this._findInput.value : "";
13857
- this._counterEl.textContent = query ? "No results" : "";
13994
+ this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
13858
13995
  } else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
13859
13996
  }
13860
13997
  };
@@ -14053,17 +14190,33 @@
14053
14190
  e.preventDefault();
14054
14191
  e.stopPropagation();
14055
14192
  this._startHandleDrag(e, id);
14056
- }));
14193
+ }), on(h, "touchstart", (e) => {
14194
+ e.preventDefault();
14195
+ e.stopPropagation();
14196
+ this._startHandleDrag({
14197
+ clientX: e.touches[0].clientX,
14198
+ clientY: e.touches[0].clientY
14199
+ }, id);
14200
+ }, { passive: false }));
14057
14201
  this._handles[id] = h;
14058
14202
  cropBox.appendChild(h);
14059
14203
  });
14060
14204
  this._disposers.push(on(cropBox, "mousedown", (e) => {
14061
- if (e.target !== cropBox && e.target !== grid && !(e.target.tagName === "DIV" && !e.target.className.includes("handle"))) return;
14062
- if (e.target.className && e.target.className.includes("an-crop-handle")) return;
14205
+ if (e.target.classList.contains("an-crop-handle")) return;
14206
+ if (e.target !== cropBox && e.target !== grid) return;
14063
14207
  e.preventDefault();
14064
14208
  e.stopPropagation();
14065
14209
  this._startBoxMove(e);
14066
- }));
14210
+ }), on(cropBox, "touchstart", (e) => {
14211
+ if (e.target.classList.contains("an-crop-handle")) return;
14212
+ if (e.target !== cropBox && e.target !== grid) return;
14213
+ e.preventDefault();
14214
+ e.stopPropagation();
14215
+ this._startBoxMove({
14216
+ clientX: e.touches[0].clientX,
14217
+ clientY: e.touches[0].clientY
14218
+ });
14219
+ }, { passive: false }));
14067
14220
  const infoEl = document.createElement("div");
14068
14221
  infoEl.className = "an-crop-info";
14069
14222
  infoEl.style.cssText = `
@@ -14221,15 +14374,26 @@
14221
14374
  * @param {(e: MouseEvent) => void} onMove
14222
14375
  */
14223
14376
  _attachDocDrag(onMove) {
14377
+ const onTouchMove = (e) => {
14378
+ e.preventDefault();
14379
+ onMove({
14380
+ clientX: e.touches[0].clientX,
14381
+ clientY: e.touches[0].clientY
14382
+ });
14383
+ };
14224
14384
  const cleanup = () => {
14225
14385
  document.removeEventListener("mousemove", onMove);
14226
14386
  document.removeEventListener("mouseup", cleanup);
14387
+ document.removeEventListener("touchmove", onTouchMove);
14388
+ document.removeEventListener("touchend", cleanup);
14227
14389
  document.body.style.userSelect = "";
14228
14390
  document.body.style.cursor = "";
14229
14391
  };
14230
14392
  document.body.style.userSelect = "none";
14231
14393
  document.addEventListener("mousemove", onMove);
14232
14394
  document.addEventListener("mouseup", cleanup, { once: true });
14395
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
14396
+ document.addEventListener("touchend", cleanup, { once: true });
14233
14397
  }
14234
14398
  _bindEsc() {
14235
14399
  const handler = (e) => {
@@ -14270,7 +14434,7 @@
14270
14434
  height: natH
14271
14435
  }, w, h);
14272
14436
  if (!canvas) {
14273
- window.alert("Cannot crop this image: the image server does not allow cross-origin access.\nUpload the image directly to use the crop tool.");
14437
+ this._showCropError("Cannot crop this image: the image server does not allow cross-origin access. Upload the image directly to use the crop tool.");
14274
14438
  this._close(false);
14275
14439
  return;
14276
14440
  }
@@ -14287,6 +14451,37 @@
14287
14451
  this.context.invoke("imageResizer.updateOverlay");
14288
14452
  }
14289
14453
  /**
14454
+ * Show a non-blocking inline error banner appended to document.body.
14455
+ * Auto-dismisses after 4 seconds.
14456
+ * @param {string} msg
14457
+ */
14458
+ _showCropError(msg) {
14459
+ const banner = document.createElement("div");
14460
+ banner.setAttribute("role", "alert");
14461
+ banner.style.cssText = [
14462
+ "position:fixed",
14463
+ "bottom:24px",
14464
+ "left:50%",
14465
+ "transform:translateX(-50%)",
14466
+ "z-index:10200",
14467
+ "max-width:420px",
14468
+ "width:max-content",
14469
+ "background:#7f1d1d",
14470
+ "color:#fecaca",
14471
+ "border:1px solid #b91c1c",
14472
+ "border-radius:8px",
14473
+ "padding:12px 18px",
14474
+ "font:13px/1.5 system-ui,sans-serif",
14475
+ "box-shadow:0 4px 16px rgba(0,0,0,.4)",
14476
+ "pointer-events:auto"
14477
+ ].join(";");
14478
+ banner.textContent = msg;
14479
+ document.body.appendChild(banner);
14480
+ setTimeout(() => {
14481
+ if (banner.parentNode) banner.parentNode.removeChild(banner);
14482
+ }, 4e3);
14483
+ }
14484
+ /**
14290
14485
  * Remove all overlay DOM elements and reset state.
14291
14486
  * @param {boolean} _committed - reserved for future use
14292
14487
  */
@@ -14701,6 +14896,16 @@
14701
14896
  if (!editable) return;
14702
14897
  editable.focus();
14703
14898
  document.execCommand("removeFormat");
14899
+ const sel = window.getSelection();
14900
+ if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
14901
+ const range = sel.getRangeAt(0);
14902
+ const ancestor = range.commonAncestorContainer;
14903
+ const root = ancestor.nodeType === 1 ? ancestor : ancestor.parentElement;
14904
+ if (root) {
14905
+ const candidates = [root, ...root.querySelectorAll("[style]")];
14906
+ for (const el of candidates) if (el.hasAttribute("style") && range.intersectsNode(el)) el.removeAttribute("style");
14907
+ }
14908
+ }
14704
14909
  ctx.invoke("editor.afterCommand");
14705
14910
  },
14706
14911
  inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
@@ -14814,6 +15019,7 @@
14814
15019
  }
14815
15020
  document.body.appendChild(el);
14816
15021
  this._el = el;
15022
+ this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
14817
15023
  }
14818
15024
  _buildColorPicker() {
14819
15025
  const picker = document.createElement("div");
@@ -14899,8 +15105,13 @@
14899
15105
  editable.focus();
14900
15106
  const sel = window.getSelection();
14901
15107
  sel.removeAllRanges();
14902
- sel.addRange(this._savedRange.cloneRange());
14903
- document.execCommand(type, false, color);
15108
+ try {
15109
+ sel.addRange(this._savedRange.cloneRange());
15110
+ } catch (_) {
15111
+ return;
15112
+ }
15113
+ const cmd = type === "hiliteColor" ? "hiliteColor" : type;
15114
+ if (!document.execCommand(cmd, false, color) && cmd === "hiliteColor") document.execCommand("backColor", false, color);
14904
15115
  this.context.invoke("editor.afterCommand");
14905
15116
  const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
14906
15117
  const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
@@ -14935,8 +15146,8 @@
14935
15146
  this._closeColorPicker();
14936
15147
  }
14937
15148
  _syncActive() {
14938
- if (!this._el) return;
14939
- this._el.querySelectorAll(".an-bubble-btn").forEach((btn) => {
15149
+ if (!this._btnCache) return;
15150
+ this._btnCache.forEach((btn) => {
14940
15151
  const activeFn = _ACTIVE[btn.dataset.name];
14941
15152
  btn.classList.toggle("an-active", !!(activeFn && activeFn()));
14942
15153
  });
@@ -15083,14 +15294,23 @@
15083
15294
  const el = document.createElement("div");
15084
15295
  el.className = "an-mention-dropdown";
15085
15296
  el.setAttribute("role", "listbox");
15297
+ el.addEventListener("mousedown", (e) => e.preventDefault());
15298
+ el.addEventListener("click", (e) => {
15299
+ const item = e.target.closest(".an-mention-item");
15300
+ if (item) this._select(+item.dataset.index);
15301
+ });
15302
+ el.addEventListener("mousemove", (e) => {
15303
+ const item = e.target.closest(".an-mention-item");
15304
+ if (item) this._highlightItem(+item.dataset.index);
15305
+ });
15086
15306
  document.body.appendChild(el);
15087
15307
  this._dropdown = el;
15088
15308
  }
15089
15309
  _renderItems(items) {
15090
15310
  const dd = this._dropdown;
15091
- dd.innerHTML = "";
15092
15311
  this._items = items.slice(0, this._cfg.maxResults);
15093
15312
  this._activeIndex = this._items.length > 0 ? 0 : -1;
15313
+ const frag = document.createDocumentFragment();
15094
15314
  this._items.forEach((item, i) => {
15095
15315
  const li = document.createElement("div");
15096
15316
  li.className = "an-mention-item";
@@ -15106,10 +15326,10 @@
15106
15326
  const label = document.createElement("span");
15107
15327
  label.textContent = item.label;
15108
15328
  li.appendChild(label);
15109
- li.addEventListener("mousedown", (e) => e.preventDefault());
15110
- li.addEventListener("click", () => this._select(i));
15111
- dd.appendChild(li);
15329
+ frag.appendChild(li);
15112
15330
  });
15331
+ dd.innerHTML = "";
15332
+ dd.appendChild(frag);
15113
15333
  this._highlightItem(this._activeIndex);
15114
15334
  }
15115
15335
  _highlightItem(index) {