autumnnote 1.7.0 → 1.8.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.
@@ -520,13 +520,74 @@
520
520
  } catch {}
521
521
  }
522
522
  /**
523
- * Inserts an unordered list or converts selection.
523
+ * Inserts an unordered (bulleted) list, or converts the current list to `<ul>`.
524
+ *
525
+ * When the cursor is already inside a list, direct DOM manipulation is used to
526
+ * transition between list types — `execCommand` alone cannot handle checklist →
527
+ * UL/OL conversions because it has no awareness of the `an-checklist` class or
528
+ * the checkbox `<input>` elements.
529
+ *
530
+ * Transition paths:
531
+ * - **Checklist → UL**: strips `an-checklist` class and all checkbox inputs;
532
+ * converts `<ol>` container to `<ul>` via `changeTagName()` if needed.
533
+ * - **OL → UL**: swaps the container tag via `changeTagName()`.
534
+ * - **UL → paragraphs**: falls back to `execCommand('insertUnorderedList')`
535
+ * which toggles the list off (browser-native behaviour).
536
+ * - **No list → UL**: falls back to `execCommand('insertUnorderedList')`.
537
+ */
538
+ /**
539
+ * Helper to get the closest ul/ol element containing the current selection.
540
+ * @returns {Element|null}
524
541
  */
525
- var insertUnorderedList = () => execCommand("insertUnorderedList");
542
+ function getSelectedList() {
543
+ const sel = globalThis.getSelection();
544
+ if (!sel?.rangeCount) return null;
545
+ let container = sel.getRangeAt(0).commonAncestorContainer;
546
+ if (container.nodeType === 3) container = container.parentElement;
547
+ return container?.closest("ul, ol") || null;
548
+ }
549
+ /**
550
+ * Strips the checklist class and checkbox inputs from a list element.
551
+ * @param {Element} listEl
552
+ */
553
+ function stripChecklist(listEl) {
554
+ listEl.classList.remove("an-checklist");
555
+ listEl.querySelectorAll("input[type=\"checkbox\"]").forEach((cb) => cb.remove());
556
+ }
557
+ function insertUnorderedList() {
558
+ const listEl = getSelectedList();
559
+ if (listEl) if (listEl.classList.contains("an-checklist")) {
560
+ stripChecklist(listEl);
561
+ if (listEl.tagName === "OL") changeTagName(listEl, "ul");
562
+ } else if (listEl.tagName === "OL") changeTagName(listEl, "ul");
563
+ else execCommand("insertUnorderedList");
564
+ else execCommand("insertUnorderedList");
565
+ }
526
566
  /**
527
- * Inserts an ordered list or converts selection.
567
+ * Inserts an ordered (numbered) list, or converts the current list to `<ol>`.
568
+ *
569
+ * When the cursor is already inside a list, direct DOM manipulation is used to
570
+ * transition between list types — `execCommand` alone cannot handle checklist →
571
+ * UL/OL conversions because it has no awareness of the `an-checklist` class or
572
+ * the checkbox `<input>` elements.
573
+ *
574
+ * Transition paths:
575
+ * - **Checklist → OL**: strips `an-checklist` class and all checkbox inputs;
576
+ * converts container to `<ol>` via `changeTagName()`.
577
+ * - **UL → OL**: swaps the container tag via `changeTagName()`.
578
+ * - **OL → paragraphs**: falls back to `execCommand('insertOrderedList')`
579
+ * which toggles the list off (browser-native behaviour).
580
+ * - **No list → OL**: falls back to `execCommand('insertOrderedList')`.
528
581
  */
529
- var insertOrderedList = () => execCommand("insertOrderedList");
582
+ function insertOrderedList() {
583
+ const listEl = getSelectedList();
584
+ if (listEl) if (listEl.classList.contains("an-checklist")) {
585
+ stripChecklist(listEl);
586
+ changeTagName(listEl, "ol");
587
+ } else if (listEl.tagName === "UL") changeTagName(listEl, "ol");
588
+ else execCommand("insertOrderedList");
589
+ else execCommand("insertOrderedList");
590
+ }
530
591
  /**
531
592
  * Set the line-height on every block-level element that intersects the current selection.
532
593
  *
@@ -652,18 +713,34 @@
652
713
  return !!(code && !code.closest("pre"));
653
714
  }
654
715
  /**
716
+ * Changes the tag name of an element in the DOM while preserving attributes and children.
717
+ * @param {Element} el
718
+ * @param {string} newTagName
719
+ * @returns {HTMLElement}
720
+ */
721
+ function changeTagName(el, newTagName) {
722
+ const newEl = document.createElement(newTagName);
723
+ for (const attr of el.attributes) newEl.setAttribute(attr.name, attr.value);
724
+ while (el.firstChild) newEl.appendChild(el.firstChild);
725
+ el.parentNode.replaceChild(newEl, el);
726
+ return newEl;
727
+ }
728
+ /**
729
+ * Ensures all list items under the list element have a checkbox.
730
+ * @param {Element} listEl
731
+ */
732
+ function ensureCheckboxes(listEl) {
733
+ listEl.querySelectorAll("li").forEach((li) => {
734
+ if (!li.querySelector("input[type=\"checkbox\"]")) {
735
+ const cb = document.createElement("input");
736
+ cb.type = "checkbox";
737
+ cb.contentEditable = "false";
738
+ li.insertBefore(cb, li.firstChild);
739
+ }
740
+ });
741
+ }
742
+ /**
655
743
  * Toggle a checklist at the current selection or caret.
656
- *
657
- * When the selection is inside an existing checklist `<ul class="an-checklist">`,
658
- * converts the selected `<li>` items back into `<p>` paragraphs and places the caret
659
- * at the start of the first converted paragraph. Otherwise creates a checklist:
660
- * - If the selection is collapsed, converts the nearest block-level ancestor (or inserts
661
- * a single checklist item at the editable root) into a checklist with one item containing
662
- * that block's text and places the caret inside the new item.
663
- * - If the selection is a range, converts each intersecting block element into one checklist
664
- * item (preserving textual content) and places the caret at the end of the last item.
665
- *
666
- * Empty or whitespace-only selections do not create a checklist.
667
744
  */
