autumnnote 1.0.9 → 1.1.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.
@@ -96,8 +96,7 @@ function isFunction(val) {
96
96
  function mergeDeep(target, source) {
97
97
  const output = {};
98
98
  for (const key of Object.keys(target)) output[key] = Array.isArray(target[key]) ? [...target[key]] : target[key];
99
- if (isPlainObject(target) && isPlainObject(source)) for (const key of Object.keys(source)) if (isPlainObject(source[key])) if (!(key in target)) output[key] = mergeDeep({}, source[key]);
100
- else output[key] = mergeDeep(target[key], source[key]);
99
+ if (isPlainObject(target) && isPlainObject(source)) for (const key of Object.keys(source)) if (isPlainObject(source[key])) output[key] = mergeDeep(isPlainObject(target[key]) ? target[key] : {}, source[key]);
101
100
  else if (Array.isArray(source[key])) output[key] = [...source[key]];
102
101
  else output[key] = source[key];
103
102
  return output;
@@ -1444,7 +1443,21 @@ var defaultOptions = {
1444
1443
  onCharLimitReached: null,
1445
1444
  onWordLimitReached: null,
1446
1445
  focusColor: null,
1447
- lang: "en"
1446
+ lang: "en",
1447
+ autoSaveRestore: false,
1448
+ autoSaveRestoreTimeout: 7,
1449
+ onAutoSaveRestore: null,
1450
+ markdownShortcuts: true,
1451
+ bubbleToolbar: false,
1452
+ bubbleToolbarItems: [
1453
+ "bold",
1454
+ "italic",
1455
+ "underline",
1456
+ "link",
1457
+ "foreColor",
1458
+ "removeFormat"
1459
+ ],
1460
+ mention: null
1448
1461
  };
1449
1462
  //#endregion
1450
1463
  //#region src/js/i18n/en.js
@@ -5655,8 +5668,8 @@ var Editor = class {
5655
5668
  * Inspired by Summernote's Toolbar module — rewritten without jQuery
5656
5669
  */
5657
5670
  var _faPageLevelReady = null;
5658
- var _S = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
5659
- var _svgWrap = (paths) => `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" ${_S} style="display:block">${paths}</svg>`;
5671
+ var _S$1 = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
5672
+ 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>`;
5660
5673
  var _SVG_MAP = new Map([
5661
5674
  ["bold", _svgWrap("<path d=\"M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/><path d=\"M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/>")],
5662
5675
  ["italic", _svgWrap("<line x1=\"19\" y1=\"4\" x2=\"10\" y2=\"4\"/><line x1=\"14\" y1=\"20\" x2=\"5\" y2=\"20\"/><line x1=\"15\" y1=\"4\" x2=\"9\" y2=\"20\"/>")],
@@ -13409,12 +13422,18 @@ var ContextMenu = class {
13409
13422
  if (!editable.contains(event.target)) return;
13410
13423
  if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
13411
13424
  event.preventDefault();
13412
- this._lastX = event.clientX;
13413
- this._lastY = event.clientY;
13414
13425
  const winSel = window.getSelection();
13415
13426
  this._savedRange = winSel && winSel.rangeCount > 0 ? winSel.getRangeAt(0).cloneRange() : null;
13416
13427
  this._renderItems(this._items);
13417
- this.showAt(event.clientX, event.clientY);
13428
+ let openX = event.clientX;
13429
+ let openY = event.clientY;
13430
+ if (this._savedRange && !this._savedRange.collapsed) try {
13431
+ const selRect = this._savedRange.getBoundingClientRect();
13432
+ if (selRect.width > 0 && selRect.height > 0) openY = selRect.bottom + 4;
13433
+ } catch (_) {}
13434
+ this._lastX = openX;
13435
+ this._lastY = openY;
13436
+ this.showAt(openX, openY);
13418
13437
  }
13419
13438
  _maybeHide(event) {
13420
13439
  if (!this.el) return;
@@ -13425,28 +13444,20 @@ var ContextMenu = class {
13425
13444
  this.el.style.display = "block";
13426
13445
  this._reposition(x, y);
13427
13446
  this.el.setAttribute("aria-hidden", "false");
13447
+ this.context.triggerEvent("contextMenu:show");
13428
13448
  }
13429
13449
  _reposition(x, y) {
13430
13450
  if (!this.el) return;
13431
13451
  const rx = x !== void 0 ? x : this._lastX;
13432
13452
  const ry = y !== void 0 ? y : this._lastY;
13433
- const rect = this.el.getBoundingClientRect();
13453
+ const w = this.el.offsetWidth;
13454
+ const h = this.el.offsetHeight;
13434
13455
  let left = rx;
13435
13456
  let top = ry;
13436
- if (left + rect.width > window.innerWidth) left = window.innerWidth - rect.width - 8;
13457
+ if (left + w > window.innerWidth - 8) left = window.innerWidth - w - 8;
13437
13458
  if (left < 8) left = 8;
13438
- if (top + rect.height > window.innerHeight) top = window.innerHeight - rect.height - 8;
13459
+ if (top + h > window.innerHeight - 8) top = window.innerHeight - h - 8;
13439
13460
  if (top < 8) top = 8;
13440
- if (this._savedRange) try {
13441
- const sel = this._savedRange.getBoundingClientRect();
13442
- if (sel.width > 0 || sel.height > 0) {
13443
- if (top < sel.bottom && top + rect.height > sel.top && left < sel.right && left + rect.width > sel.left) {
13444
- const belowTop = sel.bottom + 6;
13445
- if (belowTop + rect.height <= window.innerHeight - 8) top = belowTop;
13446
- else top = Math.max(8, sel.top - rect.height - 6);
13447
- }
13448
- }
13449
- } catch (_) {}
13450
13461
  this.el.style.left = `${left}px`;
13451
13462
  this.el.style.top = `${top}px`;
13452
13463
  }
@@ -13454,6 +13465,7 @@ var ContextMenu = class {
13454
13465
  if (!this.el) return;
13455
13466
  this.el.style.display = "none";
13456
13467
  this.el.setAttribute("aria-hidden", "true");
13468
+ this.context.triggerEvent("contextMenu:hide");
13457
13469
  }
13458
13470
  /** Read the current selection's text or highlight color for the strip.
13459
13471
  * @param {'foreColor'|'hiliteColor'} type
@@ -14568,6 +14580,771 @@ var ImageCropOverlay = class {
14568
14580
  }
14569
14581
  };
14570
14582
  //#endregion
14583
+ //#region src/js/module/AutoSaveRestore.js
14584
+ /**
14585
+ * AutoSaveRestore.js — Detects a previously auto-saved draft and offers the
14586
+ * user a banner to restore or discard it.
14587
+ *
14588
+ * Activated when both `autoSave` and `autoSaveRestore` options are true.
14589
+ * On initialize it checks localStorage for a draft that is within the
14590
+ * `autoSaveRestoreTimeout` day window. If one is found a dismissible banner
14591
+ * is prepended to the editor container.
14592
+ */
14593
+ var AutoSaveRestore = class {
14594
+ /** @param {import('../Context.js').Context} context */
14595
+ constructor(context) {
14596
+ this.context = context;
14597
+ this.options = context.options;
14598
+ /** @type {HTMLElement|null} */
14599
+ this._banner = null;
14600
+ }
14601
+ initialize() {
14602
+ if (!this.options.autoSave || !this.options.autoSaveRestore) return this;
14603
+ const key = this.options.autoSaveKey;
14604
+ const metaKey = key + ":asrmeta";
14605
+ let saved;
14606
+ let meta;
14607
+ try {
14608
+ saved = localStorage.getItem(key);
14609
+ meta = JSON.parse(localStorage.getItem(metaKey) || "{}");
14610
+ } catch (_) {
14611
+ return this;
14612
+ }
14613
+ if (!saved) return this;
14614
+ const timeout = this.options.autoSaveRestoreTimeout;
14615
+ if (timeout > 0) {
14616
+ if (Date.now() - (meta.savedAt || 0) > timeout * 864e5) {
14617
+ try {
14618
+ localStorage.removeItem(key);
14619
+ localStorage.removeItem(metaKey);
14620
+ } catch (_) {}
14621
+ return this;
14622
+ }
14623
+ }
14624
+ this._showBanner(saved, meta);
14625
+ return this;
14626
+ }
14627
+ destroy() {
14628
+ this._removeBanner();
14629
+ }
14630
+ _showBanner(draftHtml, meta) {
14631
+ const t = this.context.locale.autoSaveRestore || {};
14632
+ const label = t.found || "Draft found. Restore?";
14633
+ const restoreLabel = t.restore || "Restore";
14634
+ const discardLabel = t.discard || "Discard";
14635
+ const banner = document.createElement("div");
14636
+ banner.className = "an-asr-banner";
14637
+ banner.setAttribute("role", "alert");
14638
+ const msg = document.createElement("span");
14639
+ msg.className = "an-asr-msg";
14640
+ if (meta.savedAt) {
14641
+ const formatted = new Date(meta.savedAt).toLocaleString();
14642
+ msg.textContent = (t.foundAt || "Draft from {date}. Restore?").replace("{date}", formatted);
14643
+ } else msg.textContent = label;
14644
+ const restoreBtn = document.createElement("button");
14645
+ restoreBtn.type = "button";
14646
+ restoreBtn.className = "an-btn an-asr-btn-restore";
14647
+ restoreBtn.textContent = restoreLabel;
14648
+ restoreBtn.addEventListener("click", () => this._restore(draftHtml));
14649
+ const discardBtn = document.createElement("button");
14650
+ discardBtn.type = "button";
14651
+ discardBtn.className = "an-btn an-asr-btn-discard";
14652
+ discardBtn.textContent = discardLabel;
14653
+ discardBtn.addEventListener("click", () => this._discard());
14654
+ banner.appendChild(msg);
14655
+ banner.appendChild(restoreBtn);
14656
+ banner.appendChild(discardBtn);
14657
+ this._banner = banner;
14658
+ const container = this.context.layoutInfo.container;
14659
+ container.insertBefore(banner, container.firstChild);
14660
+ }
14661
+ _restore(draftHtml) {
14662
+ this.context.setHTML(draftHtml);
14663
+ this.context.clearHistory();
14664
+ this._removeBanner();
14665
+ if (typeof this.options.onAutoSaveRestore === "function") this.options.onAutoSaveRestore(draftHtml, this.context);
14666
+ }
14667
+ _discard() {
14668
+ const key = this.options.autoSaveKey;
14669
+ try {
14670
+ localStorage.removeItem(key);
14671
+ localStorage.removeItem(key + ":asrmeta");
14672
+ } catch (_) {}
14673
+ this._removeBanner();
14674
+ }
14675
+ _removeBanner() {
14676
+ if (this._banner && this._banner.parentNode) this._banner.parentNode.removeChild(this._banner);
14677
+ this._banner = null;
14678
+ }
14679
+ };
14680
+ //#endregion
14681
+ //#region src/js/module/MarkdownShortcuts.js
14682
+ /**
14683
+ * MarkdownShortcuts.js — Convert Markdown-style syntax typed directly in the
14684
+ * editor into rich HTML elements (input rules / auto-format).
14685
+ *
14686
+ * Activated when `markdownShortcuts: true` (the default).
14687
+ *
14688
+ * Block rules — triggered by Space or Enter at the start of a line:
14689
+ * #[#[#]]· → H1 / H2 / H3
14690
+ * >· → blockquote
14691
+ * -· or *· → unordered list item
14692
+ * 1.· → ordered list item
14693
+ * [ ]· → checklist item
14694
+ * --- → horizontal rule (on Enter)
14695
+ * ``` → code block (on Enter)
14696
+ *
14697
+ * Inline rules — triggered when the closing marker is typed:
14698
+ * **text** → <strong>
14699
+ * *text* → <em>
14700
+ * ~~text~~ → <s>
14701
+ * `code` → <code>
14702
+ */
14703
+ var MarkdownShortcuts = class {
14704
+ /** @param {import('../Context.js').Context} context */
14705
+ constructor(context) {
14706
+ this.context = context;
14707
+ this.options = context.options;
14708
+ this._disposers = [];
14709
+ }
14710
+ initialize() {
14711
+ if (!this.options.markdownShortcuts) return this;
14712
+ const editable = this.context.layoutInfo.editable;
14713
+ const d1 = on(
14714
+ editable,
14715
+ "keydown",
14716
+ /** @param {KeyboardEvent} e */
14717
+ (e) => this._onKeydown(e)
14718
+ );
14719
+ const d2 = on(editable, "input", () => this._onInput());
14720
+ this._disposers.push(d1, d2);
14721
+ return this;
14722
+ }
14723
+ destroy() {
14724
+ this._disposers.forEach((d) => d());
14725
+ this._disposers = [];
14726
+ }
14727
+ _onKeydown(e) {
14728
+ if (e.key === " ") {
14729
+ if (this._applyBlockRule()) e.preventDefault();
14730
+ } else if (e.key === "Enter") {
14731
+ if (this._applyEnterRule()) e.preventDefault();
14732
+ }
14733
+ }
14734
+ /**
14735
+ * Returns the plain text of the line the cursor is on, up to the cursor.
14736
+ * @returns {{ text: string, range: Range, lineNode: Node } | null}
14737
+ */
14738
+ _getLineContext() {
14739
+ const sel = window.getSelection();
14740
+ if (!sel || !sel.rangeCount) return null;
14741
+ const range = sel.getRangeAt(0);
14742
+ if (!range.collapsed) return null;
14743
+ const editable = this.context.layoutInfo.editable;
14744
+ if (!editable.contains(range.startContainer)) return null;
14745
+ let node = range.startContainer;
14746
+ while (node && node !== editable && !this._isBlock(node)) node = node.parentNode;
14747
+ if (!node || node === editable) node = range.startContainer;
14748
+ const tmpRange = document.createRange();
14749
+ tmpRange.setStart(node, 0);
14750
+ tmpRange.setEnd(range.startContainer, range.startOffset);
14751
+ return {
14752
+ text: tmpRange.toString(),
14753
+ range,
14754
+ lineNode: node
14755
+ };
14756
+ }
14757
+ _isBlock(node) {
14758
+ if (node.nodeType !== Node.ELEMENT_NODE) return false;
14759
+ const display = window.getComputedStyle(node).display;
14760
+ return display === "block" || display === "list-item" || display === "table-cell";
14761
+ }
14762
+ /** Applies block rule on Space key. Returns true if a rule fired. */
14763
+ _applyBlockRule() {
14764
+ const ctx = this._getLineContext();
14765
+ if (!ctx) return false;
14766
+ const { text } = ctx;
14767
+ const blockPatterns = [
14768
+ {
14769
+ re: /^(#{1,3})$/,
14770
+ handler: (m) => this._convertToHeading(m[1].length)
14771
+ },
14772
+ {
14773
+ re: /^>$/,
14774
+ handler: () => this._convertToBlockquote()
14775
+ },
14776
+ {
14777
+ re: /^[-*]$/,
14778
+ handler: () => this._convertToList("ul")
14779
+ },
14780
+ {
14781
+ re: /^1\.$/,
14782
+ handler: () => this._convertToList("ol")
14783
+ },
14784
+ {
14785
+ re: /^\[ \]$/,
14786
+ handler: () => this._convertToChecklist()
14787
+ }
14788
+ ];
14789
+ for (const { re, handler } of blockPatterns) {
14790
+ const m = text.match(re);
14791
+ if (m) {
14792
+ handler(m);
14793
+ return true;
14794
+ }
14795
+ }
14796
+ return false;
14797
+ }
14798
+ /** Applies block rule on Enter key (---, ```). Returns true if a rule fired. */
14799
+ _applyEnterRule() {
14800
+ const ctx = this._getLineContext();
14801
+ if (!ctx) return false;
14802
+ const { text } = ctx;
14803
+ if (/^-{3,}$/.test(text)) {
14804
+ this._convertToHr();
14805
+ return true;
14806
+ }
14807
+ if (/^`{3}/.test(text)) {
14808
+ this._convertToCodeBlock();
14809
+ return true;
14810
+ }
14811
+ return false;
14812
+ }
14813
+ _selectLineAndDelete() {
14814
+ const sel = window.getSelection();
14815
+ if (!sel || !sel.rangeCount) return;
14816
+ const range = sel.getRangeAt(0);
14817
+ const startRange = document.createRange();
14818
+ startRange.setStart(range.startContainer.parentNode || range.startContainer, 0);
14819
+ startRange.setEnd(range.startContainer, range.startOffset);
14820
+ startRange.deleteContents();
14821
+ }
14822
+ _convertToHeading(level) {
14823
+ this._selectLineAndDelete();
14824
+ document.execCommand("formatBlock", false, `h${level}`);
14825
+ this.context.triggerEvent("change", this.context.getHTML());
14826
+ }
14827
+ _convertToBlockquote() {
14828
+ this._selectLineAndDelete();
14829
+ document.execCommand("formatBlock", false, "blockquote");
14830
+ this.context.triggerEvent("change", this.context.getHTML());
14831
+ }
14832
+ _convertToList(type) {
14833
+ this._selectLineAndDelete();
14834
+ document.execCommand(type === "ul" ? "insertUnorderedList" : "insertOrderedList");
14835
+ this.context.triggerEvent("change", this.context.getHTML());
14836
+ }
14837
+ _convertToChecklist() {
14838
+ this._selectLineAndDelete();
14839
+ this.context.invoke("editor.checklist");
14840
+ this.context.triggerEvent("change", this.context.getHTML());
14841
+ }
14842
+ _convertToHr() {
14843
+ this._selectLineAndDelete();
14844
+ this.context.invoke("editor.insertHR");
14845
+ this.context.triggerEvent("change", this.context.getHTML());
14846
+ }
14847
+ _convertToCodeBlock() {
14848
+ this._selectLineAndDelete();
14849
+ document.execCommand("formatBlock", false, "pre");
14850
+ this.context.triggerEvent("change", this.context.getHTML());
14851
+ }
14852
+ _onInput() {
14853
+ const sel = window.getSelection();
14854
+ if (!sel || !sel.rangeCount) return;
14855
+ const range = sel.getRangeAt(0);
14856
+ if (!range.collapsed) return;
14857
+ if (!this.context.layoutInfo.editable.contains(range.startContainer)) return;
14858
+ const node = range.startContainer;
14859
+ if (node.nodeType !== Node.TEXT_NODE) return;
14860
+ const text = node.textContent;
14861
+ const offset = range.startOffset;
14862
+ const inlineRules = [
14863
+ {
14864
+ re: /\*\*(.+?)\*\*$/,
14865
+ tag: "strong"
14866
+ },
14867
+ {
14868
+ re: /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)$/,
14869
+ tag: "em"
14870
+ },
14871
+ {
14872
+ re: /~~(.+?)~~$/,
14873
+ tag: "s"
14874
+ },
14875
+ {
14876
+ re: /`([^`]+)`$/,
14877
+ tag: "code"
14878
+ }
14879
+ ];
14880
+ const upToCursor = text.slice(0, offset);
14881
+ for (const { re, tag } of inlineRules) {
14882
+ const m = upToCursor.match(re);
14883
+ if (!m) continue;
14884
+ const matchStart = upToCursor.length - m[0].length;
14885
+ const matchEnd = offset;
14886
+ const innerText = m[1];
14887
+ const before = text.slice(0, matchStart);
14888
+ const after = text.slice(matchEnd);
14889
+ const el = document.createElement(tag);
14890
+ el.textContent = innerText;
14891
+ const beforeNode = document.createTextNode(before);
14892
+ const afterNode = document.createTextNode("​" + after);
14893
+ const parent = node.parentNode;
14894
+ parent.insertBefore(beforeNode, node);
14895
+ parent.insertBefore(el, node);
14896
+ parent.insertBefore(afterNode, node);
14897
+ parent.removeChild(node);
14898
+ const newRange = document.createRange();
14899
+ newRange.setStart(afterNode, 1);
14900
+ newRange.collapse(true);
14901
+ sel.removeAllRanges();
14902
+ sel.addRange(newRange);
14903
+ this.context.triggerEvent("change", this.context.getHTML());
14904
+ break;
14905
+ }
14906
+ }
14907
+ };
14908
+ //#endregion
14909
+ //#region src/js/module/BubbleToolbar.js
14910
+ /**
14911
+ * BubbleToolbar.js — A mini floating toolbar that appears above the current
14912
+ * text selection, allowing quick access to common formatting actions without
14913
+ * reaching for the main toolbar.
14914
+ *
14915
+ * Activated when `bubbleToolbar: true`. The set of visible buttons is
14916
+ * controlled by `bubbleToolbarItems` (array of button name strings).
14917
+ *
14918
+ * Built-in names: 'bold', 'italic', 'underline', 'strikethrough',
14919
+ * 'link', 'foreColor', 'removeFormat', 'inlineCode'.
14920
+ * Any name not found in the built-in map is silently ignored.
14921
+ */
14922
+ var _S = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
14923
+ var _svg = (p) => `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" ${_S} style="display:block">${p}</svg>`;
14924
+ var _ICONS = {
14925
+ bold: _svg("<path d=\"M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/><path d=\"M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z\"/>"),
14926
+ italic: _svg("<line x1=\"19\" y1=\"4\" x2=\"10\" y2=\"4\"/><line x1=\"14\" y1=\"20\" x2=\"5\" y2=\"20\"/><line x1=\"15\" y1=\"4\" x2=\"9\" y2=\"20\"/>"),
14927
+ underline: _svg("<path d=\"M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3\"/><line x1=\"4\" y1=\"21\" x2=\"20\" y2=\"21\"/>"),
14928
+ strikethrough: _svg("<path d=\"M17.3 12H6.7\"/><path d=\"M10 6.5C10 5.1 11.1 4 12.5 4c1.4 0 2.5 1.1 2.5 2.5 0 .8-.4 1.5-1 2\"/><path d=\"M14 17.5C14 19 12.9 20 11.5 20 10.1 20 9 18.9 9 17.5c0-.8.4-1.5 1-2\"/>"),
14929
+ link: _svg("<path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/>"),
14930
+ foreColor: _svg("<path d=\"M4 20L12 4L20 20\"/><line x1=\"7.5\" y1=\"14\" x2=\"16.5\" y2=\"14\"/>"),
14931
+ removeFormat: _svg("<path d=\"m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21\"/><path d=\"M22 21H7\"/><path d=\"m5 11 9 9\"/>"),
14932
+ inlineCode: _svg("<path d=\"M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5c0 1.1.9 2 2 2h1\"/><path d=\"M16 3h1a2 2 0 0 1 2 2v5c0 1.1.9 2 2 2a2 2 0 0 1-2 2v5a2 2 0 0 1-2 2h-1\"/>")
14933
+ };
14934
+ var _ACTIONS = {
14935
+ bold: (ctx) => ctx.invoke("editor.bold"),
14936
+ italic: (ctx) => ctx.invoke("editor.italic"),
14937
+ underline: (ctx) => ctx.invoke("editor.underline"),
14938
+ strikethrough: (ctx) => ctx.invoke("editor.strikethrough"),
14939
+ link: (ctx) => ctx.invoke("linkDialog.show"),
14940
+ foreColor: (ctx) => {
14941
+ const color = window.prompt("Text color (hex or name):", "#000000");
14942
+ if (color) ctx.invoke("editor.foreColor", color);
14943
+ },
14944
+ removeFormat: (ctx) => ctx.invoke("editor.removeFormat"),
14945
+ inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
14946
+ };
14947
+ var _ACTIVE = {
14948
+ bold: () => document.queryCommandState("bold"),
14949
+ italic: () => document.queryCommandState("italic"),
14950
+ underline: () => document.queryCommandState("underline"),
14951
+ strikethrough: () => document.queryCommandState("strikeThrough")
14952
+ };
14953
+ var BubbleToolbar = class {
14954
+ /** @param {import('../Context.js').Context} context */
14955
+ constructor(context) {
14956
+ this.context = context;
14957
+ this.options = context.options;
14958
+ /** @type {HTMLElement|null} */
14959
+ this._el = null;
14960
+ this._visible = false;
14961
+ this._rafId = null;
14962
+ this._contextMenuOpen = false;
14963
+ this._disposers = [];
14964
+ this._onSelectionChange = this._onSelectionChange.bind(this);
14965
+ this._onMousedown = this._onMousedown.bind(this);
14966
+ this._onKeydown = this._onKeydown.bind(this);
14967
+ this._onContextMenu = this._onContextMenu.bind(this);
14968
+ }
14969
+ initialize() {
14970
+ if (!this.options.bubbleToolbar) return this;
14971
+ this._build();
14972
+ const d1 = on(document, "selectionchange", this._onSelectionChange);
14973
+ const d2 = on(document, "mousedown", this._onMousedown);
14974
+ const d3 = on(document, "keydown", this._onKeydown);
14975
+ const d4 = on(document, "contextmenu", this._onContextMenu);
14976
+ const d5 = this.context.on("contextMenu:show", () => {
14977
+ this._contextMenuOpen = true;
14978
+ this._hide();
14979
+ });
14980
+ const d6 = this.context.on("contextMenu:hide", () => {
14981
+ this._contextMenuOpen = false;
14982
+ });
14983
+ this._disposers.push(d1, d2, d3, d4, d5, d6);
14984
+ return this;
14985
+ }
14986
+ destroy() {
14987
+ if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
14988
+ this._el = null;
14989
+ this._disposers.forEach((d) => d());
14990
+ this._disposers = [];
14991
+ cancelAnimationFrame(this._rafId);
14992
+ }
14993
+ _build() {
14994
+ const el = document.createElement("div");
14995
+ el.className = "an-bubble-toolbar";
14996
+ el.setAttribute("role", "toolbar");
14997
+ el.setAttribute("aria-label", "Formatting");
14998
+ const items = this.options.bubbleToolbarItems || [
14999
+ "bold",
15000
+ "italic",
15001
+ "underline",
15002
+ "link",
15003
+ "foreColor",
15004
+ "removeFormat"
15005
+ ];
15006
+ for (const name of items) {
15007
+ if (!_ICONS[name] || !_ACTIONS[name]) continue;
15008
+ const btn = document.createElement("button");
15009
+ btn.type = "button";
15010
+ btn.className = "an-bubble-btn";
15011
+ btn.dataset.name = name;
15012
+ btn.setAttribute("aria-label", name);
15013
+ btn.innerHTML = _ICONS[name];
15014
+ btn.addEventListener("mousedown", (e) => {
15015
+ e.preventDefault();
15016
+ });
15017
+ btn.addEventListener("click", (e) => {
15018
+ e.preventDefault();
15019
+ e.stopPropagation();
15020
+ this.context.invoke("editor.focus");
15021
+ _ACTIONS[name](this.context);
15022
+ this.context.invoke("editor.afterCommand");
15023
+ this._syncActive();
15024
+ });
15025
+ el.appendChild(btn);
15026
+ }
15027
+ document.body.appendChild(el);
15028
+ this._el = el;
15029
+ }
15030
+ _show(rect) {
15031
+ if (!this._el) return;
15032
+ const el = this._el;
15033
+ el.style.visibility = "hidden";
15034
+ el.style.display = "flex";
15035
+ const bw = el.offsetWidth;
15036
+ const bh = el.offsetHeight;
15037
+ const gap = 8;
15038
+ let left = rect.left + rect.width / 2 - bw / 2;
15039
+ let top = rect.top - bh - gap;
15040
+ left = Math.max(8, Math.min(left, window.innerWidth - bw - 8));
15041
+ if (top < 8) top = rect.bottom + gap;
15042
+ el.style.top = `${top}px`;
15043
+ el.style.left = `${left}px`;
15044
+ el.style.visibility = "";
15045
+ this._syncActive();
15046
+ this._visible = true;
15047
+ }
15048
+ _hide() {
15049
+ if (!this._el) return;
15050
+ this._el.style.display = "none";
15051
+ this._visible = false;
15052
+ }
15053
+ _syncActive() {
15054
+ if (!this._el) return;
15055
+ this._el.querySelectorAll(".an-bubble-btn").forEach((btn) => {
15056
+ const activeFn = _ACTIVE[btn.dataset.name];
15057
+ btn.classList.toggle("an-active", !!(activeFn && activeFn()));
15058
+ });
15059
+ }
15060
+ _onSelectionChange() {
15061
+ cancelAnimationFrame(this._rafId);
15062
+ this._rafId = requestAnimationFrame(() => {
15063
+ if (this._contextMenuOpen) return;
15064
+ const sel = window.getSelection();
15065
+ if (!sel || sel.isCollapsed || !sel.rangeCount) {
15066
+ this._hide();
15067
+ return;
15068
+ }
15069
+ if (!this.context.layoutInfo.editable.contains(sel.anchorNode)) {
15070
+ this._hide();
15071
+ return;
15072
+ }
15073
+ if (this.context.options.readOnly) {
15074
+ this._hide();
15075
+ return;
15076
+ }
15077
+ const rect = sel.getRangeAt(0).getBoundingClientRect();
15078
+ if (!rect || rect.width === 0) {
15079
+ this._hide();
15080
+ return;
15081
+ }
15082
+ this._show(rect);
15083
+ });
15084
+ }
15085
+ _onMousedown(e) {
15086
+ if (!this._visible) return;
15087
+ if (this._el && this._el.contains(e.target)) return;
15088
+ if (this.context.layoutInfo.editable.contains(e.target)) return;
15089
+ this._hide();
15090
+ }
15091
+ _onKeydown(e) {
15092
+ if (e.key === "Escape" && this._visible) this._hide();
15093
+ }
15094
+ _onContextMenu() {
15095
+ this._hide();
15096
+ }
15097
+ };
15098
+ //#endregion
15099
+ //#region src/js/module/Mention.js
15100
+ /**
15101
+ * Mention.js — @mention autocomplete support.
15102
+ *
15103
+ * Activated when `mention.onSearch` option is provided.
15104
+ *
15105
+ * When the user types the trigger character (default `@`) followed by at least
15106
+ * `mention.minChars` characters, `onSearch(query, callback)` is called.
15107
+ * The callback receives an array of `{ id, label, avatar? }` items which are
15108
+ * rendered in a floating dropdown. Selecting an item inserts a non-editable
15109
+ * mention chip and fires `onInsert` (if provided) to override the default HTML.
15110
+ *
15111
+ * Options shape (passed as `mention` option object):
15112
+ * {
15113
+ * trigger: '@',
15114
+ * minChars: 1,
15115
+ * maxResults: 8,
15116
+ * debounce: 200,
15117
+ * onSearch: (query, callback) => void,
15118
+ * onInsert: (item) => string | null,
15119
+ * mentionClass: 'an-mention',
15120
+ * allowSpaces: false,
15121
+ * }
15122
+ */
15123
+ var Mention = class {
15124
+ /** @param {import('../Context.js').Context} context */
15125
+ constructor(context) {
15126
+ this.context = context;
15127
+ /** @type {HTMLElement|null} */
15128
+ this._dropdown = null;
15129
+ /** @type {string} */
15130
+ this._query = "";
15131
+ /** @type {number|null} */
15132
+ this._debounceTimer = null;
15133
+ /** @type {number} current highlighted index */
15134
+ this._activeIndex = -1;
15135
+ /** @type {Array<{id, label, avatar?}>} */
15136
+ this._items = [];
15137
+ /** @type {boolean} */
15138
+ this._open = false;
15139
+ /** Position of trigger character in the text node */
15140
+ this._triggerNode = null;
15141
+ this._triggerOffset = 0;
15142
+ /** @type {DOMRect|null} Caret rect captured synchronously during input event */
15143
+ this._caretRect = null;
15144
+ this._disposers = [];
15145
+ }
15146
+ initialize() {
15147
+ const cfg = this.context.options.mention;
15148
+ if (!cfg || typeof cfg.onSearch !== "function") return this;
15149
+ this._cfg = {
15150
+ trigger: cfg.trigger || "@",
15151
+ minChars: cfg.minChars ?? 0,
15152
+ maxResults: cfg.maxResults ?? 8,
15153
+ debounce: cfg.debounce ?? 200,
15154
+ onSearch: cfg.onSearch,
15155
+ onInsert: cfg.onInsert || null,
15156
+ mentionClass: cfg.mentionClass || "an-mention",
15157
+ allowSpaces: cfg.allowSpaces || false
15158
+ };
15159
+ this._buildDropdown();
15160
+ const editable = this.context.layoutInfo.editable;
15161
+ const d1 = on(editable, "keydown", (e) => this._onKeydown(e));
15162
+ const d2 = on(editable, "input", () => this._onInput());
15163
+ const d3 = on(document, "click", (e) => this._onDocClick(e));
15164
+ this._disposers.push(d1, d2, d3);
15165
+ return this;
15166
+ }
15167
+ destroy() {
15168
+ clearTimeout(this._debounceTimer);
15169
+ if (this._dropdown && this._dropdown.parentNode) this._dropdown.parentNode.removeChild(this._dropdown);
15170
+ this._dropdown = null;
15171
+ this._disposers.forEach((d) => d());
15172
+ this._disposers = [];
15173
+ }
15174
+ _buildDropdown() {
15175
+ const el = document.createElement("div");
15176
+ el.className = "an-mention-dropdown";
15177
+ el.setAttribute("role", "listbox");
15178
+ document.body.appendChild(el);
15179
+ this._dropdown = el;
15180
+ }
15181
+ _renderItems(items) {
15182
+ const dd = this._dropdown;
15183
+ dd.innerHTML = "";
15184
+ this._items = items.slice(0, this._cfg.maxResults);
15185
+ this._activeIndex = this._items.length > 0 ? 0 : -1;
15186
+ this._items.forEach((item, i) => {
15187
+ const li = document.createElement("div");
15188
+ li.className = "an-mention-item";
15189
+ li.setAttribute("role", "option");
15190
+ li.dataset.index = i;
15191
+ if (item.avatar) {
15192
+ const img = document.createElement("img");
15193
+ img.src = item.avatar;
15194
+ img.className = "an-mention-avatar";
15195
+ img.alt = "";
15196
+ li.appendChild(img);
15197
+ }
15198
+ const label = document.createElement("span");
15199
+ label.textContent = item.label;
15200
+ li.appendChild(label);
15201
+ li.addEventListener("mousedown", (e) => e.preventDefault());
15202
+ li.addEventListener("click", () => this._select(i));
15203
+ dd.appendChild(li);
15204
+ });
15205
+ this._highlightItem(this._activeIndex);
15206
+ }
15207
+ _highlightItem(index) {
15208
+ if (!this._dropdown) return;
15209
+ this._dropdown.querySelectorAll(".an-mention-item").forEach((el, i) => {
15210
+ el.classList.toggle("an-mention-active", i === index);
15211
+ });
15212
+ this._activeIndex = index;
15213
+ }
15214
+ _positionDropdown() {
15215
+ const dd = this._dropdown;
15216
+ const rect = this._caretRect;
15217
+ if (!rect || rect.height === 0) return;
15218
+ dd.style.visibility = "hidden";
15219
+ dd.style.display = "block";
15220
+ const ddh = dd.offsetHeight;
15221
+ const ddw = dd.offsetWidth;
15222
+ let top = rect.bottom + 4;
15223
+ let left = rect.left;
15224
+ if (rect.bottom + ddh + 8 > window.innerHeight) top = rect.top - ddh - 4;
15225
+ left = Math.max(8, Math.min(left, window.innerWidth - ddw - 8));
15226
+ dd.style.top = `${top}px`;
15227
+ dd.style.left = `${left}px`;
15228
+ dd.style.visibility = "";
15229
+ }
15230
+ _showDropdown() {
15231
+ this._open = true;
15232
+ this._positionDropdown();
15233
+ }
15234
+ _hideDropdown() {
15235
+ if (this._dropdown) this._dropdown.style.display = "none";
15236
+ this._open = false;
15237
+ this._items = [];
15238
+ this._activeIndex = -1;
15239
+ this._triggerNode = null;
15240
+ this._caretRect = null;
15241
+ this._query = "";
15242
+ }
15243
+ /**
15244
+ * Captures the caret rect synchronously during the input event.
15245
+ * Must be called while the DOM event is still live — getClientRects() on a
15246
+ * collapsed range is reliable at this point but often empty inside async callbacks.
15247
+ */
15248
+ _captureCaretRect() {
15249
+ if (this._triggerNode && this._triggerNode.isConnected) try {
15250
+ const r = document.createRange();
15251
+ const end = Math.min(this._triggerOffset + 1, this._triggerNode.textContent.length);
15252
+ r.setStart(this._triggerNode, this._triggerOffset);
15253
+ r.setEnd(this._triggerNode, end);
15254
+ const candidate = r.getBoundingClientRect();
15255
+ if (candidate.height > 0) {
15256
+ this._caretRect = candidate;
15257
+ return;
15258
+ }
15259
+ } catch (_) {}
15260
+ const sel = window.getSelection();
15261
+ if (!sel || !sel.rangeCount) return;
15262
+ const rects = sel.getRangeAt(0).getClientRects();
15263
+ if (rects.length > 0) this._caretRect = rects[rects.length - 1];
15264
+ }
15265
+ _getQueryAtCursor() {
15266
+ const sel = window.getSelection();
15267
+ if (!sel || !sel.rangeCount) return null;
15268
+ const range = sel.getRangeAt(0);
15269
+ if (!range.collapsed) return null;
15270
+ const node = range.startContainer;
15271
+ if (node.nodeType !== Node.TEXT_NODE) return null;
15272
+ const text = node.textContent.slice(0, range.startOffset);
15273
+ const trigger = this._cfg.trigger;
15274
+ const triggerIdx = text.lastIndexOf(trigger);
15275
+ if (triggerIdx === -1) return null;
15276
+ const afterTrigger = text.slice(triggerIdx + trigger.length);
15277
+ if (!this._cfg.allowSpaces && /\s/.test(afterTrigger)) return null;
15278
+ if (afterTrigger.length < this._cfg.minChars) return null;
15279
+ this._triggerNode = node;
15280
+ this._triggerOffset = triggerIdx;
15281
+ return afterTrigger;
15282
+ }
15283
+ _onInput() {
15284
+ if (!this._cfg) return;
15285
+ const query = this._getQueryAtCursor();
15286
+ if (query === null) {
15287
+ this._hideDropdown();
15288
+ return;
15289
+ }
15290
+ this._captureCaretRect();
15291
+ this._query = query;
15292
+ clearTimeout(this._debounceTimer);
15293
+ this._debounceTimer = setTimeout(() => {
15294
+ this._cfg.onSearch(this._query, (items) => {
15295
+ if (!Array.isArray(items) || items.length === 0) {
15296
+ this._hideDropdown();
15297
+ return;
15298
+ }
15299
+ this._renderItems(items);
15300
+ this._showDropdown();
15301
+ });
15302
+ }, this._cfg.debounce);
15303
+ }
15304
+ _onKeydown(e) {
15305
+ if (!this._cfg || !this._open) return;
15306
+ if (e.key === "ArrowDown") {
15307
+ e.preventDefault();
15308
+ const next = (this._activeIndex + 1) % this._items.length;
15309
+ this._highlightItem(next);
15310
+ } else if (e.key === "ArrowUp") {
15311
+ e.preventDefault();
15312
+ const prev = (this._activeIndex - 1 + this._items.length) % this._items.length;
15313
+ this._highlightItem(prev);
15314
+ } else if (e.key === "Enter" || e.key === "Tab") {
15315
+ if (this._activeIndex >= 0) {
15316
+ e.preventDefault();
15317
+ this._select(this._activeIndex);
15318
+ }
15319
+ } else if (e.key === "Escape") this._hideDropdown();
15320
+ }
15321
+ _onDocClick(e) {
15322
+ if (!this._open) return;
15323
+ if (this._dropdown && this._dropdown.contains(e.target)) return;
15324
+ this._hideDropdown();
15325
+ }
15326
+ _select(index) {
15327
+ const item = this._items[index];
15328
+ if (!item) return;
15329
+ if (this._triggerNode) {
15330
+ const node = this._triggerNode;
15331
+ node.textContent = node.textContent.slice(0, this._triggerOffset) + node.textContent.slice(this._triggerOffset + this._cfg.trigger.length + this._query.length);
15332
+ const sel = window.getSelection();
15333
+ const range = document.createRange();
15334
+ range.setStart(node, this._triggerOffset);
15335
+ range.collapse(true);
15336
+ sel.removeAllRanges();
15337
+ sel.addRange(range);
15338
+ }
15339
+ let html;
15340
+ if (typeof this._cfg.onInsert === "function") html = this._cfg.onInsert(item);
15341
+ if (!html) html = `<span class="${this._cfg.mentionClass}" data-mention-id="${item.id}" contenteditable="false">@${item.label}</span>`;
15342
+ this.context.invoke("editor.insertHTML", html + "&#8203;");
15343
+ this._hideDropdown();
15344
+ this.context.triggerEvent("change", this.context.getHTML());
15345
+ }
15346
+ };
15347
+ //#endregion
14571
15348
  //#region src/js/Context.js
14572
15349
  /**
14573
15350
  * Context.js - Central hub for the editor instance
@@ -14646,6 +15423,10 @@ var Context = class {
14646
15423
  register("shortcutsDialog", ShortcutsDialog);
14647
15424
  register("findReplace", FindReplace);
14648
15425
  register("imageCropOverlay", ImageCropOverlay);
15426
+ register("autoSaveRestore", AutoSaveRestore);
15427
+ register("markdownShortcuts", MarkdownShortcuts);
15428
+ register("bubbleToolbar", BubbleToolbar);
15429
+ register("mention", Mention);
14649
15430
  for (const [name, ModuleClass] of _customModules) register(name, ModuleClass);
14650
15431
  }
14651
15432
  /**
@@ -14677,7 +15458,9 @@ var Context = class {
14677
15458
  if (this.options.autoSave && this.options.autoSaveKey) {
14678
15459
  const d4 = this.on("change", () => {
14679
15460
  try {
14680
- localStorage.setItem(this.options.autoSaveKey, this.getHTML());
15461
+ const key = this.options.autoSaveKey;
15462
+ localStorage.setItem(key, this.getHTML());
15463
+ localStorage.setItem(key + ":asrmeta", JSON.stringify({ savedAt: Date.now() }));
14681
15464
  } catch (_) {}
14682
15465
  });
14683
15466
  this._disposers.push(d4);
@@ -15095,7 +15878,7 @@ var AutumnNote = {
15095
15878
  registerModule(name, ModuleClass) {
15096
15879
  _customModules.set(name, ModuleClass);
15097
15880
  },
15098
- version: "1.0.9"
15881
+ version: "1.1.1"
15099
15882
  };
15100
15883
  /**
15101
15884
  * @param {string|Element|NodeList|Element[]} selector