autumnnote 1.2.0 → 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);
@@ -5558,6 +5678,7 @@
5558
5678
  const openPopup = () => {
5559
5679
  isOpen = true;
5560
5680
  const rect = btn.getBoundingClientRect();
5681
+ popup.style.visibility = "hidden";
5561
5682
  popup.style.display = "block";
5562
5683
  const pw = popup.offsetWidth;
5563
5684
  const ph = popup.offsetHeight;
@@ -5567,13 +5688,12 @@
5567
5688
  if (top + ph > window.innerHeight - 8) top = rect.top - ph - 4;
5568
5689
  popup.style.left = `${left}px`;
5569
5690
  popup.style.top = `${top}px`;
5691
+ popup.style.visibility = "";
5570
5692
  btn.setAttribute("aria-expanded", "true");
5571
5693
  };
5572
5694
  const closePopup = () => {
5573
5695
  isOpen = false;
5574
5696
  popup.style.display = "none";
5575
- popup.style.top = "";
5576
- popup.style.left = "";
5577
5697
  btn.setAttribute("aria-expanded", "false");
5578
5698
  setHighlight(0, 0);
5579
5699
  };
@@ -5600,9 +5720,11 @@
5600
5720
  const d5 = on(document, "click", () => {
5601
5721
  if (isOpen) closePopup();
5602
5722
  });
5603
- this._disposers.push(d1, d2, d3, d4, d5);
5723
+ this._disposers.push(d1, d2, d3, d4, d5, () => {
5724
+ if (popup.parentNode) popup.parentNode.removeChild(popup);
5725
+ });
5604
5726
  wrap.appendChild(btn);
5605
- wrap.appendChild(popup);
5727
+ document.body.appendChild(popup);
5606
5728
  return wrap;
5607
5729
  }
5608
5730
  /**
@@ -5877,6 +5999,13 @@
5877
5999
  return _faPageLevelReady;
5878
6000
  }
5879
6001
  refresh() {
6002
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
6003
+ this._refreshRaf = requestAnimationFrame(() => {
6004
+ this._refreshRaf = null;
6005
+ this._doRefresh();
6006
+ });
6007
+ }
6008
+ _doRefresh() {
5880
6009
  if (!this.el) return;
5881
6010
  const btnMap = this._btnMap || /* @__PURE__ */ new Map();
5882
6011
  this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
@@ -6054,7 +6183,7 @@
6054
6183
  }
6055
6184
  update() {
6056
6185
  if (!this._wordCountEl || !this._charCountEl) return;
6057
- const text = this.context.layoutInfo.editable.innerText || "";
6186
+ const text = this.context.layoutInfo.editable.textContent || "";
6058
6187
  const words = _countWords(text);
6059
6188
  const chars = text.replace(/\n/g, "").length;
6060
6189
  const maxWords = this.options.maxWords || 0;
@@ -6249,7 +6378,7 @@
6249
6378
  event.preventDefault();
6250
6379
  const raw = clipboardData.getData("text/html");
6251
6380
  const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
6252
- 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);
6253
6382
  let html = raw;
6254
6383
  if (isWordContent) html = this._cleanWordHtml(html);
6255
6384
  else if (isSocialContent) html = this._cleanSocialHtml(html);
@@ -6351,7 +6480,7 @@
6351
6480
  */
6352
6481
  _dataUrlToBlob(dataUrl) {
6353
6482
  const [header, b64] = dataUrl.split(",");
6354
- const mime = header.match(/:(.*?);/)[1];
6483
+ const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
6355
6484
  const binary = atob(b64);
6356
6485
  const arr = new Uint8Array(binary.length);
6357
6486
  for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
@@ -7357,6 +7486,7 @@
7357
7486
  _select(img) {
7358
7487
  if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
7359
7488
  this._activeImg = img;
7489
+ this._lastOverlayPos = null;
7360
7490
  img.classList.add("an-image-selected");
7361
7491
  this._updateOverlayPosition();
7362
7492
  this._overlay.style.display = "block";
@@ -7380,6 +7510,14 @@
7380
7510
  const offsetParent = this._overlay.offsetParent || this._container;
7381
7511
  const containerRect = offsetParent.getBoundingClientRect();
7382
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
+ };
7383
7521
  const left = rect.left - containerRect.left + offsetParent.scrollLeft;
7384
7522
  const top = rect.top - containerRect.top + offsetParent.scrollTop;
7385
7523
  this._overlay.style.left = `${left}px`;
@@ -13078,7 +13216,7 @@
13078
13216
  cells.forEach((cell) => {
13079
13217
  cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
13080
13218
  });
13081
- 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";
13082
13220
  };
