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.umd.js
CHANGED
|
@@ -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]))
|
|
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
|
-
|
|
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
|
|
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 +
|
|
13175
|
+
if (left + w > window.innerWidth - 8) left = window.innerWidth - w - 8;
|
|
13155
13176
|
if (left < 8) left = 8;
|
|
13156
|
-
if (top +
|
|
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 + "​");
|
|
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
|
-
|
|
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);
|