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.
- package/README.md +85 -6
- package/dist/autumnnote.css +169 -0
- package/dist/autumnnote.es.js +805 -22
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +805 -22
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +3 -2
- package/src/js/Context.js +14 -2
- package/src/js/core/func.js +5 -5
- package/src/js/module/AutoSaveRestore.js +126 -0
- package/src/js/module/BubbleToolbar.js +243 -0
- package/src/js/module/ContextMenu.js +28 -26
- package/src/js/module/MarkdownShortcuts.js +253 -0
- package/src/js/module/Mention.js +337 -0
- package/src/js/settings.js +19 -0
- package/src/styles/autumnnote.scss +191 -0
- package/types/index.d.ts +56 -0
package/dist/autumnnote.es.js
CHANGED
|
@@ -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]))
|
|
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
|
-
|
|
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
|
|
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 +
|
|
13457
|
+
if (left + w > window.innerWidth - 8) left = window.innerWidth - w - 8;
|
|
13437
13458
|
if (left < 8) left = 8;
|
|
13438
|
-
if (top +
|
|
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 + "​");
|
|
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
|
-
|
|
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);
|