13083
13221
  panel.appendChild(gridEl);
13084
13222
  panel.appendChild(labelEl);
@@ -13718,7 +13856,6 @@
13718
13856
  if (!editable) return;
13719
13857
  const rawMatches = this._findRawMatches(query, editable);
13720
13858
  if (rawMatches.length === 0) return;
13721
- this._currentIndex = 0;
13722
13859
  for (let i = rawMatches.length - 1; i >= 0; i--) {
13723
13860
  const { node, start, end } = rawMatches[i];
13724
13861
  try {
@@ -13735,6 +13872,7 @@
13735
13872
  }
13736
13873
  this._matches.reverse();
13737
13874
  this._matches = this._matches.filter((m) => m.mark);
13875
+ this._currentIndex = 0;
13738
13876
  if (this._matches.length === 0) return;
13739
13877
  if (this._matches[0] && this._matches[0].mark) {
13740
13878
  this._matches[0].mark.className = "an-highlight an-highlight-current";
@@ -13760,12 +13898,13 @@
13760
13898
  this._lastCaseSensitive = this._caseSensitive;
13761
13899
  }
13762
13900
  const re = this._queryRegex;
13901
+ const MAX_RESULTS = 500;
13763
13902
  const walker = document.createTreeWalker(root, 4);
13764
13903
  let node;
13765
- while (node = walker.nextNode()) {
13904
+ while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
13766
13905
  re.lastIndex = 0;
13767
13906
  let m;
13768
- while ((m = re.exec(node.textContent)) !== null) results.push({
13907
+ while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
13769
13908
  node,
13770
13909
  start: m.index,
13771
13910
  end: m.index + m[0].length
@@ -13852,7 +13991,7 @@
13852
13991
  const total = this._matches.length;
13853
13992
  if (total === 0) {
13854
13993
  const query = this._findInput ? this._findInput.value : "";
13855
- this._counterEl.textContent = query ? "No results" : "";
13994
+ this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
13856
13995
  } else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
13857
13996
  }
13858
13997
  };
@@ -14051,17 +14190,33 @@
14051
14190
  e.preventDefault();
14052
14191
  e.stopPropagation();
14053
14192
  this._startHandleDrag(e, id);
14054
- }));
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 }));
14055
14201
  this._handles[id] = h;
14056
14202
  cropBox.appendChild(h);
14057
14203
  });
14058
14204
  this._disposers.push(on(cropBox, "mousedown", (e) => {
14059
- if (e.target !== cropBox && e.target !== grid && !(e.target.tagName === "DIV" && !e.target.className.includes("handle"))) return;
14060
- 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;
14061
14207
  e.preventDefault();
14062
14208
  e.stopPropagation();
14063
14209
  this._startBoxMove(e);
14064
- }));
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 }));
14065
14220
  const infoEl = document.createElement("div");
14066
14221
  infoEl.className = "an-crop-info";
