autumnnote 1.0.9 → 1.1.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.
@@ -28,8 +28,7 @@
28
28
  function mergeDeep(target, source) {
29
29
  const output = {};
30
30
  for (const key of Object.keys(target)) output[key] = Array.isArray(target[key]) ? [...target[key]] : target[key];
31
- 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]);
32
- else output[key] = mergeDeep(target[key], source[key]);
31
+ 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]);
33
32
  else if (Array.isArray(source[key])) output[key] = [...source[key]];
34
33
  else output[key] = source[key];
35
34
  return output;
@@ -1162,7 +1161,21 @@
1162
1161
  onCharLimitReached: null,
1163
1162
  onWordLimitReached: null,
1164
1163
  focusColor: null,
1165
- lang: "en"
1164
+ lang: "en",
1165
+ autoSaveRestore: false,
1166
+ autoSaveRestoreTimeout: 7,
1167
+ onAutoSaveRestore: null,
1168
+ markdownShortcuts: true,
1169
+ bubbleToolbar: false,
1170
+ bubbleToolbarItems: [
1171
+ "bold",
1172
+ "italic",
1173
+ "underline",
1174
+ "link",
1175
+ "foreColor",
1176
+ "removeFormat"
1177
+ ],
1178
+ mention: null
1166
1179
  };
1167
1180
  //#endregion
1168
1181
  //#region src/js/i18n/en.js
@@ -5373,8 +5386,8 @@
5373
5386
  * Inspired by Summernote's Toolbar module — rewritten without jQuery
5374
5387
  */
5375
5388
  var _faPageLevelReady = null;
5376
- var _S = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
5377
- 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>`;
5389
+ var _S$1 = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
5390
+ 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>`;
5378
5391
  var _SVG_MAP = new Map([
5379
5392
  ["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\"/>")],
5380
5393
  ["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\"/>")],
@@ -13127,12 +13140,18 @@
13127
13140
  if (!editable.contains(event.target)) return;
13128
13141
  if (this.context.layoutInfo.container.classList.contains("an-disabled")) return;
13129
13142
  event.preventDefault();
13130
- this._lastX = event.clientX;
13131
- this._lastY = event.clientY;
13132
13143
  const winSel = window.getSelection();
13133
13144
  this._savedRange = winSel && winSel.rangeCount > 0 ? winSel.getRangeAt(0).cloneRange() : null;
13134
13145
  this._renderItems(this._items);
13135
- this.showAt(event.clientX, event.clientY);
13146
+ let openX = event.clientX;
13147
+ let openY = event.clientY;
13148
+ if (this._savedRange && !this._savedRange.collapsed) try {
13149
+ const selRect = this._savedRange.getBoundingClientRect();
13150
+ if (selRect.width > 0 && selRect.height > 0) openY = selRect.bottom + 4;
13151
+ } catch (_) {}
13152
+ this._lastX = openX;
13153
+ this._lastY = openY;
13154
+ this.showAt(openX, openY);
13136
13155
  }
13137
13156
  _maybeHide(event) {
13138
13157
  if (!this.el) return;
@@ -13143,28 +13162,20 @@
13143
13162
  this.el.style.display = "block";
13144
13163
  this._reposition(x, y);
13145
13164
  this.el.setAttribute("aria-hidden", "false");
13165
+ this.context.triggerEvent("contextMenu:show");
13146
13166
  }
13147
13167
  _reposition(x, y) {
13148
13168
  if (!this.el) return;
13149
13169
  const rx = x !== void 0 ? x : this._lastX;
13150
13170
  const ry = y !== void 0 ? y : this._lastY;
13151
- const rect = this.el.getBoundingClientRect();
13171
+ const w = this.el.offsetWidth;
13172
+ const h = this.el.offsetHeight;
13152
13173
  let left = rx;
13153
13174
  let top = ry;
13154
- if (left + rect.width > window.innerWidth) left = window.innerWidth - rect.width - 8;
13175
+ if (left + w > window.innerWidth - 8) left = window.innerWidth - w - 8;
13155
13176
  if (left < 8) left = 8;
13156
- if (top + rect.height > window.innerHeight) top = window.innerHeight - rect.height - 8;
13177
+ if (top + h > window.innerHeight - 8) top = window.innerHeight - h - 8;
13157
13178
  if (top < 8) top = 8;
13158
- if (this._savedRange) try {
13159
- const sel = this._savedRange.getBoundingClientRect();
13160
- if (sel.width > 0 || sel.height > 0) {
13161
- if (top < sel.bottom && top + rect.height > sel.top && left < sel.right && left + rect.width > sel.left) {
13162
- const belowTop = sel.bottom + 6;
13163
- if (belowTop + rect.height <= window.innerHeight - 8) top = belowTop;
13164
- else top = Math.max(8, sel.top - rect.height - 6);
13165
- }
13166
- }
13167
- } catch (_) {}
13168
13179
  this.el.style.left = `${left}px`;
13169
13180
  this.el.style.top = `${top}px`;
13170
13181
  }
@@ -13172,6 +13183,7 @@
13172
13183
  if (!this.el) return;
13173
13184
  this.el.style.display = "none";