668
745
  function toggleChecklist() {
669
746
  const sel = globalThis.getSelection();
@@ -671,27 +748,26 @@
671
748
  const range = sel.getRangeAt(0);
672
749
  let container = range.commonAncestorContainer;
673
750
  if (container.nodeType === 3) container = container.parentElement;
674
- const ul = container?.closest(".an-checklist");
675
- if (ul) {
676
- const selectedLis = Array.from(ul.querySelectorAll("li")).filter((li) => sel.containsNode(li, true));
677
- if (selectedLis.length > 0) {
678
- /** @type {HTMLElement|null} */ let firstP = null;
679
- selectedLis.forEach((li) => {
751
+ const listEl = container?.closest("ul, ol");
752
+ if (listEl) if (listEl.classList.contains("an-checklist")) {
753
+ if (listEl.parentNode) {
754
+ const lis = Array.from(listEl.children);
755
+ let firstP = null;
756
+ lis.forEach((li) => {
680
757
  const p = document.createElement("p");
681
758
  for (const child of li.childNodes) {
682
759
  if (child.nodeType === 1 && child.tagName === "INPUT") continue;
683
760
  p.appendChild(child.cloneNode(true));
684
761
  }
685
- p.innerHTML = p.innerHTML.replaceAll("​", "");
762
+ p.innerHTML = p.innerHTML.replaceAll("​", "").replaceAll("​", "");
686
763
  if (!p.hasChildNodes() || !p.textContent.trim()) {
687
764
  p.innerHTML = "";
688
765
  p.appendChild(document.createTextNode("\xA0"));
689
766
  }
690
- ul.parentNode.insertBefore(p, ul);
767
+ listEl.before(p);
691
768
  if (!firstP) firstP = p;
692
- li.remove();
693
769
  });
694
- if (ul.children.length === 0) ul.remove();
770
+ listEl.remove();
695
771
  if (firstP) {
696
772
  const nr = document.createRange();
697
773
  nr.setStart(firstP.firstChild || firstP, 0);
@@ -699,11 +775,65 @@
699
775
  sel.removeAllRanges();
700
776
  sel.addRange(nr);
701
777
  }
702
- return;
778
+ }
779
+ } else {
780
+ const targetUl = changeTagName(listEl, "ul");
781
+ targetUl.classList.add("an-checklist");
782
+ ensureCheckboxes(targetUl);
783
+ const firstLi = targetUl.querySelector("li");
784
+ if (firstLi) {
785
+ const nr = document.createRange();
786
+ nr.selectNodeContents(firstLi);
787
+ nr.collapse(false);
788
+ sel.removeAllRanges();
789
+ sel.addRange(nr);
703
790
  }
704
791
  }
705
- if (range.collapsed) {
706
- const BLOCK_TAGS = new Set([
792
+ else {
793
+ const editableRoot = container?.closest("[contenteditable=\"true\"]");
794
+ if (range.collapsed) {
795
+ const BLOCK_TAGS = new Set([
796
+ "P",
797
+ "DIV",
798
+ "H1",
799
+ "H2",
800
+ "H3",
801
+ "H4",
802
+ "H5",
803
+ "H6",
804
+ "BLOCKQUOTE",
805
+ "LI"
806
+ ]);
807
+ let block = container;
808
+ while (block?.parentNode && block !== editableRoot && !BLOCK_TAGS.has(block.tagName)) block = block.parentNode;
809
+ if (block === editableRoot) block = null;
810
+ const itemText = block && BLOCK_TAGS.has(block.tagName) ? Array.from(block.childNodes).map((n) => n.textContent).join("").replaceAll("\xA0", " ") : "";
811
+ const newUl = document.createElement("ul");
812
+ newUl.className = "an-checklist";
813
+ const li = document.createElement("li");
814
+ const checkbox = document.createElement("input");
815
+ checkbox.type = "checkbox";
816
+ checkbox.contentEditable = "false";
817
+ li.appendChild(checkbox);
818
+ li.appendChild(document.createTextNode(itemText || "​"));
819
+ newUl.appendChild(li);
820
+ if (block && BLOCK_TAGS.has(block.tagName)) block.parentNode.replaceChild(newUl, block);
821
+ else {
822
+ const nativeRange = sel.getRangeAt(0);
823
+ nativeRange.deleteContents();
824
+ nativeRange.insertNode(newUl);
825
+ }
826
+ const textNode = li.lastChild;
827
+ const nr = document.createRange();
828
+ const offset = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent.length : 0;
829
+ nr.setStart(textNode, offset);
830
+ nr.collapse(true);
831
+ sel.removeAllRanges();
832
+ sel.addRange(nr);
833
+ return;
834
+ }
835
+ if (!sel.toString().replace(/[\u00a0\u200B]/g, " ").trim()) return;
836
+ const BLOCK_TAGS_MULTI = new Set([
707
837
  "P",
708
838
  "DIV",
709
839
  "H1",
@@ -713,89 +843,51 @@
713
843
  "H5",
714
844
  "H6",
715
845
  "BLOCKQUOTE",
846
+ "PRE",
716
847
  "LI"
717
848
  ]);
718
- let block = container;
719
- while (block?.parentNode && !BLOCK_TAGS.has(block.tagName)) block = block.parentNode;
720
- const itemText = block && BLOCK_TAGS.has(block.tagName) ? Array.from(block.childNodes).map((n) => n.textContent).join("").replaceAll("\xA0", " ") : "";
721
- const ul = document.createElement("ul");
722
- ul.className = "an-checklist";
723
- const li = document.createElement("li");
724
- const checkbox = document.createElement("input");
725
- checkbox.type = "checkbox";
726
- checkbox.contentEditable = "false";
727
- li.appendChild(checkbox);
728
- li.appendChild(document.createTextNode(itemText || "​"));
729
- ul.appendChild(li);
730
- if (block && BLOCK_TAGS.has(block.tagName)) block.parentNode.replaceChild(ul, block);
731
- else {
732
- const nativeRange = sel.getRangeAt(0);
733
- nativeRange.deleteContents();
734
- nativeRange.insertNode(ul);
849
+ const blocks = [];
850
+ const seenBlocks = /* @__PURE__ */ new Set();
851
+ const commonAncestor = range.commonAncestorContainer;
852
+ const iter = document.createNodeIterator(commonAncestor.nodeType === Node.TEXT_NODE ? commonAncestor.parentNode : commonAncestor, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, null);
853
+ let node;
854
+ while (node = iter.nextNode()) {
855
+ if (!range.intersectsNode(node)) continue;
856
+ let blockEl = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
857
+ while (blockEl && blockEl !== editableRoot && !BLOCK_TAGS_MULTI.has(blockEl.tagName)) blockEl = blockEl.parentElement;
858
+ if (blockEl === editableRoot) blockEl = null;
859
+ if (blockEl && !seenBlocks.has(blockEl)) {
860
+ seenBlocks.add(blockEl);
861
+ blocks.push(blockEl);
862
+ }
735
863
  }
736
- const textNode = li.lastChild;
737
- const nr = document.createRange();
738
- const offset = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent.length : 0;
739
- nr.setStart(textNode, offset);
740
- nr.collapse(true);
741
- sel.removeAllRanges();
742
- sel.addRange(nr);
743
- return;
744
- }
745
- if (!sel.toString().replace(/[\u00a0\u200B]/g, " ").trim()) return;
746
- const BLOCK_TAGS_MULTI = new Set([
747
- "P",
748
- "DIV",
749
- "H1",
750
- "H2",
751
- "H3",
752
- "H4",
753
- "H5",
754
- "H6",
755
- "BLOCKQUOTE",
756
- "PRE",
757
- "LI"
758
- ]);
759
- const blocks = [];
760
- const seenBlocks = /* @__PURE__ */ new Set();
761
- const commonAncestor = range.commonAncestorContainer;
762
- const iter = document.createNodeIterator(commonAncestor.nodeType === Node.TEXT_NODE ? commonAncestor.parentNode : commonAncestor, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, null);
763
- let node;
764
- while (node = iter.nextNode()) {
765
- if (!range.intersectsNode(node)) continue;
766
- let block = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
767
- while (block && !BLOCK_TAGS_MULTI.has(block.tagName)) block = block.parentElement;
768
- if (block && !seenBlocks.has(block)) {
769
- seenBlocks.add(block);
770
- blocks.push(block);
864
+ if (blocks.length === 0) return;
865
+ const newUl = document.createElement("ul");
866
+ newUl.className = "an-checklist";
867
+ /** @type {Text|null} */ let lastTextNode = null;
868
+ blocks.forEach((block) => {
869
+ const li = document.createElement("li");
870
+ const cb = document.createElement("input");
871
+ cb.type = "checkbox";
872
+ cb.contentEditable = "false";
873
+ li.appendChild(cb);
874
+ const blockText = Array.from(block.childNodes).map((n) => n.textContent).join("").replace(/[\u00a0\u200B]/g, " ").trim();
875
+ const tn = document.createTextNode(blockText || "");
876
+ li.appendChild(tn);
877
+ newUl.appendChild(li);
878
+ lastTextNode = tn;
879
+ });
880
+ const firstBlock = blocks[0];
881
+ firstBlock.parentNode.insertBefore(newUl, firstBlock);
882
+ blocks.forEach((block) => block.remove());
883
+ if (lastTextNode) {
884
+ const nr = document.createRange();
885
+ nr.setStart(lastTextNode, lastTextNode.textContent.length);
886
+ nr.collapse(true);
887
+ sel.removeAllRanges();
888
+ sel.addRange(nr);
771
889
  }
772
890
  }
773
- if (blocks.length === 0) return;
774
- const newUl = document.createElement("ul");
775
- newUl.className = "an-checklist";
776
- /** @type {Text|null} */ let lastTextNode = null;
777
- blocks.forEach((block) => {
778
- const li = document.createElement("li");
779
- const cb = document.createElement("input");
780
- cb.type = "checkbox";
781
- cb.setAttribute("contenteditable", "false");
782
- li.appendChild(cb);
783
- const blockText = Array.from(block.childNodes).map((n) => n.textContent).join("").replace(/[\u00a0\u200B]/g, " ").trim();
784
- const tn = document.createTextNode(blockText || "​");
785
- li.appendChild(tn);
786
- newUl.appendChild(li);
787
- lastTextNode = tn;
788
- });
789
- const firstBlock = blocks[0];
790
- firstBlock.parentNode.insertBefore(newUl, firstBlock);
791
- blocks.forEach((block) => block.remove());
792
- if (lastTextNode) {
793
- const nr = document.createRange();
794
- nr.setStart(lastTextNode, lastTextNode.textContent.length);
795
- nr.collapse(true);
796
- sel.removeAllRanges();
797
- sel.addRange(nr);
798
- }
799
891
  }
800
892
  /**
801
893
  * Returns true when the cursor is inside a checklist item.
@@ -968,6 +1060,13 @@
968
1060
  }
969
1061
  };
970
1062
  var removeFormatBtn = btn("removeFormat", "remove-format", "Remove Format", () => execCommand("removeFormat"));
1063
+ var directionBtn = btn("direction", "direction", "Toggle Text Direction (LTR / RTL)", (ctx) => {
1064
+ const editable = ctx.layoutInfo.editable;
1065
+ const next = (editable.getAttribute("dir") || "ltr") === "ltr" ? "rtl" : "ltr";
1066
+ editable.setAttribute("dir", next);
1067
+ editable.style.textAlign = next === "rtl" ? "right" : "left";
1068
+ ctx.invoke("editor.afterCommand");
1069
+ });
971
1070
  /** @type {DropdownDef} */
972
1071
  var fontFamilyBtn = {
973
1072
  name: "fontFamily",
@@ -1090,9 +1189,122 @@
1090
1189
  var fullscreenBtn = btn("fullscreen", "expand", "Fullscreen", (ctx) => ctx.invoke("fullscreen.toggle"), (ctx) => ctx.invoke("fullscreen.isActive"));
1091
1190
  var shortcutsBtn = btn("shortcuts", "keyboard", "Keyboard Shortcuts (Ctrl+Shift+/)", (ctx) => ctx.invoke("shortcutsDialog.show"));
1092
1191
  var findBtn = btn("find", "search", "Find (Ctrl+F)", (ctx) => ctx.invoke("findReplace.show", "find"));
1192
+ var findReplaceBtn = btn("findReplace", "find-replace", "Find & Replace (Ctrl+H)", (ctx) => ctx.invoke("findReplace.show", "replace"));
1093
1193
  var inlineCodeBtn = btn("inlineCode", "inline-code", "Inline Code (Ctrl+`)", (ctx) => ctx.invoke("editor.inlineCode"), () => isInlineCode());
1094
1194
  var checklistBtn = btn("checklist", "checklist", "Checklist", (ctx) => ctx.invoke("editor.toggleChecklist"), () => isInChecklist());
1095
1195
  var printBtn = btn("print", "print", "Print", (ctx) => ctx.invoke("editor.print"));
1196
+ /** @type {{ name: string, type: 'colorpicker', icon: string, tooltip: string, defaultColor: string, action: Function }} */
1197
+ var foreColorBtn = {
1198
+ name: "foreColor",
1199
+ type: "colorpicker",
1200
+ icon: "foreColor",
1201
+ tooltip: "Text Color",
1202
+ defaultColor: "#e11d48",
1203
+ action: (ctx, color) => foreColor(color)
1204
+ };
1205
+ /** @type {{ name: string, type: 'colorpicker', icon: string, tooltip: string, defaultColor: string, action: Function }} */
1206
+ var backColorBtn = {
1207
+ name: "backColor",
1208
+ type: "colorpicker",
1209
+ icon: "backColor",
1210
+ tooltip: "Highlight Color",
1211
+ defaultColor: "#fbbf24",
1212
+ action: (ctx, color) => backColor(color)
1213
+ };
1214
+ /**
1215
+ * The default toolbar button groups.
1216
+ * Each sub-array is a button group (separated by a divider).
1217
+ */
1218
+ var defaultToolbar = [
1219
+ [
1220
+ paragraphStyleBtn,
1221
+ fontFamilyBtn,
1222
+ fontSizeBtn,
1223
+ lineHeightBtn
1224
+ ],
1225
+ [undoBtn, redoBtn],
1226
+ [
1227
+ boldBtn,
1228
+ italicBtn,
1229
+ underlineBtn,
1230
+ strikeBtn,
1231
+ inlineCodeBtn
1232
+ ],
1233
+ [superscriptBtn, subscriptBtn],
1234
+ [foreColorBtn, backColorBtn],
1235
+ [
1236
+ alignLeftBtn,
1237
+ alignCenterBtn,
1238
+ alignRightBtn,
1239
+ alignJustifyBtn
1240
+ ],
1241
+ [
1242
+ ulBtn,
1243
+ olBtn,
1244
+ checklistBtn,
1245
+ indentBtn,
1246
+ outdentBtn
1247
+ ],
1248
+ [
1249
+ hrBtn,
1250
+ linkBtn,
1251
+ imageBtn,
1252
+ videoBtn,
1253
+ tableBtn,
1254
+ emojiBtn,
1255
+ iconBtn
1256
+ ],
1257
+ [
1258
+ removeFormatBtn,
1259
+ codeviewBtn,
1260
+ fullscreenBtn,
1261
+ findBtn,
1262
+ printBtn,
1263
+ shortcutsBtn
1264
+ ]
1265
+ ];
1266
+ var buttons = {
1267
+ boldBtn,
1268
+ italicBtn,
1269
+ underlineBtn,
1270
+ strikeBtn,
1271
+ superscriptBtn,
1272
+ subscriptBtn,
1273
+ alignLeftBtn,
1274
+ alignCenterBtn,
1275
+ alignRightBtn,
1276
+ alignJustifyBtn,
1277
+ ulBtn,
1278
+ olBtn,
1279
+ indentBtn,
1280
+ outdentBtn,
1281
+ undoBtn,
1282
+ redoBtn,
1283
+ hrBtn,
1284
+ linkBtn,
1285
+ imageBtn,
1286
+ videoBtn,
1287
+ emojiBtn,
1288
+ iconBtn,
1289
+ tableBtn,
1290
+ fontSizeBtn,
1291
+ removeFormatBtn,
1292
+ directionBtn,
1293
+ fontFamilyBtn,
1294
+ paragraphStyleBtn,
1295
+ lineHeightBtn,
1296
+ codeviewBtn,
1297
+ fullscreenBtn,
1298
+ shortcutsBtn,
1299
+ findBtn,
1300
+ findReplaceBtn,
1301
+ inlineCodeBtn,
1302
+ checklistBtn,
1303
+ printBtn,
1304
+ foreColorBtn,
1305
+ backColorBtn,
1306
+ defaultToolbar
1307
+ };
1096
1308
  //#endregion
1097
1309
  //#region src/js/settings.js
1098
1310
  /**
@@ -1166,68 +1378,7 @@
1166
1378
  maxHeight: 0,
1167
1379
  focus: false,
1168
1380
  resizable: true,
1169
- toolbar: [
1170
- [
1171
- paragraphStyleBtn,
1172
- fontFamilyBtn,
1173
- fontSizeBtn,
1174
- lineHeightBtn
1175
- ],
1176
- [undoBtn, redoBtn],
1177
- [
1178
- boldBtn,
1179
- italicBtn,
1180
- underlineBtn,
1181
- strikeBtn,
1182
- inlineCodeBtn
1183
- ],
1184
- [superscriptBtn, subscriptBtn],
1185
- [{
1186
- name: "foreColor",
1187
- type: "colorpicker",
1188
- icon: "foreColor",
1189
- tooltip: "Text Color",
1190
- defaultColor: "#e11d48",
1191
- action: (ctx, color) => foreColor(color)
1192
- }, {
1193
- name: "backColor",
1194
- type: "colorpicker",
1195
- icon: "backColor",
1196
- tooltip: "Highlight Color",
1197
- defaultColor: "#fbbf24",
1198
- action: (ctx, color) => backColor(color)
1199
- }],
1200
- [
1201
- alignLeftBtn,
1202
- alignCenterBtn,
1203
- alignRightBtn,
1204
- alignJustifyBtn
1205
- ],
1206
- [
1207
- ulBtn,
1208
- olBtn,
1209
- checklistBtn,
1210
- indentBtn,
1211
- outdentBtn
1212
- ],
1213
- [
1214
- hrBtn,
1215
- linkBtn,
1216
- imageBtn,
1217
- videoBtn,
1218
- tableBtn,
1219
- emojiBtn,
1220
- iconBtn
1221
- ],
1222
- [
1223
- removeFormatBtn,
1224
- codeviewBtn,
1225
- fullscreenBtn,
1226
- findBtn,
1227
- printBtn,
1228
- shortcutsBtn
1229
- ]
1230
- ],
1381
+ toolbar: defaultToolbar,
1231
1382
  useBootstrap: false,
1232
1383
  toolbarButtonClass: "btn btn-sm btn-light",
1233
1384
  useFontAwesome: true,
@@ -1448,6 +1599,7 @@
1448
1599
  findPlaceholder: "Find…",
1449
1600
  searchAriaLabel: "Search text",
1450
1601
  caseSensitive: "\xA0Case sensitive",
1602
+ wholeWord: "Whole Word",
1451
1603
  prevBtn: "← Prev",
1452
1604
  nextBtn: "Next →",
1453
1605
  replacePlaceholder: "Replace with…",
@@ -1458,6 +1610,12 @@
1458
1610
  useRegex: "Use Regular Expression",
1459
1611
  close: "×"
1460
1612
  },
1613
+ autoSaveRestore: {
1614
+ found: "Draft found. Restore?",
1615
+ foundAt: "Draft from {date}. Restore?",
1616
+ restore: "Restore",
1617
+ discard: "Discard"
1618
+ },
1461
1619
  shortcutsDialog: {
1462
1620
  title: "Keyboard Shortcuts",
1463
1621
  ariaLabel: "Keyboard Shortcuts",
@@ -1823,6 +1981,7 @@
1823
1981
  findPlaceholder: "Tìm…",
1824
1982
  searchAriaLabel: "Văn bản tìm kiếm",
1825
1983
  caseSensitive: "\xA0Phân biệt hoa thường",
1984
+ wholeWord: "Toàn bộ từ",
1826
1985
  prevBtn: "← Trước",
1827
1986
  nextBtn: "Tiếp →",
1828
1987
  replacePlaceholder: "Thay thế bằng…",
@@ -1990,9 +2149,16 @@
1990
2149
  columnWidth: "Chiều rộng cột",
1991
2150
  rowHeight: "Chiều cao hàng",
1992
2151
  tableBorderWidth: "Độ rộng viền bảng",
2152
+ tableBorderColor: "Màu viền bảng",
1993
2153
  deleteTable: "Xóa bảng",
2154
+ cellAlignLeft: "Căn trái",
2155
+ cellAlignCenter: "Căn giữa",
2156
+ cellAlignRight: "Căn phải",
2157
+ cellAlignJustify: "Căn đều",
2158
+ toggleHeaderRow: "Bật/tắt hàng tiêu đề",
1994
2159
  cellBackground: "Màu Nền Ô",
1995
2160
  noShading: "Xóa Màu Nền",
2161
+ noBorderColor: "Xóa màu viền",
1996
2162
  columnWidthPx: "Chiều rộng cột (px)",
1997
2163
  rowHeightPx: "Chiều cao hàng (px)",
1998
2164
  tableBorderWidthPx: "Độ rộng viền bảng (px)",
@@ -2020,6 +2186,12 @@
2020
2186
  errors: {
2021
2187
  imageFormat: (type) => `Định dạng "${type}" không được hỗ trợ hiển thị trên trình duyệt. Vui lòng chuyển đổi sang JPEG, PNG hoặc WebP.`,
2022
2188
  imageSize: (maxSize) => `Tệp hình ảnh quá lớn. Kích thước tối đa cho phép là ${maxSize} MB.`
2189
+ },
2190
+ autoSaveRestore: {
2191
+ found: "Tìm thấy bản nháp. Khôi phục?",
2192
+ foundAt: "Bản nháp từ {date}. Khôi phục?",
2193
+ restore: "Khôi phục",
2194
+ discard: "Bỏ qua"
2023
2195
  }
2024
2196
  },
2025
2197
  ja: {
@@ -2163,6 +2335,7 @@
2163
2335
  findPlaceholder: "検索…",
2164
2336
  searchAriaLabel: "検索テキスト",
2165
2337
  caseSensitive: "\xA0大文字/小文字を区別",
2338
+ wholeWord: "単語単位",
2166
2339
  prevBtn: "← 前へ",
2167
2340
  nextBtn: "次へ →",
2168
2341
  replacePlaceholder: "置換後…",
@@ -2330,7 +2503,16 @@
2330
2503
  columnWidth: "列幅",
2331
2504
  rowHeight: "行の高さ",
2332
2505
  tableBorderWidth: "テーブルの枠幅",
2506
+ tableBorderColor: "テーブルの枠線色",
2333
2507
  deleteTable: "テーブルを削除",
2508
+ cellAlignLeft: "左揃え",
2509
+ cellAlignCenter: "中央揃え",
2510
+ cellAlignRight: "右揃え",
2511
+ cellAlignJustify: "両端揃え",
2512
+ toggleHeaderRow: "ヘッダー行の切り替え",
2513
+ cellBackground: "セル背景色",
2514
+ noShading: "背景色なし",
2515
+ noBorderColor: "枠線色なし",
2334
2516
  columnWidthPx: "列幅 (px)",
2335
2517
  rowHeightPx: "行の高さ (px)",
2336
2518
  tableBorderWidthPx: "テーブルの枠幅 (px)",
@@ -2358,6 +2540,12 @@
2358
2540
  errors: {
2359
2541
  imageFormat: (type) => `形式 "${type}" はブラウザでの表示をサポートしていません。JPEG、PNG、または WebP に変換してください。`,
2360
2542
  imageSize: (maxSize) => `画像ファイルが大きすぎます。最大許容サイズは ${maxSize} MB です。`
2543
+ },
2544
+ autoSaveRestore: {
2545
+ found: "下書きが見つかりました。復元しますか?",
2546
+ foundAt: "{date} の下書きが見つかりました。復元しますか?",
2547
+ restore: "復元",
2548
+ discard: "破棄"
2361
2549
  }
2362
2550
  },
2363
2551
  zh: {
@@ -2501,6 +2689,7 @@
2501
2689
  findPlaceholder: "查找…",
2502
2690
  searchAriaLabel: "搜索文字",
2503
2691
  caseSensitive: "\xA0区分大小写",
2692
+ wholeWord: "全字匹配",
2504
2693
  prevBtn: "← 上一个",
2505
2694
  nextBtn: "下一个 →",
2506
2695
  replacePlaceholder: "替换为…",
@@ -2668,7 +2857,16 @@
2668
2857
  columnWidth: "列宽",
2669
2858
  rowHeight: "行高",
2670
2859
  tableBorderWidth: "表格边框宽度",
2860
+ tableBorderColor: "表格边框颜色",
2671
2861
  deleteTable: "删除表格",
2862
+ cellAlignLeft: "左对齐",
2863
+ cellAlignCenter: "居中对齐",
2864
+ cellAlignRight: "右对齐",
2865
+ cellAlignJustify: "两端对齐",
2866
+ toggleHeaderRow: "切换标题行",
2867
+ cellBackground: "单元格背景",
2868
+ noShading: "无底纹",
2869
+ noBorderColor: "无边框颜色",
2672
2870
  columnWidthPx: "列宽 (px)",
2673
2871
  rowHeightPx: "行高 (px)",
2674
2872
  tableBorderWidthPx: "表格边框宽度 (px)",
@@ -2696,6 +2894,12 @@
2696
2894
  errors: {
2697
2895
  imageFormat: (type) => `格式 "${type}" 不支持在浏览器中显示。请转换为 JPEG、PNG 或 WebP。`,
2698
2896
  imageSize: (maxSize) => `图片文件过大。最大允许大小为 ${maxSize} MB。`
2897
+ },
2898
+ autoSaveRestore: {
2899
+ found: "找到草稿。是否恢复?",
2900
+ foundAt: "发现 {date} 的草稿。是否恢复?",
2901
+ restore: "恢复",
2902
+ discard: "放弃"
2699
2903
  }
2700
2904
  },
2701
2905
  fr: {
@@ -2839,6 +3043,7 @@
2839
3043
  findPlaceholder: "Rechercher…",
2840
3044
  searchAriaLabel: "Texte à rechercher",
2841
3045
  caseSensitive: "\xA0Respecter la casse",
3046
+ wholeWord: "Mot entier",
2842
3047
  prevBtn: "← Préc.",
2843
3048
  nextBtn: "Suiv. →",
2844
3049
  replacePlaceholder: "Remplacer par…",
@@ -3006,7 +3211,16 @@
3006
3211
  columnWidth: "Largeur de colonne",
3007
3212
  rowHeight: "Hauteur de ligne",
3008
3213
  tableBorderWidth: "Épaisseur des bordures",
3214
+ tableBorderColor: "Couleur de bordure",
3009
3215
  deleteTable: "Supprimer le tableau",
3216
+ cellAlignLeft: "Aligner à gauche",
3217
+ cellAlignCenter: "Centrer",
3218
+ cellAlignRight: "Aligner à droite",
3219
+ cellAlignJustify: "Justifier",
3220
+ toggleHeaderRow: "Ligne d'en-tête",
3221
+ cellBackground: "Couleur de fond",
3222
+ noShading: "Aucun fond",
3223
+ noBorderColor: "Aucune bordure",
3010
3224
  columnWidthPx: "Largeur de colonne (px)",
3011
3225
  rowHeightPx: "Hauteur de ligne (px)",
3012
3226
  tableBorderWidthPx: "Épaisseur des bordures (px)",
@@ -3034,6 +3248,12 @@
3034
3248
  errors: {
3035
3249
  imageFormat: (type) => `Le format "${type}" n'est pas pris en charge par le navigateur. Veuillez le convertir en JPEG, PNG ou WebP.`,
3036
3250
  imageSize: (maxSize) => `Le fichier image est trop volumineux. La taille maximale autorisée est de ${maxSize}\u00a0Mo.`
3251
+ },
3252
+ autoSaveRestore: {
3253
+ found: "Brouillon trouvé. Restaurer ?",
3254
+ foundAt: "Brouillon du {date}. Restaurer ?",
3255
+ restore: "Restaurer",
3256
+ discard: "Ignorer"
3037
3257
  }
3038
3258
  },
3039
3259
  de: {
@@ -3177,6 +3397,7 @@
3177
3397
  findPlaceholder: "Suchen…",
3178
3398
  searchAriaLabel: "Suchtext",
3179
3399
  caseSensitive: "\xA0Groß-/Kleinschreibung",
3400
+ wholeWord: "Ganzes Wort",
3180
3401
  prevBtn: "← Zurück",
3181
3402
  nextBtn: "Weiter →",
3182
3403
  replacePlaceholder: "Ersetzen durch…",
@@ -3344,7 +3565,16 @@
3344
3565
  columnWidth: "Spaltenbreite",
3345
3566
  rowHeight: "Zeilenhöhe",
3346
3567
  tableBorderWidth: "Tabellenrahmenbreite",
3568
+ tableBorderColor: "Tabellenrahmenfarbe",
3347
3569
  deleteTable: "Tabelle löschen",
3570
+ cellAlignLeft: "Linksbündig",
3571
+ cellAlignCenter: "Zentrieren",
3572
+ cellAlignRight: "Rechtsbündig",
3573
+ cellAlignJustify: "Blocksatz",
3574
+ toggleHeaderRow: "Kopfzeile umschalten",
3575
+ cellBackground: "Zellhintergrund",
3576
+ noShading: "Kein Hintergrund",
3577
+ noBorderColor: "Keine Rahmenfarbe",
3348
3578
  columnWidthPx: "Spaltenbreite (px)",
3349
3579
  rowHeightPx: "Zeilenhöhe (px)",
3350
3580
  tableBorderWidthPx: "Tabellenrahmenbreite (px)",
@@ -3372,6 +3602,12 @@
3372
3602
  errors: {
3373
3603
  imageFormat: (type) => `Das Format „${type}" wird in Webbrowsern nicht unterstützt. Bitte konvertieren Sie es zuerst in JPEG, PNG oder WebP.`,
3374
3604
  imageSize: (maxSize) => `Die Bilddatei ist zu groß. Die maximal zulässige Größe beträgt ${maxSize} MB.`
3605
+ },
3606
+ autoSaveRestore: {
3607
+ found: "Entwurf gefunden. Wiederherstellen?",
3608
+ foundAt: "Entwurf vom {date}. Wiederherstellen?",
3609
+ restore: "Wiederherstellen",
3610
+ discard: "Verwerfen"
3375
3611
  }
3376
3612
  },
3377
3613
  es: {
@@ -3515,6 +3751,7 @@
3515
3751
  findPlaceholder: "Buscar…",
3516
3752
  searchAriaLabel: "Texto de búsqueda",
3517
3753
  caseSensitive: "\xA0Distinguir mayúsculas",
3754
+ wholeWord: "Palabra completa",
3518
3755
  prevBtn: "← Anterior",
3519
3756
  nextBtn: "Siguiente →",
3520
3757
  replacePlaceholder: "Reemplazar con…",
@@ -3682,7 +3919,16 @@
3682
3919
  columnWidth: "Ancho de columna",
3683
3920
  rowHeight: "Alto de fila",
3684
3921
  tableBorderWidth: "Grosor del borde de la tabla",
3922
+ tableBorderColor: "Color del borde de la tabla",
3685
3923
  deleteTable: "Eliminar tabla",
3924
+ cellAlignLeft: "Alinear a la izquierda",
3925
+ cellAlignCenter: "Centrar",
3926
+ cellAlignRight: "Alinear a la derecha",
3927
+ cellAlignJustify: "Justificar",
3928
+ toggleHeaderRow: "Activar fila de encabezado",
3929
+ cellBackground: "Fondo de celda",
3930
+ noShading: "Sin fondo",
3931
+ noBorderColor: "Sin color de borde",
3686
3932
  columnWidthPx: "Ancho de columna (px)",
3687
3933
  rowHeightPx: "Alto de fila (px)",
3688
3934
  tableBorderWidthPx: "Grosor del borde (px)",
@@ -3710,6 +3956,12 @@
3710
3956
  errors: {
3711
3957
  imageFormat: (type) => `El formato "${type}" no es compatible con los navegadores web. Por favor, conviértalo primero a JPEG, PNG o WebP.`,
3712
3958
  imageSize: (maxSize) => `El archivo de imagen es demasiado grande. El tamaño máximo permitido es ${maxSize} MB.`
3959
+ },
3960
+ autoSaveRestore: {
3961
+ found: "Borrador encontrado. ¿Restaurar?",
3962
+ foundAt: "Borrador del {date}. ¿Restaurar?",
3963
+ restore: "Restaurar",
3964
+ discard: "Descartar"
3713
3965
  }
3714
3966
  },
3715
3967
  ko: {
@@ -3853,6 +4105,7 @@
3853
4105
  findPlaceholder: "찾기…",
3854
4106
  searchAriaLabel: "검색 텍스트",
3855
4107
  caseSensitive: "\xA0대소문자 구분",
4108
+ wholeWord: "전체 단어",
3856
4109
  prevBtn: "← 이전",
3857
4110
  nextBtn: "다음 →",
3858
4111
  replacePlaceholder: "바꿀 내용…",
@@ -4020,7 +4273,16 @@
4020
4273
  columnWidth: "열 너비",
4021
4274
  rowHeight: "행 높이",
4022
4275
  tableBorderWidth: "표 테두리 너비",
4276
+ tableBorderColor: "표 테두리 색상",
4023
4277
  deleteTable: "표 삭제",
4278
+ cellAlignLeft: "왼쪽 정렬",
4279
+ cellAlignCenter: "가운데 정렬",
4280
+ cellAlignRight: "오른쪽 정렬",
4281
+ cellAlignJustify: "양쪽 정렬",
4282
+ toggleHeaderRow: "머리글 행 전환",
4283
+ cellBackground: "셀 배경색",
4284
+ noShading: "배경 없음",
4285
+ noBorderColor: "테두리 색 없음",
4024
4286
  columnWidthPx: "열 너비 (px)",
4025
4287
  rowHeightPx: "행 높이 (px)",
4026
4288
  tableBorderWidthPx: "표 테두리 너비 (px)",
@@ -4048,6 +4310,12 @@
4048
4310
  errors: {
4049
4311
  imageFormat: (type) => `"${type}" 형식은 웹 브라우저에서 지원되지 않습니다. JPEG, PNG 또는 WebP로 변환해 주세요.`,
4050
4312
  imageSize: (maxSize) => `이미지 파일이 너무 큽니다. 최대 허용 크기는 ${maxSize} MB입니다.`
4313
+ },
4314
+ autoSaveRestore: {
4315
+ found: "초안을 찾았습니다. 복원하시겠습니까?",
4316
+ foundAt: "{date}의 초안을 찾았습니다. 복원하시겠습니까?",
4317
+ restore: "복원",
4318
+ discard: "삭제"
4051
4319
  }
4052
4320
  }
4053
4321
  };
@@ -4532,7 +4800,11 @@
4532
4800
  const sel = globalThis.getSelection();
4533
4801
  if (!sel || sel.rangeCount === 0) return;
4534
4802
  const range = sel.getRangeAt(0);
4535
- range.deleteContents();
4803
+ try {
4804
+ range.deleteContents();
4805
+ } catch (_) {
4806
+ return;
4807
+ }
4536
4808
  const BLOCK = new Set([
4537
4809
  "P",
4538
4810
  "DIV",
@@ -4557,7 +4829,11 @@
4557
4829
  table.after(p);
4558
4830
  }
4559
4831
  if (!anchor.textContent.trim() && !anchor.querySelector("img, video, table")) anchor.remove();
4560
- } else range.insertNode(table);
4832
+ } else try {
4833
+ range.insertNode(table);
4834
+ } catch (_) {
4835
+ return;
4836
+ }
4561
4837
  const firstCell = table.querySelector("td, th");
4562
4838
  if (firstCell) {
4563
4839
  const nr = document.createRange();
@@ -4640,6 +4916,23 @@
4640
4916
  var isFAIcon = (n) => !!(n?.nodeName === "I" && _FA_PATTERN.test(n.className || ""));
4641
4917
  var isZwsAnchor = (n) => !!(n?.nodeType === Node.TEXT_NODE && (n.textContent === "​" || n.textContent === ""));
4642
4918
  /**
4919
+ * Extracts the content from `startContainer:startOffset` to the end of `li`.
4920
+ * Returns an empty fragment if the range is invalid (e.g. detached node).
4921
+ * @param {Range} nativeRange
4922
+ * @param {Element} li
4923
+ * @returns {DocumentFragment}
4924
+ */
4925
+ function extractAfterContent(nativeRange, li) {
4926
+ try {
4927
+ const r = document.createRange();
4928
+ r.setStart(nativeRange.startContainer, nativeRange.startOffset);
4929
+ r.setEnd(li, li.childNodes.length);
4930
+ return r.extractContents();
4931
+ } catch (_) {
4932
+ return document.createDocumentFragment();
4933
+ }
4934
+ }
4935
+ /**
4643
4936
  * Handles special keydown behaviour inside the editor.
4644
4937
  * @param {KeyboardEvent} event
4645
4938
  * @param {HTMLElement} editable
@@ -4847,12 +5140,10 @@
4847
5140
  }
4848
5141
  if (!nativeRange.collapsed) {
4849
5142
  nativeRange.deleteContents();
5143
+ if (sel.rangeCount === 0 || !checkLi.isConnected) return true;
4850
5144
  nativeRange = sel.getRangeAt(0);
4851
5145
  }
4852
- const afterRange = document.createRange();
4853
- afterRange.setStart(nativeRange.startContainer, nativeRange.startOffset);
4854
- afterRange.setEnd(checkLi, checkLi.childNodes.length);
4855
- const afterFrag = afterRange.extractContents();
5146
+ const afterFrag = extractAfterContent(nativeRange, checkLi);
4856
5147
  const newLi = document.createElement("li");
4857
5148
  const cb = document.createElement("input");
4858
5149
  cb.type = "checkbox";
@@ -4968,7 +5259,15 @@
4968
5259
  const items = Array.from(el.querySelectorAll(":scope > li"));
4969
5260
  if (!items.length) return inner();
4970
5261
  const indent = " ".repeat(depth);
4971
- const lines = items.map((li) => `${indent}- ${_domToMd(li, depth + 1).trim()}`).join("\n");
5262
+ const isChecklist = el.classList.contains("an-checklist");
5263
+ const lines = items.map((li) => {
5264
+ let prefix = "- ";
5265
+ if (isChecklist) {
5266
+ const cb = li.querySelector("input[type=\"checkbox\"]");
5267
+ prefix = (cb ? cb.checked : false) ? "- [x] " : "- [ ] ";
5268
+ }
5269
+ return `${indent}${prefix}${_domToMd(li, depth + 1).trim()}`;
5270
+ }).join("\n");
4972
5271
  return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
4973
5272
  }
4974
5273
  case "ol": {
@@ -5008,7 +5307,7 @@
5008
5307
  * @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
5009
5308
  */
5010
5309
  function isMarkdown(text) {
5011
- return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|^```|^\*{2}.+?\*{2}/m.test(text);
5310
+ return /^#{1,6} [^\s]|^[ \t]*[-*+] [^\s]|^[ \t]*\d+\. [^\s]|^> [^\s]|^```|^\*{2}[^*\n]+\*{2}/m.test(text);
5012
5311
  }
5013
5312
  /**
5014
5313
  * Converts a Markdown string to an HTML string.
@@ -5058,11 +5357,20 @@
5058
5357
  }
5059
5358
  if (/^[-*+] /.test(line)) {
5060
5359
  const items = [];
5360
+ const isChecklist = /^[-*+]\s+\[[ xX]\]\s+/.test(line);
5361
+ const listTag = isChecklist ? "ul class=\"an-checklist\"" : "ul";
5061
5362
  while (i < lines.length && /^[-*+] /.test(lines[i])) {
5062
- items.push(`<li>${_inline(lines[i].slice(2))}</li>`);
5363
+ if (/^[-*+]\s+\[[ xX]\]\s+/.test(lines[i]) !== isChecklist) break;
5364
+ const content = lines[i].slice(2);
5365
+ if (isChecklist) {
5366
+ const cbMatch = /^\[([ xX])\][ \t]+/.exec(content);
5367
+ const cbHtml = `<input type="checkbox" contenteditable="false"${cbMatch?.[1]?.toLowerCase() === "x" ? " checked" : ""}>`;
5368
+ const textContent = cbMatch ? content.slice(cbMatch[0].length) : content;
5369
+ items.push(`<li>${cbHtml}${_inline(textContent)}</li>`);
5370
+ } else items.push(`<li>${_inline(content)}</li>`);
5063
5371
  i++;
5064
5372
  }
5065
- out.push(`<ul>${items.join("")}</ul>`);
5373
+ out.push(`<${listTag}>${items.join("")}</${listTag.split(" ")[0]}>`);
5066
5374
  continue;
5067
5375
  }
5068
5376
  if (/^\d+\. /.test(line)) {
@@ -5119,7 +5427,7 @@
5119
5427
  text = text.replace(/_{2}([^_\n]+?)_{2}/g, (_, c) => `<strong>${_esc(c)}</strong>`);
5120
5428
  text = text.replace(/\*([^*\n]+?)\*/g, (_, c) => `<em>${_esc(c)}</em>`);
5121
5429
  text = text.replace(/_([^_\n]+?)_/g, (_, c) => `<em>${_esc(c)}</em>`);
5122
- text = text.replace(/~~([^\n]+?)~~/g, (_, c) => `<del>${_esc(c)}</del>`);
5430
+ text = text.replace(/~~([^~\n]+?)~~/g, (_, c) => `<del>${_esc(c)}</del>`);
5123
5431
  text = text.replace(/`([^`]+)`/g, (_, c) => `<code>${_esc(c)}</code>`);
5124
5432
  return text;
5125
5433
  }
@@ -6673,6 +6981,15 @@
6673
6981
  if (!clipboardData) return;
6674
6982
  const forcePlain = this._forcePlain;
6675
6983
  this._forcePlain = false;
6984
+ const maxBytes = (this.options.maxPasteSize ?? 5) * 1024 * 1024;
6985
+ if (maxBytes > 0) {
6986
+ const text = clipboardData.getData("text/plain") || "";
6987
+ const html = clipboardData.getData("text/html") || "";
6988
+ if (Math.max(text.length, html.length) > maxBytes) {
6989
+ event.preventDefault();
6990
+ return;
6991
+ }
6992
+ }
6676
6993
  if (clipboardData.items) {
6677
6994
  const imageItems = Array.from(clipboardData.items).filter((item) => item.kind === "file" && item.type.startsWith("image/"));
6678
6995
  if (imageItems.length > 0) {
@@ -6809,7 +7126,7 @@
6809
7126
  const mime = /:(.*?);/.exec(header)?.[1] ?? "image/png";
6810
7127
  const binary = atob(b64);
6811
7128
  const arr = new Uint8Array(binary.length);
6812
- for (let i = 0; i < binary.length; i++) arr[i] = binary.codePointAt(i);
7129
+ for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
6813
7130
  return new Blob([arr], { type: mime });
6814
7131
  }
6815
7132
  /**
@@ -7415,6 +7732,7 @@
7415
7732
  return overlay;
7416
7733
  }
7417
7734
  _onFileChange() {
7735
+ if (this.context._alive === false) return;
7418
7736
  const file = this._fileInput?.files?.[0];
7419
7737
  if (!file?.type?.startsWith("image/")) return;
7420
7738
  if (!new Set([
@@ -7795,6 +8113,7 @@
7795
8113
  const aspectRatio = startW / (startH || 1);
7796
8114
  const isCorner = pos.length === 2;
7797
8115
  const editable = this.context.layoutInfo.editable;
8116
+ const minSz = this.context.options?.minImageSize ?? 20;
7798
8117
  let _raf = null;
7799
8118
  const onMove = (me) => {
7800
8119
  if (_raf !== null) return;
@@ -7807,15 +8126,15 @@
7807
8126
  const maxW = editable.clientWidth || Infinity;
7808
8127
  let newW = startW;
7809
8128
  let newH = startH;
7810
- if (pos.includes("e")) newW = Math.max(20, startW + dx);
7811
- if (pos.includes("w")) newW = Math.max(20, startW - dx);
7812
- if (pos.includes("s")) newH = Math.max(20, startH + dy);
7813
- if (pos.includes("n")) newH = Math.max(20, startH - dy);
8129
+ if (pos.includes("e")) newW = Math.max(minSz, startW + dx);
8130
+ if (pos.includes("w")) newW = Math.max(minSz, startW - dx);
8131
+ if (pos.includes("s")) newH = Math.max(minSz, startH + dy);
8132
+ if (pos.includes("n")) newH = Math.max(minSz, startH - dy);
7814
8133
  newW = Math.min(newW, maxW);
7815
- if (isCorner) if (Math.abs(dx) >= Math.abs(dy)) newH = Math.max(20, Math.round(newW / aspectRatio));
8134
+ if (isCorner) if (Math.abs(dx) >= Math.abs(dy)) newH = Math.max(minSz, Math.round(newW / aspectRatio));
7816
8135
  else {
7817
- newW = Math.min(Math.max(20, Math.round(newH * aspectRatio)), maxW);
7818
- newH = Math.max(20, Math.round(newW / aspectRatio));
8136
+ newW = Math.min(Math.max(minSz, Math.round(newH * aspectRatio)), maxW);
8137
+ newH = Math.max(minSz, Math.round(newW / aspectRatio));
7819
8138
  }
7820
8139
  img.style.width = `${newW}px`;
7821
8140
  img.style.height = `${newH}px`;
@@ -9979,8 +10298,28 @@
9979
10298
  return direction === "asc" ? aText.localeCompare(bText) : bText.localeCompare(aText);
9980
10299
  });
9981
10300
  rows.forEach((row) => tbody.appendChild(row));
10301
+ this._markSortIndicator(table, colIdx, direction);
9982
10302
  this.context.invoke("editor.afterCommand");
9983
10303
  }
10304
+ /**
10305
+ * Marks the header cell of the sorted column with `an-sort-asc`/`an-sort-desc`
10306
+ * so the active sort column and direction are visible, and clears any previous
10307
+ * indicator. No-op for tables without a `<thead>` (no header row to mark).
10308
+ * @param {HTMLTableElement} table
10309
+ * @param {number} colIdx
10310
+ * @param {'asc'|'desc'} direction
10311
+ */
10312
+ _markSortIndicator(table, colIdx, direction) {
10313
+ const thead = table.querySelector("thead");
10314
+ if (!thead) return;
10315
+ thead.querySelectorAll(".an-sort-asc, .an-sort-desc").forEach((el) => {
10316
+ el.classList.remove("an-sort-asc", "an-sort-desc");
10317
+ });
10318
+ const headerRow = thead.querySelector("tr");
10319
+ if (!headerRow) return;
10320
+ const headerCell = getCellAtVisualCol(headerRow, colIdx);
10321
+ if (headerCell) headerCell.classList.add(direction === "asc" ? "an-sort-asc" : "an-sort-desc");
10322
+ }
9984
10323
  _exportTableCSV() {
9985
10324
  const table = this._activeTable;
9986
10325
  if (!table) return;
@@ -14258,6 +14597,7 @@
14258
14597
  this._currentIndex = -1;
14259
14598
  this._caseSensitive = false;
14260
14599
  this._useRegex = false;
14600
+ this._wholeWord = false;
14261
14601
  /** @type {'find'|'replace'} */
14262
14602
  this._mode = "find";
14263
14603
  /** Cached compiled regex — reused when query and case-sensitivity are unchanged */
@@ -14265,6 +14605,7 @@
14265
14605
  this._lastQuery = null;
14266
14606
  this._lastCaseSensitive = null;
14267
14607
  this._lastUseRegex = null;
14608
+ this._lastWholeWord = null;
14268
14609
  this._focusTimer = null;
14269
14610
  }
14270
14611
  destroy() {
@@ -14375,6 +14716,13 @@
14375
14716
  "aria-label": L.useRegex
14376
14717
  });
14377
14718
  regexBtn.textContent = ".*";
14719
+ const wholeWordBtn = createElement("button", {
14720
+ type: "button",
14721
+ class: "an-fr-icon-btn",
14722
+ title: L.wholeWord,
14723
+ "aria-label": L.wholeWord
14724
+ });
14725
+ wholeWordBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="18" height="10" rx="2"/><line x1="7" y1="21" x2="7" y2="17"/><line x1="17" y1="21" x2="17" y2="17"/></svg>`;
14378
14726
  const prevBtn = createElement("button", {
14379
14727
  type: "button",
14380
14728
  class: "an-fr-icon-btn",
@@ -14391,7 +14739,7 @@
14391
14739
  nextBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>`;
14392
14740
  const counter = createElement("span", { class: "an-fr-counter" });
14393
14741
  this._counterEl = counter;
14394
- searchBar.append(findInput, caseCheckbox, caseBtn, regexBtn, prevBtn, nextBtn, counter);
14742
+ searchBar.append(findInput, caseCheckbox, caseBtn, regexBtn, wholeWordBtn, prevBtn, nextBtn, counter);
14395
14743
  box.appendChild(searchBar);
14396
14744
  const replaceRow = createElement("div", { class: "an-fr-replace-row" });
14397
14745
  replaceRow.style.display = "none";
@@ -14459,7 +14807,14 @@
14459
14807
  this._lastQuery = null;
14460
14808
  this._onSearch();
14461
14809
  });
14462
- this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, dRegex);
14810
+ const dWholeWord = on(wholeWordBtn, "click", () => {
14811
+ this._wholeWord = !this._wholeWord;
14812
+ wholeWordBtn.classList.toggle("an-fr-icon-btn--active", this._wholeWord);
14813
+ this._queryRegex = null;
14814
+ this._lastQuery = null;
14815
+ this._onSearch();
14816
+ });
14817
+ this._disposers.push(d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, dRegex, dWholeWord);
14463
14818
  return overlay;
14464
14819
  }
14465
14820
  _onSearch() {
@@ -14517,10 +14872,11 @@
14517
14872
  */
14518
14873
  _findRawMatches(query, root) {
14519
14874
  const results = [];
14520
- if (this._lastQuery !== query || this._lastCaseSensitive !== this._caseSensitive || this._lastUseRegex !== this._useRegex) {
14875
+ if (this._lastQuery !== query || this._lastCaseSensitive !== this._caseSensitive || this._lastUseRegex !== this._useRegex || this._lastWholeWord !== this._wholeWord) {
14521
14876
  const flags = this._caseSensitive ? "g" : "gi";
14522
14877
  try {
14523
- const pattern = this._useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
14878
+ let pattern = this._useRegex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
14879
+ if (this._wholeWord) pattern = `\\b${pattern}\\b`;
14524
14880
  this._queryRegex = new RegExp(pattern, flags);
14525
14881
  } catch (_) {
14526
14882
  this._queryRegex = null;
@@ -14528,6 +14884,7 @@
14528
14884
  this._lastQuery = query;
14529
14885
  this._lastCaseSensitive = this._caseSensitive;
14530
14886
  this._lastUseRegex = this._useRegex;
14887
+ this._lastWholeWord = this._wholeWord;
14531
14888
  }
14532
14889
  if (!this._queryRegex) return results;
14533
14890
  const re = this._queryRegex;
@@ -15881,6 +16238,7 @@
15881
16238
  * debounce: 200,
15882
16239
  * onSearch: (query, callback) => void,
15883
16240
  * onInsert: (item) => string | null,
16241
+ * onError: (err: Error) => void,
15884
16242
  * mentionClass: 'an-mention',
15885
16243
  * allowSpaces: false,
15886
16244
  * }
@@ -16073,8 +16431,18 @@
16073
16431
  this._renderItems(items);
16074
16432
  this._showDropdown();
16075
16433
  };
16076
- const result = this._cfg.onSearch(this._query, cb);
16077
- if (result && typeof result.then === "function") result.then(cb).catch(() => this._hideDropdown());
16434
+ let result;
16435
+ try {
16436
+ result = this._cfg.onSearch(this._query, cb);
16437
+ } catch (err) {
16438
+ this._hideDropdown();
16439
+ if (typeof this._cfg.onError === "function") this._cfg.onError(err);
16440
+ return;
16441
+ }
16442
+ if (result && typeof result.then === "function") result.then(cb).catch((err) => {
16443
+ this._hideDropdown();
16444
+ if (typeof this._cfg.onError === "function") this._cfg.onError(err);
16445
+ });
16078
16446
  }, this._cfg.debounce);
16079
16447
  }
16080
16448
  _onKeydown(e) {
@@ -16342,10 +16710,13 @@
16342
16710
  }
16343
16711
  /**
16344
16712
  * Returns the current HTML content of the editor.
16713
+ * Zero-width spaces (U+200B) inserted by inline editing helpers are stripped
16714
+ * from the output so they don't leak into the consumer's HTML.
16345
16715
  * @returns {string}
16346
16716
  */
16347
16717
  getHTML() {
16348
- return this.invoke("editor.getHTML");
16718
+ const html = this.invoke("editor.getHTML");
16719
+ return typeof html === "string" ? html.replace(/​/g, "") : html;
16349
16720
  }
16350
16721
  /**
16351
16722
  * Sets the HTML content of the editor.
@@ -16516,6 +16887,25 @@
16516
16887
  }));
16517
16888
  }
16518
16889
  /**
16890
+ * Moves focus into the editable area.
16891
+ */
16892
+ focus() {
16893
+ this.layoutInfo.editable.focus();
16894
+ }
16895
+ /**
16896
+ * Removes focus from the editable area.
16897
+ */
16898
+ blur() {
16899
+ this.layoutInfo.editable.blur();
16900
+ }
16901
+ /**
16902
+ * Returns true when the editor is currently in fullscreen mode.
16903
+ * @returns {boolean}
16904
+ */
16905
+ isFullscreen() {
16906
+ return this.invoke("fullscreen.isActive") === true;
16907
+ }
16908
+ /**
16519
16909
  * Sets whether the editor is disabled (readonly).
16520
16910
  * @param {boolean} disabled
16521
16911
  */
@@ -16685,8 +17075,10 @@
16685
17075
  registerButton(btnDef);
16686
17076
  return this;
16687
17077
  },
17078
+ /** All pre-built button definitions — accessible in every module format including UMD/CJS. */
17079
+ buttons,
16688
17080
  /** Library version */
16689
- version: "1.7.0"
17081
+ version: "1.8.0"
16690
17082
  };
16691
17083
  /**
16692
17084
  * @param {string|Element|NodeList|Element[]} selector