autumnnote 1.2.1 → 1.4.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.
@@ -27,19 +27,29 @@ function debounce(fn, delay) {
27
27
  };
28
28
  }
29
29
  /**
30
- * Throttle a function call.
31
- * @param {Function} fn
32
- * @param {number} limit - milliseconds
33
- * @returns {Function}
30
+ * Create a wrapper that limits how often `fn` can be invoked while ensuring the last call in a burst is executed.
31
+ * @param {Function} fn - Function to be throttled.
32
+ * @param {number} limit - Time window in milliseconds during which at most one call is allowed.
33
+ * @returns {Function} A wrapper function that invokes `fn` at most once per `limit` milliseconds; calls preserve `this` and original arguments and schedule a trailing invocation for the final call in a burst.
34
34
  */
35
35
  function throttle(fn, limit) {
36
- let lastCall = 0;
36
+ let lastCall = -Infinity;
37
+ let trailingTimer = null;
37
38
  return function(...args) {
38
- const now = Date.now();
39
- if (now - lastCall >= limit) {
39
+ const now = performance.now();
40
+ const elapsed = now - lastCall;
41
+ if (elapsed >= limit) {
40
42
  lastCall = now;
43
+ clearTimeout(trailingTimer);
44
+ trailingTimer = null;
41
45
  return fn.apply(this, args);
42
46
  }
47
+ clearTimeout(trailingTimer);
48
+ trailingTimer = setTimeout(() => {
49
+ lastCall = performance.now();
50
+ trailingTimer = null;
51
+ fn.apply(this, args);
52
+ }, limit - elapsed);
43
53
  };
44
54
  }
45
55
  /**
@@ -118,12 +128,12 @@ function isPlainObject(val) {
118
128
  function rect2bnd(rect) {
119
129
  if (!rect) return null;
120
130
  return {
121
- top: Math.round(rect.top),
122
- left: Math.round(rect.left),
123
- width: Math.round(rect.width),
124
- height: Math.round(rect.height),
125
- bottom: Math.round(rect.bottom),
126
- right: Math.round(rect.right)
131
+ top: rect.top,
132
+ left: rect.left,
133
+ width: rect.width,
134
+ height: rect.height,
135
+ bottom: rect.bottom,
136
+ right: rect.right
127
137
  };
128
138
  }
129
139
  //#endregion
@@ -283,13 +293,16 @@ function nodeValue(node) {
283
293
  return isText(node) ? node.nodeValue : node.textContent || "";
284
294
  }
285
295
  /**
286
- * Returns true if a node is empty (no visible content).
287
- * @param {Node} node
288
- * @returns {boolean}
296
+ * Determine whether a DOM node contains no visible content.
297
+ *
298
+ * Text nodes are considered empty when their `nodeValue` is empty. Void elements (e.g., `img`, `br`, `input`) are considered non-empty. An element with exactly one `<br>` child is treated as empty. For other elements, emptiness means trimmed `textContent` is empty and there are no descendant `img`, `video`, `hr`, or `table` elements.
299
+ * @param {Node} node - Node to inspect for visible content.
300
+ * @returns {boolean} `true` if the node has no visible content, `false` otherwise.
289
301
  */
290
302
  function isEmpty(node) {
291
303
  if (isText(node)) return !node.nodeValue;
292
304
  if (isVoid(node)) return false;
305
+ if (node.childNodes.length === 1 && node.firstChild?.nodeName === "BR") return true;
293
306
  return !node.textContent.trim() && !node.querySelector("img, video, hr, table");
294
307
  }
295
308
  /**
@@ -701,9 +714,15 @@ function outdent() {
701
714
  execCommand("outdent");
702
715
  }
703
716
  /**
704
- * G.5 helper: splits a checklist at checkLi, converts it to a <p>,
705
- * and keeps items before/after as separate checklists.
706
- * @param {HTMLElement} checkLi
717
+ * Convert a checklist <li> into a paragraph and move any following items into a new checklist.
718
+ *
719
+ * Preserves inline markup from the converted item, strips zero-width space anchors,
720
+ * and replaces empty content with a non‑breaking space. If there are list items
721
+ * after the converted item they are moved into a new <ul class="an-checklist">
722
+ * inserted immediately after the original list. The original <li> is removed and
723
+ * the original list is removed if it becomes empty. Attempts to place the caret
724
+ * at the start of the newly created <p>.
725
+ * @param {HTMLElement} checkLi - The checklist `<li>` element to convert to a `<p>`.
707
726
  */
708
727
  function _checklistItemToP(checkLi) {
709
728
  const checkUl = checkLi.closest(".an-checklist");
@@ -712,7 +731,15 @@ function _checklistItemToP(checkLi) {
712
731
  const liIndex = allLis.indexOf(checkLi);
713
732
  const afterLis = allLis.slice(liIndex + 1);
714
733
  const p = document.createElement("p");
715
- p.textContent = Array.from(checkLi.childNodes).filter((n) => !(n.nodeType === 1 && n.tagName === "INPUT")).map((n) => n.textContent).join("").replace(/\u200B/g, "").trim() || "\xA0";
734
+ for (const child of checkLi.childNodes) {
735
+ if (child.nodeType === 1 && child.tagName === "INPUT") continue;
736
+ p.appendChild(child.cloneNode(true));
737
+ }
738
+ p.innerHTML = p.innerHTML.replace(/\u200B/g, "");
739
+ if (!p.hasChildNodes() || !p.textContent.trim()) {
740
+ p.innerHTML = "";
741
+ p.appendChild(document.createTextNode("\xA0"));
742
+ }
716
743
  if (afterLis.length > 0) {
717
744
  const newUl = document.createElement("ul");
718
745
  newUl.className = "an-checklist";
@@ -743,9 +770,12 @@ var insertUnorderedList = () => execCommand("insertUnorderedList");
743
770
  */
744
771
  var insertOrderedList = () => execCommand("insertOrderedList");
745
772
  /**
746
- * Applies a line-height value to every block-level element that intersects
747
- * the current selection.
748
- * @param {string} value - unitless multiplier, e.g. '1.5'
773
+ * Set the line-height on every block-level element that intersects the current selection.
774
+ *
775
+ * If the selection is collapsed, the nearest enclosing block element receives the style.
776
+ * For a non-collapsed selection, all unique block ancestors of text nodes that intersect the range are updated;
777
+ * if none are found, the nearest block ancestor of the range's common ancestor is updated.
778
+ * @param {string} value - Line-height value to apply; typically a unitless multiplier (for example, "1.5").
749
779
  */
750
780
  function lineHeight(value) {
751
781
  const sel = window.getSelection();
@@ -780,9 +810,9 @@ function lineHeight(value) {
780
810
  return;
781
811
  }
782
812
  const blocks = /* @__PURE__ */ new Set();
783
- const iter = document.createNodeIterator(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, null);
813
+ const iter = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, { acceptNode: (node) => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP });
784
814
  let textNode;
785
- while (textNode = iter.nextNode()) if (range.intersectsNode(textNode)) {
815
+ while (textNode = iter.nextNode()) {
786
816
  const block = nearestBlock(textNode);
787
817
  if (block) blocks.add(block);
788
818
  }
@@ -864,9 +894,18 @@ function isInlineCode() {
864
894
  return !!(code && !code.closest("pre"));
865
895
  }
866
896
  /**
867
- * Toggles a task-list at the cursor.
868
- * If inside a checklist <li>, converts it (and any other selected items) back to <p> elements.
869
- * Otherwise inserts a new <ul class="an-checklist"> with one item per selected line.
897
+ * Toggle a checklist at the current selection or caret.
898
+ *
899
+ * When the selection is inside an existing checklist `<ul class="an-checklist">`,
900
+ * converts the selected `<li>` items back into `<p>` paragraphs and places the caret
901
+ * at the start of the first converted paragraph. Otherwise creates a checklist:
902
+ * - If the selection is collapsed, converts the nearest block-level ancestor (or inserts
903
+ * a single checklist item at the editable root) into a checklist with one item containing
904
+ * that block's text and places the caret inside the new item.
905
+ * - If the selection is a range, converts each intersecting block element into one checklist
906
+ * item (preserving textual content) and places the caret at the end of the last item.
907
+ *
908
+ * Empty or whitespace-only selections do not create a checklist.
870
909
  */
871
910
  function toggleChecklist() {
872
911
  const sel = window.getSelection();
@@ -925,8 +964,9 @@ function toggleChecklist() {
925
964
  ul.appendChild(li);
926
965
  if (block && BLOCK_TAGS.has(block.tagName)) block.parentNode.replaceChild(ul, block);
927
966
  else {
928
- document.execCommand("insertHTML", false, ul.outerHTML);
929
- return;
967
+ const nativeRange = sel.getRangeAt(0);
968
+ nativeRange.deleteContents();
969
+ nativeRange.insertNode(ul);
930
970
  }
931
971
  const textNode = li.lastChild;
932
972
  const nr = document.createRange();
@@ -1049,6 +1089,34 @@ function btn(name, icon, tooltip, action, isActive, isDisabled) {
1049
1089
  isDisabled
1050
1090
  };
1051
1091
  }
1092
+ /**
1093
+ * Global registry for custom buttons registered via AutumnNote.registerButton()
1094
+ * or via a plugin's `buttons` array. Toolbar resolves string names from here.
1095
+ * @type {Map<string, object>}
1096
+ */
1097
+ var _buttonRegistry = /* @__PURE__ */ new Map();
1098
+ /**
1099
+ * Registers a button definition in the global registry so it can be referenced
1100
+ * by string name in toolbar configuration: `toolbar: [['myBtn', boldBtn]]`.
1101
+ * @param {object} btnDef - Any ToolbarItemDef-compatible object with a `name` string.
1102
+ */
1103
+ function registerButton(btnDef) {
1104
+ if (!btnDef || typeof btnDef.name !== "string") {
1105
+ console.warn("[AutumnNote] registerButton: btnDef must have a string `name` property.");
1106
+ return;
1107
+ }
1108
+ if (_buttonRegistry.has(btnDef.name)) console.warn(`[AutumnNote] registerButton: overwriting existing button "${btnDef.name}".`);
1109
+ _buttonRegistry.set(btnDef.name, btnDef);
1110
+ }
1111
+ /**
1112
+ * Looks up a button definition by name from the global registry.
1113
+ * Returns undefined when not found.
1114
+ * @param {string} name
1115
+ * @returns {object|undefined}
1116
+ */
1117
+ function getButton(name) {
1118
+ return _buttonRegistry.get(name);
1119
+ }
1052
1120
  var boldBtn = btn("bold", "bold", "Bold (Ctrl+B)", () => bold(), () => document.queryCommandState("bold"));
1053
1121
  var italicBtn = btn("italic", "italic", "Italic (Ctrl+I)", () => italic(), () => document.queryCommandState("italic"));
1054
1122
  var underlineBtn = btn("underline", "underline", "Underline (Ctrl+U)", () => underline(), () => {
@@ -1614,6 +1682,7 @@ var en = {
1614
1682
  replaceAriaLabel: "Replace with",
1615
1683
  replaceBtn: "Replace",
1616
1684
  replaceAllBtn: "Replace All",
1685
+ noResults: "No results",
1617
1686
  close: "×"
1618
1687
  },
1619
1688
  shortcutsDialog: {
@@ -1966,6 +2035,7 @@ var locales = {
1966
2035
  replaceAriaLabel: "Thay thế bằng",
1967
2036
  replaceBtn: "Thay thế",
1968
2037
  replaceAllBtn: "Thay thế tất cả",
2038
+ noResults: "Không có kết quả",
1969
2039
  close: "×"
1970
2040
  },
1971
2041
  shortcutsDialog: {
@@ -2295,6 +2365,7 @@ var locales = {
2295
2365
  replacePlaceholder: "置換後…",
2296
2366
  replaceAriaLabel: "置換後のテキスト",
2297
2367
  replaceBtn: "置換",
2368
+ noResults: "結果なし",
2298
2369
  replaceAllBtn: "すべて置換",
2299
2370
  close: "×"
2300
2371
  },
@@ -2625,6 +2696,7 @@ var locales = {
2625
2696
  replacePlaceholder: "替换为…",
2626
2697
  replaceAriaLabel: "替换为",
2627
2698
  replaceBtn: "替换",
2699
+ noResults: "没有结果",
2628
2700
  replaceAllBtn: "全部替换",
2629
2701
  close: "×"
2630
2702
  },
@@ -2955,6 +3027,7 @@ var locales = {
2955
3027
  replacePlaceholder: "Remplacer par…",
2956
3028
  replaceAriaLabel: "Remplacer par",
2957
3029
  replaceBtn: "Remplacer",
3030
+ noResults: "Aucun résultat",
2958
3031
  replaceAllBtn: "Tout remplacer",
2959
3032
  close: "×"
2960
3033
  },
@@ -3285,6 +3358,7 @@ var locales = {
3285
3358
  replacePlaceholder: "Ersetzen durch…",
3286
3359
  replaceAriaLabel: "Ersetzen durch",
3287
3360
  replaceBtn: "Ersetzen",
3361
+ noResults: "Keine Ergebnisse",
3288
3362
  replaceAllBtn: "Alle ersetzen",
3289
3363
  close: "×"
3290
3364
  },
@@ -3615,6 +3689,7 @@ var locales = {
3615
3689
  replacePlaceholder: "Reemplazar con…",
3616
3690
  replaceAriaLabel: "Reemplazar con",
3617
3691
  replaceBtn: "Reemplazar",
3692
+ noResults: "Sin resultados",
3618
3693
  replaceAllBtn: "Reemplazar todo",
3619
3694
  close: "×"
3620
3695
  },
@@ -3945,6 +4020,7 @@ var locales = {
3945
4020
  replacePlaceholder: "바꿀 내용…",
3946
4021
  replaceAriaLabel: "바꿀 내용",
3947
4022
  replaceBtn: "바꾸기",
4023
+ noResults: "결과 없음",
3948
4024
  replaceAllBtn: "모두 바꾸기",
3949
4025
  close: "×"
3950
4026
  },
@@ -4165,8 +4241,10 @@ var PROHIBITED_TAGS = [
4165
4241
  "object",
4166
4242
  "embed",
4167
4243
  "form",
4168
- "button"
4244
+ "base"
4169
4245
  ];
4246
+ /** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
4247
+ var UNWRAP_TAGS = new Set(["button"]);
4170
4248
  /** Attributes whose values must be sanitised as URLs. */
4171
4249
  var URL_ATTRS = [
4172
4250
  "href",
@@ -4184,60 +4262,61 @@ var TRUSTED_IFRAME_HOSTS = new Set([
4184
4262
  "player.vimeo.com"
4185
4263
  ]);
4186
4264
  /**
4187
- * Sanitises an HTML string by removing dangerous elements and attributes.
4188
- * Uses DOMParser so the sanitisation follows normal browser parsing rules —
4189
- * no regex shortcuts that can be bypassed by encoding tricks.
4265
+ * Produce a sanitized HTML string with dangerous elements and attributes removed.
4190
4266
  *
4191
- * - Strips PROHIBITED_TAGS (script, style, iframe, object, embed, form, button)
4192
- * - Allows input[type="checkbox"] only inside ul.an-checklist li; removes all other <input>
4193
- * - Removes all on* event-handler attributes
4194
- * - Rejects javascript: and vbscript: URLs in URL attributes
4195
- * - Rejects data: URIs everywhere except img[src] (base64 uploads)
4267
+ * Removes disallowed tags and wrappers, strips event-handler attributes, rejects
4268
+ * `javascript:`/`vbscript:` URLs and most `data:` URIs, restricts iframe `src`
4269
+ * to trusted hosts when enabled, and permits only checklist checkboxes as inputs.
4196
4270
  *
4197
- * @param {string} html
4198
- * @returns {string}
4271
+ * @param {string} html - HTML fragment to sanitize.
4272
+ * @param {Object} [options]
4273
+ * @param {boolean} [options.allowIframes=false] - If true, `iframe` elements are not removed but their `src` is restricted to trusted hosts and `srcdoc` is removed.
4274
+ * @returns {string} The sanitized HTML fragment.
4199
4275
  */
4200
4276
  function sanitiseHTML(html, { allowIframes = false } = {}) {
4201
4277
  const doc = new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html");
4202
- (allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS).forEach((tag) => {
4203
- doc.querySelectorAll(tag).forEach((el) => el.remove());
4204
- });
4205
- doc.querySelectorAll("*").forEach((el) => {
4206
- Array.from(el.attributes).forEach((attr) => {
4278
+ const allElements = Array.from(doc.querySelectorAll("*"));
4279
+ const prohibited = new Set(allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS);
4280
+ for (const el of allElements) {
4281
+ const tag = el.tagName.toLowerCase();
4282
+ if (UNWRAP_TAGS.has(tag)) {
4283
+ el.replaceWith(...el.childNodes);
4284
+ continue;
4285
+ }
4286
+ if (prohibited.has(tag)) {
4287
+ el.remove();
4288
+ continue;
4289
+ }
4290
+ for (const attr of Array.from(el.attributes)) {
4207
4291
  if (attr.name.startsWith("on")) {
4208
4292
  el.removeAttribute(attr.name);
4209
- return;
4293
+ continue;
4210
4294
  }
4211
4295
  if (URL_ATTRS.includes(attr.name)) {
4212
4296
  const val = attr.value.trim();
4213
4297
  if (/^(javascript|vbscript):/i.test(val)) {
4214
4298
  el.removeAttribute(attr.name);
4215
- return;
4299
+ continue;
4216
4300
  }
4217
4301
  if (/^data:/i.test(val) && !(attr.name === "src" && el.tagName === "IMG")) el.removeAttribute(attr.name);
4218
4302
  }
4219
4303
  if (el.tagName === "IFRAME") {
4220
4304
  if (attr.name === "srcdoc") {
4221
4305
  el.removeAttribute(attr.name);
4222
- return;
4223
- }
4224
- if (attr.name === "src") {
4225
- if (!isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
4226
- return;
4306
+ continue;
4227
4307
  }
4308
+ if (attr.name === "src" && !isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
4228
4309
  }
4229
- });
4230
- });
4231
- doc.querySelectorAll("input").forEach((el) => {
4232
- if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
4233
- else Array.from(el.attributes).forEach((attr) => {
4234
- if (![
4310
+ }
4311
+ if (tag === "input") {
4312
+ if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
4313
+ else for (const attr of Array.from(el.attributes)) if (![
4235
4314
  "type",
4236
4315
  "checked",
4237
4316
  "contenteditable"
4238
4317
  ].includes(attr.name)) el.removeAttribute(attr.name);
4239
- });
4240
- });
4318
+ }
4319
+ }
4241
4320
  return doc.body.innerHTML;
4242
4321
  }
4243
4322
  /**
@@ -4438,7 +4517,18 @@ var History = class {
4438
4517
  const sel = window.getSelection();
4439
4518
  sel.removeAllRanges();
4440
4519
  sel.addRange(range);
4441
- } catch (_) {}
4520
+ } catch (_) {
4521
+ try {
4522
+ const fb = document.createRange();
4523
+ fb.setStart(this.editable, 0);
4524
+ fb.collapse(true);
4525
+ const s = window.getSelection();
4526
+ if (s) {
4527
+ s.removeAllRanges();
4528
+ s.addRange(fb);
4529
+ }
4530
+ } catch (_2) {}
4531
+ }
4442
4532
  }
4443
4533
  _savePoint() {
4444
4534
  if (this.stackOffset < this.stack.length - 1) this.stack = this.stack.slice(0, this.stackOffset + 1);
@@ -4465,6 +4555,10 @@ var History = class {
4465
4555
  * @returns {{ html: string, images: Object<string,string> }}
4466
4556
  */
4467
4557
  _tokenizeImages(html) {
4558
+ if (!html.includes("data:")) return {
4559
+ html,
4560
+ images: {}
4561
+ };
4468
4562
  const images = {};
4469
4563
  let index = 0;
4470
4564
  return {
@@ -4536,11 +4630,12 @@ var History = class {
4536
4630
  * Inspired by Summernote's table handling
4537
4631
  */
4538
4632
  /**
4539
- * Creates a table element with the specified dimensions.
4540
- * @param {number} cols
4541
- * @param {number} rows
4542
- * @param {{ headerRow?: boolean }} [opts]
4543
- * @returns {HTMLTableElement}
4633
+ * Build an HTML table with the given number of columns and rows, optionally including a header row.
4634
+ * @param {number} cols - Number of columns in each row.
4635
+ * @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
4636
+ * @param {{ headerRow?: boolean }} [opts] - Options object.
4637
+ * @param {boolean} [opts.headerRow=false] - When true and `rows > 0`, creates a header row (`<thead>`) plus body rows for the remainder.
4638
+ * @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
4544
4639
  */
4545
4640
  function createTable(cols, rows, opts = {}) {
4546
4641
  const { headerRow = false } = opts;
@@ -4549,7 +4644,7 @@ function createTable(cols, rows, opts = {}) {
4549
4644
  const thead = createElement("thead");
4550
4645
  const tr = createElement("tr");
4551
4646
  for (let c = 0; c < cols; c++) {
4552
- const th = createElement("th", {}, ["\xA0"]);
4647
+ const th = createElement("th", {}, [document.createElement("br")]);
4553
4648
  tr.appendChild(th);
4554
4649
  }
4555
4650
  thead.appendChild(tr);
@@ -4561,7 +4656,7 @@ function createTable(cols, rows, opts = {}) {
4561
4656
  for (let r = 0; r < bodyRows; r++) {
4562
4657
  const tr = createElement("tr");
4563
4658
  for (let c = 0; c < cols; c++) {
4564
- const td = createElement("td", {}, ["\xA0"]);
4659
+ const td = createElement("td", {}, [document.createElement("br")]);
4565
4660
  tr.appendChild(td);
4566
4661
  }
4567
4662
  tbody.appendChild(tr);
@@ -4569,13 +4664,52 @@ function createTable(cols, rows, opts = {}) {
4569
4664
  return table;
4570
4665
  }
4571
4666
  /**
4572
- * Inserts a table at the current cursor position.
4573
- * @param {number} cols
4574
- * @param {number} rows
4575
- * @param {{ headerRow?: boolean }} [opts]
4667
+ * Insert a table at the current selection and place the caret into its first cell.
4668
+ * @param {number} cols - Number of columns for the new table.
4669
+ * @param {number} rows - Number of rows for the new table.
4670
+ * @param {{ headerRow?: boolean }} [opts] - Options for table creation.
4671
+ * @param {boolean} [opts.headerRow=false] - If true, include a header row as the first row.
4576
4672
  */
4577
4673
  function insertTable(cols, rows, opts = {}) {
4578
- execCommand("insertHTML", createTable(cols, rows, opts).outerHTML);
4674
+ if (cols <= 0 || rows <= 0) return;
4675
+ const table = createTable(cols, rows, opts);
4676
+ const sel = window.getSelection();
4677
+ if (!sel || sel.rangeCount === 0) return;
4678
+ const range = sel.getRangeAt(0);
4679
+ range.deleteContents();
4680
+ const BLOCK = new Set([
4681
+ "P",
4682
+ "DIV",
4683
+ "H1",
4684
+ "H2",
4685
+ "H3",
4686
+ "H4",
4687
+ "H5",
4688
+ "H6",
4689
+ "BLOCKQUOTE",
4690
+ "LI",
4691
+ "PRE"
4692
+ ]);
4693
+ let anchor = range.startContainer;
4694
+ if (anchor.nodeType === 3) anchor = anchor.parentElement;
4695
+ while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) anchor = anchor.parentElement;
4696
+ if (anchor && BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentNode) {
4697
+ anchor.after(table);
4698
+ if (!table.nextElementSibling) {
4699
+ const p = document.createElement("p");
4700
+ p.appendChild(document.createElement("br"));
4701
+ table.after(p);
4702
+ }
4703
+ if (!anchor.textContent.trim() && !anchor.querySelector("img, video, table")) anchor.remove();
4704
+ } else range.insertNode(table);
4705
+ const firstCell = table.querySelector("td, th");
4706
+ if (firstCell) {
4707
+ const nr = document.createRange();
4708
+ nr.setStart(firstCell, 0);
4709
+ nr.collapse(true);
4710
+ sel.removeAllRanges();
4711
+ sel.addRange(nr);
4712
+ }
4579
4713
  }
4580
4714
  //#endregion
4581
4715
  //#region src/js/core/key.js
@@ -4790,7 +4924,7 @@ function handleKeydown(event, editable, options = {}) {
4790
4924
  if (para && para.nodeName.toUpperCase() === "PRE") {
4791
4925
  if (event.shiftKey) return false;
4792
4926
  event.preventDefault();
4793
- execCommand("insertText", " ");
4927
+ execCommand("insertText", " ".repeat(options.tabSize || 4));
4794
4928
  return true;
4795
4929
  }
4796
4930
  if (options.tabSize) {
@@ -4883,6 +5017,11 @@ function handleKeydown(event, editable, options = {}) {
4883
5017
  return true;
4884
5018
  }
4885
5019
  const para = closestPara(range.sc, editable);
5020
+ if (para && para.nodeName.toUpperCase() === "PRE") {
5021
+ event.preventDefault();
5022
+ execCommand("insertText", "\n");
5023
+ return true;
5024
+ }
4886
5025
  if (para && para.nodeName.toUpperCase() === "BLOCKQUOTE") {
4887
5026
  const native = range.toNativeRange();
4888
5027
  native.setEnd(para, para.childNodes.length);
@@ -4916,11 +5055,22 @@ function handleKeydown(event, editable, options = {}) {
4916
5055
  function htmlToMarkdown(html) {
4917
5056
  return _domToMd(new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html").body).replace(/\n{3,}/g, "\n\n").trim();
4918
5057
  }
4919
- function _domToMd(node) {
5058
+ /**
5059
+ * Convert a DOM node subtree into Markdown.
5060
+ *
5061
+ * Recursively produces a Markdown string representing the given DOM node and its descendants,
5062
+ * handling common HTML constructs such as paragraphs, headings, lists (with nested indentation),
5063
+ * blockquotes, fenced and inline code, links, images, tables, horizontal rules, and basic inline emphasis.
5064
+ *
5065
+ * @param {Node} node - The DOM node to convert.
5066
+ * @param {number} [depth=0] - Current nesting depth used to indent nested list items.
5067
+ * @returns {string} The Markdown representation of the node subtree.
5068
+ */
5069
+ function _domToMd(node, depth = 0) {
4920
5070
  if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
4921
5071
  if (node.nodeType !== 1) return "";
4922
5072
  const tag = node.nodeName.toLowerCase();
4923
- const inner = () => Array.from(node.childNodes).map(_domToMd).join("");
5073
+ const inner = () => Array.from(node.childNodes).map((n) => _domToMd(n, depth)).join("");
4924
5074
  switch (tag) {
4925
5075
  case "p":
4926
5076
  case "div": return `\n\n${inner()}\n\n`;
@@ -4960,12 +5110,16 @@ function _domToMd(node) {
4960
5110
  case "ul": {
4961
5111
  const items = Array.from(node.querySelectorAll(":scope > li"));
4962
5112
  if (!items.length) return inner();
4963
- return `\n\n${items.map((li) => `- ${_domToMd(li).trim()}`).join("\n")}\n\n`;
5113
+ const indent = " ".repeat(depth);
5114
+ const lines = items.map((li) => `${indent}- ${_domToMd(li, depth + 1).trim()}`).join("\n");
5115
+ return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
4964
5116
  }
4965
5117
  case "ol": {
4966
5118
  const items = Array.from(node.querySelectorAll(":scope > li"));
4967
5119
  if (!items.length) return inner();
4968
- return `\n\n${items.map((li, i) => `${i + 1}. ${_domToMd(li).trim()}`).join("\n")}\n\n`;
5120
+ const indent = " ".repeat(depth);
5121
+ const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
5122
+ return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
4969
5123
  }
4970
5124
  case "li": return inner();
4971
5125
  case "hr": return "\n\n---\n\n";
@@ -4989,12 +5143,15 @@ function _domToMd(node) {
4989
5143
  }
4990
5144
  }
4991
5145
  /**
4992
- * Returns true if the text contains recognisable Markdown patterns.
4993
- * @param {string} text
4994
- * @returns {boolean}
5146
+ * Detects whether a string likely contains Markdown syntax.
5147
+ *
5148
+ * Checks for common Markdown constructs such as ATX headings, unordered or
5149
+ * ordered list items, blockquotes, fenced code blocks, and bold emphasis.
5150
+ * @param {string} text - Input text to inspect for Markdown patterns.
5151
+ * @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
4995
5152
  */
4996
5153
  function isMarkdown(text) {
4997
- return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|\*{2}.+?\*{2}|^```/m.test(text);
5154
+ return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|^```|^\*{2}.+?\*{2}/m.test(text);
4998
5155
  }
4999
5156
  /**
5000
5157
  * Converts a Markdown string to an HTML string.
@@ -5668,6 +5825,8 @@ var Editor = class {
5668
5825
  * Toolbar.js - Builds and manages the editor toolbar UI
5669
5826
  * Inspired by Summernote's Toolbar module — rewritten without jQuery
5670
5827
  */
5828
+ /** Resolve a toolbar item: string → registry lookup, object → pass-through. */
5829
+ var _resolveBtn = (item) => typeof item === "string" ? getButton(item) : item;
5671
5830
  var _faPageLevelReady = null;
5672
5831
  var _S$1 = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
5673
5832
  var _svgWrap = (paths) => `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${_S$1} style="display:block">${paths}</svg>`;
@@ -5757,15 +5916,19 @@ var Toolbar = class {
5757
5916
  this._disposers = [];
5758
5917
  /** @type {Array<() => void>} closers for all open color picker popups */
5759
5918
  this._colorPickerClosers = [];
5919
+ /** @type {number|null} rAF handle for debounced refresh */
5920
+ this._refreshRaf = null;
5760
5921
  }
5761
5922
  initialize() {
5762
5923
  this.el = createElement("div", { class: "an-toolbar" });
5763
5924
  this._faReady = this._detectFontAwesome();
5764
5925
  this._buildButtons();
5765
- this._btnMap = new Map((this.options.toolbar || []).flat().map((b) => [b.name, b]));
5926
+ this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
5766
5927
  return this;
5767
5928
  }
5768
5929
  destroy() {
5930
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
5931
+ this._refreshRaf = null;
5769
5932
  this._disposers.forEach((d) => d());
5770
5933
  this._disposers = [];
5771
5934
  if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
@@ -5776,7 +5939,12 @@ var Toolbar = class {
5776
5939
  const fragment = document.createDocumentFragment();
5777
5940
  toolbar.forEach((group) => {
5778
5941
  const groupEl = createElement("div", { class: "an-btn-group" });
5779
- group.forEach((btnDef) => {
5942
+ group.forEach((item) => {
5943
+ const btnDef = _resolveBtn(item);
5944
+ if (!btnDef) {
5945
+ console.warn(`[AutumnNote] Toolbar: button "${item}" not found in registry. Skipped.`);
5946
+ return;
5947
+ }
5780
5948
  let el;
5781
5949
  if (btnDef.type === "select") el = this._createSelect(btnDef);
5782
5950
  else if (btnDef.type === "grid") el = this._createGridPicker(btnDef);
@@ -6161,6 +6329,13 @@ var Toolbar = class {
6161
6329
  return _faPageLevelReady;
6162
6330
  }
6163
6331
  refresh() {
6332
+ if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
6333
+ this._refreshRaf = requestAnimationFrame(() => {
6334
+ this._refreshRaf = null;
6335
+ this._doRefresh();
6336
+ });
6337
+ }
6338
+ _doRefresh() {
6164
6339
  if (!this.el) return;
6165
6340
  const btnMap = this._btnMap || /* @__PURE__ */ new Map();
6166
6341
  this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
@@ -6189,6 +6364,24 @@ var Toolbar = class {
6189
6364
  hide() {
6190
6365
  if (this.el) this.el.style.display = "none";
6191
6366
  }
6367
+ /**
6368
+ * Tears down and re-renders the toolbar in-place.
6369
+ * Call after registering new buttons post-create via context.use(plugin)
6370
+ * or AutumnNote.registerButton() to make them appear in the toolbar.
6371
+ */
6372
+ rebuild() {
6373
+ if (this._refreshRaf) {
6374
+ cancelAnimationFrame(this._refreshRaf);
6375
+ this._refreshRaf = null;
6376
+ }
6377
+ this._disposers.forEach((d) => d());
6378
+ this._disposers = [];
6379
+ if (this.el) this.el.innerHTML = "";
6380
+ this._faReady = this._detectFontAwesome();
6381
+ this._buildButtons();
6382
+ this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
6383
+ this.refresh();
6384
+ }
6192
6385
  };
6193
6386
  //#endregion
6194
6387
  //#region src/js/module/Statusbar.js
@@ -6338,7 +6531,7 @@ var Statusbar = class {
6338
6531
  }
6339
6532
  update() {
6340
6533
  if (!this._wordCountEl || !this._charCountEl) return;
6341
- const text = this.context.layoutInfo.editable.innerText || "";
6534
+ const text = this.context.layoutInfo.editable.textContent || "";
6342
6535
  const words = _countWords(text);
6343
6536
  const chars = text.replace(/\n/g, "").length;
6344
6537
  const maxWords = this.options.maxWords || 0;
@@ -6533,7 +6726,7 @@ var Clipboard = class {
6533
6726
  event.preventDefault();
6534
6727
  const raw = clipboardData.getData("text/html");
6535
6728
  const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
6536
- const isSocialContent = /\bdata-testid\b/.test(raw) || /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
6729
+ const isSocialContent = /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
6537
6730
  let html = raw;
6538
6731
  if (isWordContent) html = this._cleanWordHtml(html);
6539
6732
  else if (isSocialContent) html = this._cleanSocialHtml(html);
@@ -6635,7 +6828,7 @@ var Clipboard = class {
6635
6828
  */
6636
6829
  _dataUrlToBlob(dataUrl) {
6637
6830
  const [header, b64] = dataUrl.split(",");
6638
- const mime = header.match(/:(.*?);/)[1];
6831
+ const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
6639
6832
  const binary = atob(b64);
6640
6833
  const arr = new Uint8Array(binary.length);
6641
6834
  for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
@@ -7641,6 +7834,7 @@ var ImageResizer = class {
7641
7834
  _select(img) {
7642
7835
  if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
7643
7836
  this._activeImg = img;
7837
+ this._lastOverlayPos = null;
7644
7838
  img.classList.add("an-image-selected");
7645
7839
  this._updateOverlayPosition();
7646
7840
  this._overlay.style.display = "block";
@@ -7664,6 +7858,14 @@ var ImageResizer = class {
7664
7858
  const offsetParent = this._overlay.offsetParent || this._container;
7665
7859
  const containerRect = offsetParent.getBoundingClientRect();
7666
7860
  const rect = this._activeImg.getBoundingClientRect();
7861
+ const p = this._lastOverlayPos;
7862
+ if (p && p.l === rect.left && p.t === rect.top && p.w === rect.width && p.h === rect.height) return;
7863
+ this._lastOverlayPos = {
7864
+ l: rect.left,
7865
+ t: rect.top,
7866
+ w: rect.width,
7867
+ h: rect.height
7868
+ };
7667
7869
  const left = rect.left - containerRect.left + offsetParent.scrollLeft;
7668
7870
  const top = rect.top - containerRect.top + offsetParent.scrollTop;
7669
7871
  this._overlay.style.left = `${left}px`;
@@ -13362,7 +13564,7 @@ var ContextMenu = class {
13362
13564
  cells.forEach((cell) => {
13363
13565
  cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
13364
13566
  });
13365
- labelEl.textContent = rows && cols ? `${cols} × ${rows}` : this.context.locale.contextMenu.table || "Insert Table";
13567
+ labelEl.textContent = rows && cols ? `${rows} × ${cols}` : this.context.locale.contextMenu.table || "Insert Table";
13366
13568
  };
13367
13569
  panel.appendChild(gridEl);
13368
13570
  panel.appendChild(labelEl);
@@ -14002,7 +14204,6 @@ var FindReplace = class {
14002
14204
  if (!editable) return;
14003
14205
  const rawMatches = this._findRawMatches(query, editable);
14004
14206
  if (rawMatches.length === 0) return;
14005
- this._currentIndex = 0;
14006
14207
  for (let i = rawMatches.length - 1; i >= 0; i--) {
14007
14208
  const { node, start, end } = rawMatches[i];
14008
14209
  try {
@@ -14019,6 +14220,7 @@ var FindReplace = class {
14019
14220
  }
14020
14221
  this._matches.reverse();
14021
14222
  this._matches = this._matches.filter((m) => m.mark);
14223
+ this._currentIndex = 0;
14022
14224
  if (this._matches.length === 0) return;
14023
14225
  if (this._matches[0] && this._matches[0].mark) {
14024
14226
  this._matches[0].mark.className = "an-highlight an-highlight-current";
@@ -14044,12 +14246,13 @@ var FindReplace = class {
14044
14246
  this._lastCaseSensitive = this._caseSensitive;
14045
14247
  }
14046
14248
  const re = this._queryRegex;
14249
+ const MAX_RESULTS = 500;
14047
14250
  const walker = document.createTreeWalker(root, 4);
14048
14251
  let node;
14049
- while (node = walker.nextNode()) {
14252
+ while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
14050
14253
  re.lastIndex = 0;
14051
14254
  let m;
14052
- while ((m = re.exec(node.textContent)) !== null) results.push({
14255
+ while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
14053
14256
  node,
14054
14257
  start: m.index,
14055
14258
  end: m.index + m[0].length
@@ -14136,7 +14339,7 @@ var FindReplace = class {
14136
14339
  const total = this._matches.length;
14137
14340
  if (total === 0) {
14138
14341
  const query = this._findInput ? this._findInput.value : "";
14139
- this._counterEl.textContent = query ? "No results" : "";
14342
+ this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
14140
14343
  } else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
14141
14344
  }
14142
14345
  };
@@ -14335,17 +14538,33 @@ var ImageCropOverlay = class {
14335
14538
  e.preventDefault();
14336
14539
  e.stopPropagation();
14337
14540
  this._startHandleDrag(e, id);
14338
- }));
14541
+ }), on(h, "touchstart", (e) => {
14542
+ e.preventDefault();
14543
+ e.stopPropagation();
14544
+ this._startHandleDrag({
14545
+ clientX: e.touches[0].clientX,
14546
+ clientY: e.touches[0].clientY
14547
+ }, id);
14548
+ }, { passive: false }));
14339
14549
  this._handles[id] = h;
14340
14550
  cropBox.appendChild(h);
14341
14551
  });
14342
14552
  this._disposers.push(on(cropBox, "mousedown", (e) => {
14343
- if (e.target !== cropBox && e.target !== grid && !(e.target.tagName === "DIV" && !e.target.className.includes("handle"))) return;
14344
- if (e.target.className && e.target.className.includes("an-crop-handle")) return;
14553
+ if (e.target.classList.contains("an-crop-handle")) return;
14554
+ if (e.target !== cropBox && e.target !== grid) return;
14345
14555
  e.preventDefault();
14346
14556
  e.stopPropagation();
14347
14557
  this._startBoxMove(e);
14348
- }));
14558
+ }), on(cropBox, "touchstart", (e) => {
14559
+ if (e.target.classList.contains("an-crop-handle")) return;
14560
+ if (e.target !== cropBox && e.target !== grid) return;
14561
+ e.preventDefault();
14562
+ e.stopPropagation();
14563
+ this._startBoxMove({
14564
+ clientX: e.touches[0].clientX,
14565
+ clientY: e.touches[0].clientY
14566
+ });
14567
+ }, { passive: false }));
14349
14568
  const infoEl = document.createElement("div");
14350
14569
  infoEl.className = "an-crop-info";
14351
14570
  infoEl.style.cssText = `
@@ -14503,15 +14722,26 @@ var ImageCropOverlay = class {
14503
14722
  * @param {(e: MouseEvent) => void} onMove
14504
14723
  */
14505
14724
  _attachDocDrag(onMove) {
14725
+ const onTouchMove = (e) => {
14726
+ e.preventDefault();
14727
+ onMove({
14728
+ clientX: e.touches[0].clientX,
14729
+ clientY: e.touches[0].clientY
14730
+ });
14731
+ };
14506
14732
  const cleanup = () => {
14507
14733
  document.removeEventListener("mousemove", onMove);
14508
14734
  document.removeEventListener("mouseup", cleanup);
14735
+ document.removeEventListener("touchmove", onTouchMove);
14736
+ document.removeEventListener("touchend", cleanup);
14509
14737
  document.body.style.userSelect = "";
14510
14738
  document.body.style.cursor = "";
14511
14739
  };
14512
14740
  document.body.style.userSelect = "none";
14513
14741
  document.addEventListener("mousemove", onMove);
14514
14742
  document.addEventListener("mouseup", cleanup, { once: true });
14743
+ document.addEventListener("touchmove", onTouchMove, { passive: false });
14744
+ document.addEventListener("touchend", cleanup, { once: true });
14515
14745
  }
14516
14746
  _bindEsc() {
14517
14747
  const handler = (e) => {
@@ -14552,7 +14782,7 @@ var ImageCropOverlay = class {
14552
14782
  height: natH
14553
14783
  }, w, h);
14554
14784
  if (!canvas) {
14555
- window.alert("Cannot crop this image: the image server does not allow cross-origin access.\nUpload the image directly to use the crop tool.");
14785
+ this._showCropError("Cannot crop this image: the image server does not allow cross-origin access. Upload the image directly to use the crop tool.");
14556
14786
  this._close(false);
14557
14787
  return;
14558
14788
  }
@@ -14569,6 +14799,37 @@ var ImageCropOverlay = class {
14569
14799
  this.context.invoke("imageResizer.updateOverlay");
14570
14800
  }
14571
14801
  /**
14802
+ * Show a non-blocking inline error banner appended to document.body.
14803
+ * Auto-dismisses after 4 seconds.
14804
+ * @param {string} msg
14805
+ */
14806
+ _showCropError(msg) {
14807
+ const banner = document.createElement("div");
14808
+ banner.setAttribute("role", "alert");
14809
+ banner.style.cssText = [
14810
+ "position:fixed",
14811
+ "bottom:24px",
14812
+ "left:50%",
14813
+ "transform:translateX(-50%)",
14814
+ "z-index:10200",
14815
+ "max-width:420px",
14816
+ "width:max-content",
14817
+ "background:#7f1d1d",
14818
+ "color:#fecaca",
14819
+ "border:1px solid #b91c1c",
14820
+ "border-radius:8px",
14821
+ "padding:12px 18px",
14822
+ "font:13px/1.5 system-ui,sans-serif",
14823
+ "box-shadow:0 4px 16px rgba(0,0,0,.4)",
14824
+ "pointer-events:auto"
14825
+ ].join(";");
14826
+ banner.textContent = msg;
14827
+ document.body.appendChild(banner);
14828
+ setTimeout(() => {
14829
+ if (banner.parentNode) banner.parentNode.removeChild(banner);
14830
+ }, 4e3);
14831
+ }
14832
+ /**
14572
14833
  * Remove all overlay DOM elements and reset state.
14573
14834
  * @param {boolean} _committed - reserved for future use
14574
14835
  */
@@ -14983,6 +15244,16 @@ var _ACTIONS = {
14983
15244
  if (!editable) return;
14984
15245
  editable.focus();
14985
15246
  document.execCommand("removeFormat");
15247
+ const sel = window.getSelection();
15248
+ if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
15249
+ const range = sel.getRangeAt(0);
15250
+ const ancestor = range.commonAncestorContainer;
15251
+ const root = ancestor.nodeType === 1 ? ancestor : ancestor.parentElement;
15252
+ if (root) {
15253
+ const candidates = [root, ...root.querySelectorAll("[style]")];
15254
+ for (const el of candidates) if (el.hasAttribute("style") && range.intersectsNode(el)) el.removeAttribute("style");
15255
+ }
15256
+ }
14986
15257
  ctx.invoke("editor.afterCommand");
14987
15258
  },
14988
15259
  inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
@@ -15096,6 +15367,7 @@ var BubbleToolbar = class {
15096
15367
  }
15097
15368
  document.body.appendChild(el);
15098
15369
  this._el = el;
15370
+ this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
15099
15371
  }
15100
15372
  _buildColorPicker() {
15101
15373
  const picker = document.createElement("div");
@@ -15181,8 +15453,13 @@ var BubbleToolbar = class {
15181
15453
  editable.focus();
15182
15454
  const sel = window.getSelection();
15183
15455
  sel.removeAllRanges();
15184
- sel.addRange(this._savedRange.cloneRange());
15185
- document.execCommand(type, false, color);
15456
+ try {
15457
+ sel.addRange(this._savedRange.cloneRange());
15458
+ } catch (_) {
15459
+ return;
15460
+ }
15461
+ const cmd = type === "hiliteColor" ? "hiliteColor" : type;
15462
+ if (!document.execCommand(cmd, false, color) && cmd === "hiliteColor") document.execCommand("backColor", false, color);
15186
15463
  this.context.invoke("editor.afterCommand");
15187
15464
  const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
15188
15465
  const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
@@ -15217,8 +15494,8 @@ var BubbleToolbar = class {
15217
15494
  this._closeColorPicker();
15218
15495
  }
15219
15496
  _syncActive() {
15220
- if (!this._el) return;
15221
- this._el.querySelectorAll(".an-bubble-btn").forEach((btn) => {
15497
+ if (!this._btnCache) return;
15498
+ this._btnCache.forEach((btn) => {
15222
15499
  const activeFn = _ACTIVE[btn.dataset.name];
15223
15500
  btn.classList.toggle("an-active", !!(activeFn && activeFn()));
15224
15501
  });
@@ -15365,14 +15642,23 @@ var Mention = class {
15365
15642
  const el = document.createElement("div");
15366
15643
  el.className = "an-mention-dropdown";
15367
15644
  el.setAttribute("role", "listbox");
15645
+ el.addEventListener("mousedown", (e) => e.preventDefault());
15646
+ el.addEventListener("click", (e) => {
15647
+ const item = e.target.closest(".an-mention-item");
15648
+ if (item) this._select(+item.dataset.index);
15649
+ });
15650
+ el.addEventListener("mousemove", (e) => {
15651
+ const item = e.target.closest(".an-mention-item");
15652
+ if (item) this._highlightItem(+item.dataset.index);
15653
+ });
15368
15654
  document.body.appendChild(el);
15369
15655
  this._dropdown = el;
15370
15656
  }
15371
15657
  _renderItems(items) {
15372
15658
  const dd = this._dropdown;
15373
- dd.innerHTML = "";
15374
15659
  this._items = items.slice(0, this._cfg.maxResults);
15375
15660
  this._activeIndex = this._items.length > 0 ? 0 : -1;
15661
+ const frag = document.createDocumentFragment();
15376
15662
  this._items.forEach((item, i) => {
15377
15663
  const li = document.createElement("div");
15378
15664
  li.className = "an-mention-item";
@@ -15388,10 +15674,10 @@ var Mention = class {
15388
15674
  const label = document.createElement("span");
15389
15675
  label.textContent = item.label;
15390
15676
  li.appendChild(label);
15391
- li.addEventListener("mousedown", (e) => e.preventDefault());
15392
- li.addEventListener("click", () => this._select(i));
15393
- dd.appendChild(li);
15677
+ frag.appendChild(li);
15394
15678
  });
15679
+ dd.innerHTML = "";
15680
+ dd.appendChild(frag);
15395
15681
  this._highlightItem(this._activeIndex);
15396
15682
  }
15397
15683
  _highlightItem(index) {
@@ -15543,6 +15829,8 @@ var Mention = class {
15543
15829
  */
15544
15830
  /** Module registry shared across all Context instances (populated via AutumnNote.registerModule). */
15545
15831
  var _customModules = /* @__PURE__ */ new Map();
15832
+ /** Global plugin registry (populated via AutumnNote.use()). Applied to every new Context. */
15833
+ var _globalPlugins = /* @__PURE__ */ new Map();
15546
15834
  var Context = class {
15547
15835
  /**
15548
15836
  * @param {HTMLElement} targetEl - The element to replace with the editor
@@ -15559,6 +15847,8 @@ var Context = class {
15559
15847
  this._listeners = /* @__PURE__ */ new Map();
15560
15848
  /** @type {Map<string, object>} */
15561
15849
  this._modules = /* @__PURE__ */ new Map();
15850
+ /** @type {Map<string, { plugin: object, publicApi: * }>} */
15851
+ this._plugins = /* @__PURE__ */ new Map();
15562
15852
  this._disposers = [];
15563
15853
  this._alive = false;
15564
15854
  }
@@ -15581,6 +15871,7 @@ var Context = class {
15581
15871
  if (this.options.focus) editable.focus();
15582
15872
  this._alive = true;
15583
15873
  this.invoke("toolbar.refresh");
15874
+ this._applyGlobalPlugins();
15584
15875
  if (typeof this.options.onInit === "function") this.options.onInit(this);
15585
15876
  return this;
15586
15877
  }
@@ -15632,6 +15923,46 @@ var Context = class {
15632
15923
  this._modules.set(name, instance);
15633
15924
  return this;
15634
15925
  }
15926
+ /**
15927
+ * Installs a plugin on this editor instance.
15928
+ * If called after create(), buttons are registered immediately but the toolbar
15929
+ * must be rebuilt via ctx.invoke('toolbar.rebuild') to render new buttons.
15930
+ * @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
15931
+ * @param {object} [options] - Forwarded to plugin.install(context, options)
15932
+ * @returns {this}
15933
+ */
15934
+ use(plugin, options = {}) {
15935
+ if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
15936
+ this._installPlugin(plugin, options);
15937
+ return this;
15938
+ }
15939
+ /**
15940
+ * Returns the public API returned by plugin.install(), or null.
15941
+ * @param {string} name
15942
+ * @returns {*}
15943
+ */
15944
+ getPlugin(name) {
15945
+ return this._plugins.get(name)?.publicApi ?? null;
15946
+ }
15947
+ _installPlugin(plugin, pluginOptions = {}) {
15948
+ const { name } = plugin;
15949
+ if (!name || typeof name !== "string") {
15950
+ console.warn("[AutumnNote] Plugin must have a string `name` property.");
15951
+ return;
15952
+ }
15953
+ if (this._plugins.has(name)) {
15954
+ console.warn(`[AutumnNote] Plugin "${name}" already installed on this instance. Skipping.`);
15955
+ return;
15956
+ }
15957
+ const publicApi = typeof plugin.install === "function" ? plugin.install(this, pluginOptions) ?? null : null;
15958
+ this._plugins.set(name, {
15959
+ plugin,
15960
+ publicApi
15961
+ });
15962
+ }
15963
+ _applyGlobalPlugins() {
15964
+ for (const { plugin, options } of _globalPlugins.values()) this._installPlugin(plugin, options);
15965
+ }
15635
15966
  _bindEditorEvents(editable) {
15636
15967
  const d0 = on(editable, "input", () => this._syncToTarget());
15637
15968
  const d1 = on(editable, "focus", () => {
@@ -15882,6 +16213,10 @@ var Context = class {
15882
16213
  if (typeof module.destroy === "function") module.destroy();
15883
16214
  });
15884
16215
  this._modules.clear();
16216
+ for (const { plugin } of this._plugins.values()) if (typeof plugin.uninstall === "function") try {
16217
+ plugin.uninstall(this);
16218
+ } catch (_) {}
16219
+ this._plugins.clear();
15885
16220
  this._disposers.forEach((d) => d());
15886
16221
  this._disposers = [];
15887
16222
  const container = this.layoutInfo.container;
@@ -16068,7 +16403,27 @@ var AutumnNote = {
16068
16403
  registerModule(name, ModuleClass) {
16069
16404
  _customModules.set(name, ModuleClass);
16070
16405
  },
16071
- version: "1.1.1"
16406
+ use(plugin, options = {}) {
16407
+ if (!plugin || typeof plugin.name !== "string") throw new TypeError("[AutumnNote] AutumnNote.use: plugin must have a string `name` property.");
16408
+ if (_globalPlugins.has(plugin.name)) {
16409
+ console.warn(`[AutumnNote] Plugin "${plugin.name}" already registered globally. Skipping.`);
16410
+ return this;
16411
+ }
16412
+ if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
16413
+ _globalPlugins.set(plugin.name, {
16414
+ plugin,
16415
+ options
16416
+ });
16417
+ return this;
16418
+ },
16419
+ hasPlugin(name) {
16420
+ return _globalPlugins.has(name);
16421
+ },
16422
+ registerButton(btnDef) {
16423
+ registerButton(btnDef);
16424
+ return this;
16425
+ },
16426
+ version: "1.4.0"
16072
16427
  };
16073
16428
  /**
16074
16429
  * @param {string|Element|NodeList|Element[]} selector
@@ -16081,6 +16436,6 @@ function resolveElements(selector) {
16081
16436
  return [];
16082
16437
  }
16083
16438
  //#endregion
16084
- export { Context, ELEMENT_NODE, TEXT_NODE, WrappedRange, alignCenterBtn, alignJustifyBtn, alignLeftBtn, alignRightBtn, all, ancestors, any, backColorBtn, boldBtn, checklistBtn, children, chunk, clamp, closest, closestPara, codeviewBtn, collapsedRange, compose, createElement, currentRange, debounce, AutumnNote as default, defaultOptions, defaultToolbar, directionBtn, emojiBtn, env, findBtn, findReplaceBtn, first, flatten, fontFamilyBtn, fontSizeBtn, foreColorBtn, fromNativeRange, fullscreenBtn, groupBy, hrBtn, iconBtn, identity, imageBtn, indentBtn, initial, inlineCodeBtn, insertAfter, isAnchor, isEditable, isElement, isEmpty, isFunction, isImage, isInline, isInsideEditable, isKey, isLi, isList, isModifier, isNil, isPara, isPlainObject, isSelectionInside, isString, isTable, isText, isVoid, italicBtn, key, last, lineHeightBtn, linkBtn, locales, mergeDeep, nextElement, nodeValue, olBtn, on, outdentBtn, outerHtml, paragraphStyleBtn, placeCaret, prevElement, printBtn, rangeFromElement, rect2bnd, redoBtn, remove, removeFormatBtn, resolveLocale, sanitiseHTML, sanitiseUrl, shortcutsBtn, splitText, strikeBtn, subscriptBtn, superscriptBtn, tableBtn, tail, throttle, trapFocus, ulBtn, underlineBtn, undoBtn, unique, unwrap, videoBtn, withSavedRange, wrap };
16439
+ export { Context, ELEMENT_NODE, TEXT_NODE, WrappedRange, _buttonRegistry, alignCenterBtn, alignJustifyBtn, alignLeftBtn, alignRightBtn, all, ancestors, any, backColorBtn, boldBtn, checklistBtn, children, chunk, clamp, closest, closestPara, codeviewBtn, collapsedRange, compose, createElement, currentRange, debounce, AutumnNote as default, defaultOptions, defaultToolbar, directionBtn, emojiBtn, env, findBtn, findReplaceBtn, first, flatten, fontFamilyBtn, fontSizeBtn, foreColorBtn, fromNativeRange, fullscreenBtn, getButton, groupBy, hrBtn, iconBtn, identity, imageBtn, indentBtn, initial, inlineCodeBtn, insertAfter, isAnchor, isEditable, isElement, isEmpty, isFunction, isImage, isInline, isInsideEditable, isKey, isLi, isList, isModifier, isNil, isPara, isPlainObject, isSelectionInside, isString, isTable, isText, isVoid, italicBtn, key, last, lineHeightBtn, linkBtn, locales, mergeDeep, nextElement, nodeValue, olBtn, on, outdentBtn, outerHtml, paragraphStyleBtn, placeCaret, prevElement, printBtn, rangeFromElement, rect2bnd, redoBtn, registerButton, remove, removeFormatBtn, resolveLocale, sanitiseHTML, sanitiseUrl, shortcutsBtn, splitText, strikeBtn, subscriptBtn, superscriptBtn, tableBtn, tail, throttle, trapFocus, ulBtn, underlineBtn, undoBtn, unique, unwrap, videoBtn, withSavedRange, wrap };
16085
16440
 
16086
16441
  //# sourceMappingURL=autumnnote.es.js.map