13174
13185
  this.el.setAttribute("aria-hidden", "true");
13186
+ this.context.triggerEvent("contextMenu:hide");
13175
13187
  }
13176
13188
  /** Read the current selection's text or highlight color for the strip.
13177
13189
  * @param {'foreColor'|'hiliteColor'} type
@@ -14286,6 +14298,771 @@
14286
14298
  }
14287
14299
  };
14288
14300
  //#endregion
14301
+ //#region src/js/module/AutoSaveRestore.js
14302
+ /**
14303
+ * AutoSaveRestore.js — Detects a previously auto-saved draft and offers the
14304
+ * user a banner to restore or discard it.
14305
+ *
14306
+ * Activated when both `autoSave` and `autoSaveRestore` options are true.
14307
+ * On initialize it checks localStorage for a draft that is within the
14308
+ * `autoSaveRestoreTimeout` day window. If one is found a dismissible banner
14309
+ * is prepended to the editor container.
14310
+ */
14311
+ var AutoSaveRestore = class {
14312
+ /** @param {import('../Context.js').Context} context */
14313
+ constructor(context) {
14314
+ this.context = context;
14315
+ this.options = context.options;
14316
+ /** @type {HTMLElement|null} */
14317
+ this._banner = null;
14318
+ }
14319
+ initialize() {
14320
+ if (!this.options.autoSave || !this.options.autoSaveRestore) return this;
14321
+ const key = this.options.autoSaveKey;
14322
+ const metaKey = key + ":asrmeta";
14323
+ let saved;
14324
+ let meta;
14325
+ try {
14326
+ saved = localStorage.getItem(key);
14327
+ meta = JSON.parse(localStorage.getItem(metaKey) || "{}");
14328
+ } catch (_) {
14329
+ return this;
14330
+ }
14331
+ if (!saved) return this;
14332
+ const timeout = this.options.autoSaveRestoreTimeout;
14333
+ if (timeout > 0) {
14334
+ if (Date.now() - (meta.savedAt || 0) > timeout * 864e5) {
14335
+ try {
14336
+ localStorage.removeItem(key);
14337
+ localStorage.removeItem(metaKey);
14338
+ } catch (_) {}
14339
+ return this;
14340
+ }
14341
+ }
14342
+ this._showBanner(saved, meta);
14343
+ return this;
14344
+ }
14345
+ destroy() {
14346
+ this._removeBanner();
14347
+ }
14348
+ _showBanner(draftHtml, meta) {
14349
+ const t = this.context.locale.autoSaveRestore || {};
14350
+ const label = t.found || "Draft found. Restore?";
14351
+ const restoreLabel = t.restore || "Restore";
14352
+ const discardLabel = t.discard || "Discard";
14353
+ const banner = document.createElement("div");
14354
+ banner.className = "an-asr-banner";
14355
+ banner.setAttribute("role", "alert");
14356
+ const msg = document.createElement("span");
14357
+ msg.className = "an-asr-msg";
14358
+ if (meta.savedAt) {
14359
+ const formatted = new Date(meta.savedAt).toLocaleString();
14360
+ msg.textContent = (t.foundAt || "Draft from {date}. Restore?").replace("{date}", formatted);
14361
+ } else msg.textContent = label;
14362
+ const restoreBtn = document.createElement("button");
14363
+ restoreBtn.type = "button";
14364
+ restoreBtn.className = "an-btn an-asr-btn-restore";
14365
+ restoreBtn.textContent = restoreLabel;
14366
+ restoreBtn.addEventListener("click", () => this._restore(draftHtml));
14367
+ const discardBtn = document.createElement("button");
14368
+ discardBtn.type = "button";
14369
+ discardBtn.className = "an-btn an-asr-btn-discard";
14370
+ discardBtn.textContent = discardLabel;
14371
+ discardBtn.addEventListener("click", () => this._discard());
14372
+ banner.appendChild(msg);
14373
+ banner.appendChild(restoreBtn);
14374
+ banner.appendChild(discardBtn);
14375
+ this._banner = banner;
14376
+ const container = this.context.layoutInfo.container;
14377
+ container.insertBefore(banner, container.firstChild);
14378
+ }
14379
+ _restore(draftHtml) {
14380
+ this.context.setHTML(draftHtml);
14381
+ this.context.clearHistory();
14382
+ this._removeBanner();
14383
+ if (typeof this.options.onAutoSaveRestore === "function") this.options.onAutoSaveRestore(draftHtml, this.context);
14384
+ }
14385
+ _discard() {
14386
+ const key = this.options.autoSaveKey;
14387
+ try {
14388
+ localStorage.removeItem(key);
14389
+ localStorage.removeItem(key + ":asrmeta");
14390
+ } catch (_) {}
14391
+ this._removeBanner();
14392
+ }
14393
+ _removeBanner() {
14394
+ if (this._banner && this._banner.parentNode) this._banner.parentNode.removeChild(this._banner);
14395
+ this._banner = null;
14396
+ }
14397
+ };
14398
+ //#endregion
14399
+ //#region src/js/module/MarkdownShortcuts.js
14400
+ /**
14401
+ * MarkdownShortcuts.js — Convert Markdown-style syntax typed directly in the
14402
+ * editor into rich HTML elements (input rules / auto-format).
14403
+ *
14404
+ * Activated when `markdownShortcuts: true` (the default).
14405
+ *
14406
+ * Block rules — triggered by Space or Enter at the start of a line:
14407
+ * #[#[#]]· → H1 / H2 / H3
14408
+ * >· → blockquote
14409
+ * -· or *· → unordered list item
14410
+ * 1.· → ordered list item
14411
+ * [ ]· → checklist item
14412
+ * --- → horizontal rule (on Enter)
14413
+ * ``` → code block (on Enter)
14414
+ *
14415
+ * Inline rules — triggered when the closing marker is typed:
14416
+ * **text** → <strong>
14417
+ * *text* → <em>
14418
+ * ~~text~~ → <s>
14419
+ * `code` → <code>
14420
+ */
14421
+ var MarkdownShortcuts = class {
14422
+ /** @param {import('../Context.js').Context} context */
14423
+ constructor(context) {
14424
+ this.context = context;
14425
+ this.options = context.options;
14426
+ this._disposers = [];
14427
+ }
14428
+ initialize() {
14429
+ if (!this.options.markdownShortcuts) return this;
14430
+ const editable = this.context.layoutInfo.editable;
14431
+ const d1 = on(
14432
+ editable,
14433
+ "keydown",
14434
+ /** @param {KeyboardEvent} e */
14435
+ (e) => this._onKeydown(e)
14436
+ );
14437
+ const d2 = on(editable, "input", () => this._onInput());
14438
+ this._disposers.push(d1, d2);
14439
+ return this;
14440
+ }
14441
+ destroy() {
14442
+ this._disposers.forEach((d) => d());
14443
+ this._disposers = [];
14444
+ }
14445
+ _onKeydown(e) {
14446
+ if (e.key === " ") {
14447
+ if (this._applyBlockRule()) e.preventDefault();
14448
+ } else if (e.key === "Enter") {
14449
+ if (this._applyEnterRule()) e.preventDefault();
14450
+ }
14451
+ }
14452
+ /**
14453
+ * Returns the plain text of the line the cursor is on, up to the cursor.
14454
+ * @returns {{ text: string, range: Range, lineNode: Node } | null}
14455
+ */
14456
+ _getLineContext() {
14457
+ const sel = window.getSelection();
14458
+ if (!sel || !sel.rangeCount) return null;
14459
+ const range = sel.getRangeAt(0);
14460
+ if (!range.collapsed) return null;
14461
+ const editable = this.context.layoutInfo.editable;
14462
+ if (!editable.contains(range.startContainer)) return null;
14463
+ let node = range.startContainer;
14464
+ while (node && node !== editable && !this._isBlock(node)) node = node.parentNode;
14465
+ if (!node || node === editable) node = range.startContainer;
14466
+ const tmpRange = document.createRange();
14467
+ tmpRange.setStart(node, 0);
14468
+ tmpRange.setEnd(range.startContainer, range.startOffset);
14469
+ return {
14470
+ text: tmpRange.toString(),
14471
+ range,
14472
+ lineNode: node
14473
+ };
14474
+ }
14475
+ _isBlock(node) {
14476
+ if (node.nodeType !== Node.ELEMENT_NODE) return false;
14477
+ const display = window.getComputedStyle(node).display;
14478
+ return display === "block" || display === "list-item" || display === "table-cell";
14479
+ }
14480
+ /** Applies block rule on Space key. Returns true if a rule fired. */
14481
+ _applyBlockRule() {
14482
+ const ctx = this._getLineContext();
14483
+ if (!ctx) return false;
14484
+ const { text } = ctx;
14485
+ const blockPatterns = [
14486
+ {
14487
+ re: /^(#{1,3})$/,
14488
+ handler: (m) => this._convertToHeading(m[1].length)
14489
+ },
14490
+ {
14491
+ re: /^>$/,
14492
+ handler: () => this._convertToBlockquote()
14493
+ },
14494
+ {
14495
+ re: /^[-*]$/,
14496
+ handler: () => this._convertToList("ul")
14497
+ },
14498
+ {
14499
+ re: /^1\.$/,
14500
+ handler: () => this._convertToList("ol")
14501
+ },
14502
+ {
14503
+ re: /^\[ \]$/,
14504
+ handler: () => this._convertToChecklist()
14505
+ }
14506
+ ];
14507
+ for (const { re, handler } of blockPatterns) {
14508
+ const m = text.match(re);
14509
+ if (m) {
14510
+ handler(m);
14511
+ return true;
14512
+ }
14513
+ }
14514
+ return false;
14515
+ }
14516
+ /** Applies block rule on Enter key (---, ```). Returns true if a rule fired. */
14517
+ _applyEnterRule() {
14518
+ const ctx = this._getLineContext();
14519
+ if (!ctx) return false;
14520
+ const { text } = ctx;
14521
+ if (/^-{3,}$/.test(text)) {
14522
+ this._convertToHr();
14523
+ return true;
14524
+ }
14525
+ if (/^`{3}/.test(text)) {
14526
+ this._convertToCodeBlock();
14527
+ return true;
14528
+ }
14529
+ return false;
14530
+ }
14531
+ _selectLineAndDelete() {
14532
+ const sel = window.getSelection();
14533
+ if (!sel || !sel.rangeCount) return;
14534
+ const range = sel.getRangeAt(0);
14535
+ const startRange = document.createRange();
14536
+ startRange.setStart(range.startContainer.parentNode || range.startContainer, 0);
14537
+ startRange.setEnd(range.startContainer, range.startOffset);
14538
+ startRange.deleteContents();
14539
+ }
14540
+ _convertToHeading(level) {
14541
+ this._selectLineAndDelete();
14542
+ document.execCommand("formatBlock", false, `h${level}`);
14543
+ this.context.triggerEvent("change", this.context.getHTML());
14544
+ }
14545
+ _convertToBlockquote() {
14546
+ this._selectLineAndDelete();
14547
+ document.execCommand("formatBlock", false, "blockquote");
14548
+ this.context.triggerEvent("change", this.context.getHTML());
14549
+ }
14550
+ _convertToList(type) {
14551
+ this._selectLineAndDelete();
14552
+ document.execCommand(type === "ul" ? "insertUnorderedList" : "insertOrderedList");
14553
+ this.context.triggerEvent("change", this.context.getHTML());
14554
+ }
14555
+ _convertToChecklist() {
14556
+ this._selectLineAndDelete();
14557
+ this.context.invoke("editor.checklist");
14558
+ this.context.triggerEvent("change", this.context.getHTML());
14559
+ }
14560
+ _convertToHr() {
14561
+ this._selectLineAndDelete();
14562
+ this.context.invoke("editor.insertHR");
14563
+ this.context.triggerEvent("change", this.context.getHTML());
14564
+ }
14565
+ _convertToCodeBlock() {
14566
+ this._selectLineAndDelete();
14567
+ document.execCommand("formatBlock", false, "pre");
14568
+ this.context.triggerEvent("change", this.context.getHTML());
14569
+ }
14570
+ _onInput() {
14571
+ const sel = window.getSelection();
14572
+ if (!sel || !sel.rangeCount) return;
14573
+ const range = sel.getRangeAt(0);
14574
+ if (!range.collapsed) return;
14575
+ if (!this.context.layoutInfo.editable.contains(range.startContainer)) return;
14576
+ const node = range.startContainer;
14577
+ if (node.nodeType !== Node.TEXT_NODE) return;
14578
+ const text = node.textContent;
14579
+ const offset = range.startOffset;
14580
+ const inlineRules = [
14581
+ {
14582
+ re: /\*\*(.+?)\*\*$/,
14583
+ tag: "strong"
14584
+ },
14585
+ {
14586
+ re: /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)$/,
14587
+ tag: "em"
14588
+ },
14589
+ {
14590
+ re: /~~(.+?)~~$/,
14591
+ tag: "s"
14592
+ },
14593
+ {
14594
+ re: /`([^`]+)`$/,
14595
+ tag: "code"
14596
+ }
14597
+ ];
14598
+ const upToCursor = text.slice(0, offset);
14599
+ for (const { re, tag } of inlineRules) {
14600
+ const m = upToCursor.match(re);
14601
+ if (!m) continue;
14602
+ const matchStart = upToCursor.length - m[0].length;
14603
+ const matchEnd = offset;
14604
+ const innerText = m[1];
14605
+ const before = text.slice(0, matchStart);
14606
+ const after = text.slice(matchEnd);
14607
+ const el = document.createElement(tag);
14608
+ el.textContent = innerText;
14609
+ const beforeNode = document.createTextNode(before);
14610
+ const afterNode = document.createTextNode("​" + after);
14611
+ const parent = node.parentNode;
14612
+ parent.insertBefore(beforeNode, node);
14613
+ parent.insertBefore(el, node);
14614
+ parent.insertBefore(afterNode, node);
14615
+ parent.removeChild(node);
14616
+ const newRange = document.createRange();
14617
+ newRange.setStart(afterNode, 1);
14618
+ newRange.collapse(true);
14619
+ sel.removeAllRanges();
14620
+ sel.addRange(newRange);
14621
+ this.context.triggerEvent("change", this.context.getHTML());
14622
+ break;
14623
+ }
14624
+ }
14625
+ };
14626
+ //#endregion
14627
+ //#region src/js/module/BubbleToolbar.js
14628
+ /**
14629
+ * BubbleToolbar.js — A mini floating toolbar that appears above the current
14630
+ * text selection, allowing quick access to common formatting actions without
14631
+ * reaching for the main toolbar.
14632
+ *
14633
+ * Activated when `bubbleToolbar: true`. The set of visible buttons is
14634
+ * controlled by `bubbleToolbarItems` (array of button name strings).
14635
+ *
14636
+ * Built-in names: 'bold', 'italic', 'underline', 'strikethrough',
14637
+ * 'link', 'foreColor', 'removeFormat', 'inlineCode'.
14638
+ * Any name not found in the built-in map is silently ignored.
14639
+ */
14640
+ var _S = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
14641
+ 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>`;
14642
+ var _ICONS = {
14643
+ 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\"/>"),
14644
+ 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\"/>"),
14645
+ 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\"/>"),
14646
+ 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\"/>"),
14647
+ 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\"/>"),
14648
+ foreColor: _svg("<path d=\"M4 20L12 4L20 20\"/><line x1=\"7.5\" y1=\"14\" x2=\"16.5\" y2=\"14\"/>"),
14649
+ 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\"/>"),
14650
+ 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\"/>")
14651
+ };
14652
+ var _ACTIONS = {
14653
+ bold: (ctx) => ctx.invoke("editor.bold"),
14654
+ italic: (ctx) => ctx.invoke("editor.italic"),
14655
+ underline: (ctx) => ctx.invoke("editor.underline"),
14656
+ strikethrough: (ctx) => ctx.invoke("editor.strikethrough"),
14657
+ link: (ctx) => ctx.invoke("linkDialog.show"),
14658
+ foreColor: (ctx) => {
14659
+ const color = window.prompt("Text color (hex or name):", "#000000");
14660
+ if (color) ctx.invoke("editor.foreColor", color);
14661
+ },
14662
+ removeFormat: (ctx) => ctx.invoke("editor.removeFormat"),
14663
+ inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
14664
+ };
14665
+ var _ACTIVE = {
14666
+ bold: () => document.queryCommandState("bold"),
14667
+ italic: () => document.queryCommandState("italic"),
14668
+ underline: () => document.queryCommandState("underline"),
14669
+ strikethrough: () => document.queryCommandState("strikeThrough")
14670
+ };
14671
+ var BubbleToolbar = class {
14672
+ /** @param {import('../Context.js').Context} context */
14673
+ constructor(context) {
14674
+ this.context = context;
14675
+ this.options = context.options;
14676
+ /** @type {HTMLElement|null} */
14677
+ this._el = null;
14678
+ this._visible = false;
14679
+ this._rafId = null;
14680
+ this._contextMenuOpen = false;
14681
+ this._disposers = [];
14682
+ this._onSelectionChange = this._onSelectionChange.bind(this);
14683
+ this._onMousedown = this._onMousedown.bind(this);
14684
+ this._onKeydown = this._onKeydown.bind(this);
14685
+ this._onContextMenu = this._onContextMenu.bind(this);
14686
+ }
14687
+ initialize() {
14688
+ if (!this.options.bubbleToolbar) return this;
14689
+ this._build();
14690
+ const d1 = on(document, "selectionchange", this._onSelectionChange);
14691
+ const d2 = on(document, "mousedown", this._onMousedown);
14692
+ const d3 = on(document, "keydown", this._onKeydown);
14693
+ const d4 = on(document, "contextmenu", this._onContextMenu);
14694
+ const d5 = this.context.on("contextMenu:show", () => {
14695
+ this._contextMenuOpen = true;
14696
+ this._hide();
14697
+ });
14698
+ const d6 = this.context.on("contextMenu:hide", () => {
14699
+ this._contextMenuOpen = false;
14700
+ });
14701
+ this._disposers.push(d1, d2, d3, d4, d5, d6);
14702
+ return this;
14703
+ }
14704
+ destroy() {
14705
+ if (this._el && this._el.parentNode) this._el.parentNode.removeChild(this._el);
14706
+ this._el = null;
14707
+ this._disposers.forEach((d) => d());
14708
+ this._disposers = [];
14709
+ cancelAnimationFrame(this._rafId);
14710
+ }
14711
+ _build() {
14712
+ const el = document.createElement("div");
14713
+ el.className = "an-bubble-toolbar";
14714
+ el.setAttribute("role", "toolbar");
14715
+ el.setAttribute("aria-label", "Formatting");
14716
+ const items = this.options.bubbleToolbarItems || [
14717
+ "bold",
14718
+ "italic",
14719
+ "underline",
14720
+ "link",
14721
+ "foreColor",
14722
+ "removeFormat"
14723
+ ];
14724
+ for (const name of items) {
14725
+ if (!_ICONS[name] || !_ACTIONS[name]) continue;
14726
+ const btn = document.createElement("button");
14727
+ btn.type = "button";
14728
+ btn.className = "an-bubble-btn";
14729
+ btn.dataset.name = name;
14730
+ btn.setAttribute("aria-label", name);
14731
+ btn.innerHTML = _ICONS[name];
14732
+ btn.addEventListener("mousedown", (e) => {
14733
+ e.preventDefault();
14734
+ });
14735
+ btn.addEventListener("click", (e) => {
14736
+ e.preventDefault();
14737
+ e.stopPropagation();
14738
+ this.context.invoke("editor.focus");
14739
+ _ACTIONS[name](this.context);
14740
+ this.context.invoke("editor.afterCommand");
14741
+ this._syncActive();
14742
+ });
14743
+ el.appendChild(btn);
14744
+ }
14745
+ document.body.appendChild(el);
14746
+ this._el = el;
14747
+ }
14748
+ _show(rect) {
14749
+ if (!this._el) return;
14750
+ const el = this._el;
14751
+ el.style.visibility = "hidden";
14752
+ el.style.display = "flex";
14753
+ const bw = el.offsetWidth;
14754
+ const bh = el.offsetHeight;
14755
+ const gap = 8;
14756
+ let left = rect.left + rect.width / 2 - bw / 2;
14757
+ let top = rect.top - bh - gap;
14758
+ left = Math.max(8, Math.min(left, window.innerWidth - bw - 8));
14759
+ if (top < 8) top = rect.bottom + gap;
14760
+ el.style.top = `${top}px`;
14761
+ el.style.left = `${left}px`;
14762
+ el.style.visibility = "";
14763
+ this._syncActive();
14764
+ this._visible = true;
14765
+ }
14766
+ _hide() {
14767
+ if (!this._el) return;
14768
+ this._el.style.display = "none";
14769
+ this._visible = false;
14770
+ }
14771
+ _syncActive() {
14772
+ if (!this._el) return;
14773
+ this._el.querySelectorAll(".an-bubble-btn").forEach((btn) => {
14774
+ const activeFn = _ACTIVE[btn.dataset.name];
14775
+ btn.classList.toggle("an-active", !!(activeFn && activeFn()));
14776
+ });
14777
+ }
14778
+ _onSelectionChange() {
14779
+ cancelAnimationFrame(this._rafId);
14780
+ this._rafId = requestAnimationFrame(() => {
14781
+ if (this._contextMenuOpen) return;
14782
+ const sel = window.getSelection();
14783
+ if (!sel || sel.isCollapsed || !sel.rangeCount) {
14784
+ this._hide();
14785
+ return;
14786
+ }
14787
+ if (!this.context.layoutInfo.editable.contains(sel.anchorNode)) {
14788
+ this._hide();
14789
+ return;
14790
+ }
14791
+ if (this.context.options.readOnly) {
14792
+ this._hide();
14793
+ return;
14794
+ }
14795
+ const rect = sel.getRangeAt(0).getBoundingClientRect();
14796
+ if (!rect || rect.width === 0) {
14797
+ this._hide();
14798
+ return;
14799
+ }
14800
+ this._show(rect);
14801
+ });
14802
+ }
14803
+ _onMousedown(e) {
14804
+ if (!this._visible) return;
14805
+ if (this._el && this._el.contains(e.target)) return;
14806
+ if (this.context.layoutInfo.editable.contains(e.target)) return;
14807
+ this._hide();
14808
+ }
14809
+ _onKeydown(e) {
14810
+ if (e.key === "Escape" && this._visible) this._hide();
14811
+ }
14812
+ _onContextMenu() {
14813
+ this._hide();
14814
+ }
14815
+ };
14816
+ //#endregion
14817
+ //#region src/js/module/Mention.js
14818
+ /**
14819
+ * Mention.js — @mention autocomplete support.
14820
+ *
14821
+ * Activated when `mention.onSearch` option is provided.
14822
+ *
14823
+ * When the user types the trigger character (default `@`) followed by at least
14824
+ * `mention.minChars` characters, `onSearch(query, callback)` is called.
14825
+ * The callback receives an array of `{ id, label, avatar? }` items which are
14826
+ * rendered in a floating dropdown. Selecting an item inserts a non-editable
14827
+ * mention chip and fires `onInsert` (if provided) to override the default HTML.
14828
+ *
14829
+ * Options shape (passed as `mention` option object):
14830
+ * {
14831
+ * trigger: '@',
14832
+ * minChars: 1,
14833
+ * maxResults: 8,
14834
+ * debounce: 200,
14835
+ * onSearch: (query, callback) => void,
14836
+ * onInsert: (item) => string | null,
14837
+ * mentionClass: 'an-mention',
14838
+ * allowSpaces: false,
14839
+ * }
14840
+ */
14841
+ var Mention = class {
14842
+ /** @param {import('../Context.js').Context} context */
14843
+ constructor(context) {
14844
+ this.context = context;
14845
+ /** @type {HTMLElement|null} */
14846
+ this._dropdown = null;
14847
+ /** @type {string} */
14848
+ this._query = "";
14849
+ /** @type {number|null} */
14850
+ this._debounceTimer = null;
14851
+ /** @type {number} current highlighted index */
14852
+ this._activeIndex = -1;
14853
+ /** @type {Array<{id, label, avatar?}>} */
14854
+ this._items = [];
14855
+ /** @type {boolean} */
14856
+ this._open = false;
14857
+ /** Position of trigger character in the text node */
14858
+ this._triggerNode = null;
14859
+ this._triggerOffset = 0;
14860
+ /** @type {DOMRect|null} Caret rect captured synchronously during input event */
14861
+ this._caretRect = null;
14862
+ this._disposers = [];
14863
+ }
14864
+ initialize() {
14865
+ const cfg = this.context.options.mention;
14866
+ if (!cfg || typeof cfg.onSearch !== "function") return this;
14867
+ this._cfg = {
14868
+ trigger: cfg.trigger || "@",
14869
+ minChars: cfg.minChars ?? 0,
14870
+ maxResults: cfg.maxResults ?? 8,
14871
+ debounce: cfg.debounce ?? 200,
14872
+ onSearch: cfg.onSearch,
14873
+ onInsert: cfg.onInsert || null,
14874
+ mentionClass: cfg.mentionClass || "an-mention",
14875
+ allowSpaces: cfg.allowSpaces || false
14876
+ };
14877
+ this._buildDropdown();
14878
+ const editable = this.context.layoutInfo.editable;
14879
+ const d1 = on(editable, "keydown", (e) => this._onKeydown(e));
14880
+ const d2 = on(editable, "input", () => this._onInput());
14881
+ const d3 = on(document, "click", (e) => this._onDocClick(e));
14882
+ this._disposers.push(d1, d2, d3);
14883
+ return this;
14884
+ }
14885
+ destroy() {
14886
+ clearTimeout(this._debounceTimer);
14887
+ if (this._dropdown && this._dropdown.parentNode) this._dropdown.parentNode.removeChild(this._dropdown);
14888
+ this._dropdown = null;
14889
+ this._disposers.forEach((d) => d());
14890
+ this._disposers = [];
14891
+ }
14892
+ _buildDropdown() {
14893
+ const el = document.createElement("div");
14894
+ el.className = "an-mention-dropdown";
14895
+ el.setAttribute("role", "listbox");
14896
+ document.body.appendChild(el);
14897
+ this._dropdown = el;
14898
+ }
14899
+ _renderItems(items) {
14900
+ const dd = this._dropdown;
14901
+ dd.innerHTML = "";
14902
+ this._items = items.slice(0, this._cfg.maxResults);
14903
+ this._activeIndex = this._items.length > 0 ? 0 : -1;
14904
+ this._items.forEach((item, i) => {
14905
+ const li = document.createElement("div");
14906
+ li.className = "an-mention-item";
14907
+ li.setAttribute("role", "option");
14908
+ li.dataset.index = i;
14909
+ if (item.avatar) {
14910
+ const img = document.createElement("img");
14911
+ img.src = item.avatar;
14912
+ img.className = "an-mention-avatar";
14913
+ img.alt = "";
14914
+ li.appendChild(img);
14915
+ }
14916
+ const label = document.createElement("span");
14917
+ label.textContent = item.label;
14918
+ li.appendChild(label);
14919
+ li.addEventListener("mousedown", (e) => e.preventDefault());
14920
+ li.addEventListener("click", () => this._select(i));
14921
+ dd.appendChild(li);
14922
+ });
14923
+ this._highlightItem(this._activeIndex);
14924
+ }
14925
+ _highlightItem(index) {
14926
+ if (!this._dropdown) return;
14927
+ this._dropdown.querySelectorAll(".an-mention-item").forEach((el, i) => {
14928
+ el.classList.toggle("an-mention-active", i === index);
14929
+ });
14930
+ this._activeIndex = index;
14931
+ }
14932
+ _positionDropdown() {
14933
+ const dd = this._dropdown;
14934
+ const rect = this._caretRect;
14935
+ if (!rect || rect.height === 0) return;
14936
+ dd.style.visibility = "hidden";
14937
+ dd.style.display = "block";
14938
+ const ddh = dd.offsetHeight;
14939
+ const ddw = dd.offsetWidth;
14940
+ let top = rect.bottom + 4;
14941
+ let left = rect.left;
14942
+ if (rect.bottom + ddh + 8 > window.innerHeight) top = rect.top - ddh - 4;
14943
+ left = Math.max(8, Math.min(left, window.innerWidth - ddw - 8));
14944
+ dd.style.top = `${top}px`;
14945
+ dd.style.left = `${left}px`;
14946
+ dd.style.visibility = "";
14947
+ }
14948
+ _showDropdown() {
14949
+ this._open = true;
14950
+ this._positionDropdown();
14951
+ }
14952
+ _hideDropdown() {
14953
+ if (this._dropdown) this._dropdown.style.display = "none";
14954
+ this._open = false;
14955
+ this._items = [];
14956
+ this._activeIndex = -1;
14957
+ this._triggerNode = null;
14958
+ this._caretRect = null;
14959
+ this._query = "";
14960
+ }
14961
+ /**
14962
+ * Captures the caret rect synchronously during the input event.
14963
+ * Must be called while the DOM event is still live — getClientRects() on a
14964
+ * collapsed range is reliable at this point but often empty inside async callbacks.
14965
+ */
14966
+ _captureCaretRect() {
14967
+ if (this._triggerNode && this._triggerNode.isConnected) try {
14968
+ const r = document.createRange();
14969
+ const end = Math.min(this._triggerOffset + 1, this._triggerNode.textContent.length);
14970
+ r.setStart(this._triggerNode, this._triggerOffset);
14971
+ r.setEnd(this._triggerNode, end);
14972
+ const candidate = r.getBoundingClientRect();
14973
+ if (candidate.height > 0) {
14974
+ this._caretRect = candidate;
14975
+ return;
14976
+ }
14977
+ } catch (_) {}
14978
+ const sel = window.getSelection();
14979
+ if (!sel || !sel.rangeCount) return;
14980
+ const rects = sel.getRangeAt(0).getClientRects();
14981
+ if (rects.length > 0) this._caretRect = rects[rects.length - 1];
14982
+ }
14983
+ _getQueryAtCursor() {
14984
+ const sel = window.getSelection();
14985
+ if (!sel || !sel.rangeCount) return null;
14986
+ const range = sel.getRangeAt(0);
14987
+ if (!range.collapsed) return null;
14988
+ const node = range.startContainer;
14989
+ if (node.nodeType !== Node.TEXT_NODE) return null;
14990
+ const text = node.textContent.slice(0, range.startOffset);
14991
+ const trigger = this._cfg.trigger;
14992
+ const triggerIdx = text.lastIndexOf(trigger);
14993
+ if (triggerIdx === -1) return null;
14994
+ const afterTrigger = text.slice(triggerIdx + trigger.length);
14995
+ if (!this._cfg.allowSpaces && /\s/.test(afterTrigger)) return null;
14996
+ if (afterTrigger.length < this._cfg.minChars) return null;
14997
+ this._triggerNode = node;
14998
+ this._triggerOffset = triggerIdx;
14999
+ return afterTrigger;
15000
+ }
15001
+ _onInput() {
15002
+ if (!this._cfg) return;
15003
+ const query = this._getQueryAtCursor();
15004
+ if (query === null) {
15005
+ this._hideDropdown();
15006
+ return;
15007
+ }
15008
+ this._captureCaretRect();
15009
+ this._query = query;
15010
+ clearTimeout(this._debounceTimer);
15011
+ this._debounceTimer = setTimeout(() => {
15012
+ this._cfg.onSearch(this._query, (items) => {
15013
+ if (!Array.isArray(items) || items.length === 0) {
15014
+ this._hideDropdown();
15015
+ return;
15016
+ }
15017
+ this._renderItems(items);
15018
+ this._showDropdown();
15019
+ });
15020
+ }, this._cfg.debounce);
15021
+ }
15022
+ _onKeydown(e) {
15023
+ if (!this._cfg || !this._open) return;
15024
+ if (e.key === "ArrowDown") {
15025
+ e.preventDefault();
15026
+ const next = (this._activeIndex + 1) % this._items.length;
15027
+ this._highlightItem(next);
15028
+ } else if (e.key === "ArrowUp") {
15029
+ e.preventDefault();
15030
+ const prev = (this._activeIndex - 1 + this._items.length) % this._items.length;
15031
+ this._highlightItem(prev);
15032
+ } else if (e.key === "Enter" || e.key === "Tab") {
15033
+ if (this._activeIndex >= 0) {
15034
+ e.preventDefault();
15035
+ this._select(this._activeIndex);
15036
+ }
15037
+ } else if (e.key === "Escape") this._hideDropdown();
15038
+ }
15039
+ _onDocClick(e) {
15040
+ if (!this._open) return;
15041
+ if (this._dropdown && this._dropdown.contains(e.target)) return;
15042
+ this._hideDropdown();
15043
+ }
15044
+ _select(index) {
15045
+ const item = this._items[index];
15046
+ if (!item) return;
15047
+ if (this._triggerNode) {
15048
+ const node = this._triggerNode;
15049
+ node.textContent = node.textContent.slice(0, this._triggerOffset) + node.textContent.slice(this._triggerOffset + this._cfg.trigger.length + this._query.length);
15050
+ const sel = window.getSelection();
15051
+ const range = document.createRange();
15052
+ range.setStart(node, this._triggerOffset);
15053
+ range.collapse(true);
15054
+ sel.removeAllRanges();
15055
+ sel.addRange(range);
15056
+ }
15057
+ let html;
15058
+ if (typeof this._cfg.onInsert === "function") html = this._cfg.onInsert(item);
15059
+ if (!html) html = `<span class="${this._cfg.mentionClass}" data-mention-id="${item.id}" contenteditable="false">@${item.label}</span>`;
15060
+ this.context.invoke("editor.insertHTML", html + "&#8203;");
15061
+ this._hideDropdown();
15062
+ this.context.triggerEvent("change", this.context.getHTML());
15063
+ }
15064
+ };
15065
+ //#endregion
14289
15066
  //#region src/js/Context.js
14290
15067
  /**
14291
15068
  * Context.js - Central hub for the editor instance
@@ -14364,6 +15141,10 @@
14364
15141
  register("shortcutsDialog", ShortcutsDialog);
14365
15142
  register("findReplace", FindReplace);
14366
15143
  register("imageCropOverlay", ImageCropOverlay);
15144
+ register("autoSaveRestore", AutoSaveRestore);
15145
+ register("markdownShortcuts", MarkdownShortcuts);
15146
+ register("bubbleToolbar", BubbleToolbar);
15147
+ register("mention", Mention);
14367
15148
  for (const [name, ModuleClass] of _customModules) register(name, ModuleClass);
14368
15149
  }
14369
15150
  /**
@@ -14395,7 +15176,9 @@
14395
15176
  if (this.options.autoSave && this.options.autoSaveKey) {
14396
15177
  const d4 = this.on("change", () => {
14397
15178
  try {
14398
- localStorage.setItem(this.options.autoSaveKey, this.getHTML());
15179
+ const key = this.options.autoSaveKey;
15180
+ localStorage.setItem(key, this.getHTML());
15181
+ localStorage.setItem(key + ":asrmeta", JSON.stringify({ savedAt: Date.now() }));
14399
15182
  } catch (_) {}
14400
15183
  });
14401
15184
  this._disposers.push(d4);