14067
14222
  infoEl.style.cssText = `
@@ -14219,15 +14374,26 @@
14219
14374
  * @param {(e: MouseEvent) => void} onMove
14220
14375
  */
14221
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
+ };
14222
14384
  const cleanup = () => {
14223
14385
  document.removeEventListener("mousemove", onMove);
14224
14386
  document.removeEventListener("mouseup", cleanup);
14387
+ document.removeEventListener("touchmove", onTouchMove);
14388
+ document.removeEventListener("touchend", cleanup);
14225
14389
  document.body.style.userSelect = "";
14226
14390
  document.body.style.cursor = "";
14227
14391
  };
14228
14392
  document.body.style.userSelect = "none";
14229
14393
  document.addEventListener("mousemove", onMove);
14230
14394
  document.addEventListener("mouseup", cleanup, { once: true });
14395
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
14396
+ document.addEventListener("touchend", cleanup, { once: true });
14231
14397
  }
14232
14398
  _bindEsc() {
14233
14399
  const handler = (e) => {
@@ -14268,7 +14434,7 @@
14268
14434
  height: natH
14269
14435
  }, w, h);
14270
14436
  if (!canvas) {
14271
- 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.");
14272
14438
  this._close(false);
14273
14439
  return;
14274
14440
  }
@@ -14285,6 +14451,37 @@
14285
14451
  this.context.invoke("imageResizer.updateOverlay");
14286
14452
  }
14287
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
+ /**
14288
14485
  * Remove all overlay DOM elements and reset state.
14289
14486
  * @param {boolean} _committed - reserved for future use
14290
14487
  */
@@ -14699,6 +14896,16 @@
14699
14896
  if (!editable) return;
14700
14897
  editable.focus();
14701
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
+ }
14702
14909
  ctx.invoke("editor.afterCommand");
14703
14910
  },
14704
14911
  inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
@@ -14812,6 +15019,7 @@
14812
15019
  }
14813
15020
  document.body.appendChild(el);
14814
15021
  this._el = el;
15022
+ this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
14815
15023
  }
14816
15024
  _buildColorPicker() {
14817
15025
  const picker = document.createElement("div");
@@ -14897,8 +15105,13 @@
14897
15105
  editable.focus();
14898
15106
  const sel = window.getSelection();
14899
15107
  sel.removeAllRanges();
14900
- sel.addRange(this._savedRange.cloneRange());
14901
- 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);
14902
15115
  this.context.invoke("editor.afterCommand");
14903
15116
  const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
14904
15117
  const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
@@ -14933,8 +15146,8 @@
14933
15146
  this._closeColorPicker();
14934
15147
  }
14935
15148
  _syncActive() {
14936
- if (!this._el) return;
14937
- this._el.querySelectorAll(".an-bubble-btn").forEach((btn) => {
15149
+ if (!this._btnCache) return;
15150
+ this._btnCache.forEach((btn) => {
14938
15151
  const activeFn = _ACTIVE[btn.dataset.name];
14939
15152
  btn.classList.toggle("an-active", !!(activeFn && activeFn()));
14940
15153
  });
@@ -15081,14 +15294,23 @@
15081
15294
  const el = document.createElement("div");
15082
15295
  el.className = "an-mention-dropdown";
15083
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
+ });
15084
15306
  document.body.appendChild(el);
15085
15307
  this._dropdown = el;
15086
15308
  }
15087
15309
  _renderItems(items) {
15088
15310
  const dd = this._dropdown;
15089
- dd.innerHTML = "";
15090
15311
  this._items = items.slice(0, this._cfg.maxResults);
15091
15312
  this._activeIndex = this._items.length > 0 ? 0 : -1;
15313
+ const frag = document.createDocumentFragment();
15092
15314
  this._items.forEach((item, i) => {
15093
15315
  const li = document.createElement("div");
15094
15316
  li.className = "an-mention-item";
@@ -15104,10 +15326,10 @@
15104
15326
  const label = document.createElement("span");
15105
15327
  label.textContent = item.label;
15106
15328
  li.appendChild(label);
15107
- li.addEventListener("mousedown", (e) => e.preventDefault());
15108
- li.addEventListener("click", () => this._select(i));
15109
- dd.appendChild(li);
15329
+ frag.appendChild(li);
15110
15330
  });
15331
+ dd.innerHTML = "";
15332
+ dd.appendChild(frag);
15111
15333
  this._highlightItem(this._activeIndex);
15112
15334
  }
15113
15335
  _highlightItem(index) {