autumnnote 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -10
- package/dist/autumnnote.es.js +463 -108
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +433 -91
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/js/Context.js +64 -0
- package/src/js/core/dom.js +6 -3
- package/src/js/core/func.js +24 -13
- package/src/js/core/markdown.js +26 -8
- package/src/js/core/sanitise.js +57 -46
- package/src/js/editing/History.js +12 -1
- package/src/js/editing/Style.js +49 -21
- package/src/js/editing/Table.js +53 -13
- package/src/js/editing/Typing.js +9 -2
- package/src/js/i18n/de.js +1 -0
- package/src/js/i18n/en.js +1 -0
- package/src/js/i18n/es.js +1 -0
- package/src/js/i18n/fr.js +1 -0
- package/src/js/i18n/ja.js +1 -0
- package/src/js/i18n/ko.js +1 -0
- package/src/js/i18n/vi.js +1 -0
- package/src/js/i18n/zh.js +1 -0
- package/src/js/index.js +42 -2
- package/src/js/module/BubbleToolbar.js +26 -7
- package/src/js/module/Buttons.js +37 -0
- package/src/js/module/Clipboard.js +2 -2
- package/src/js/module/ContextMenu.js +1 -1
- package/src/js/module/FindReplace.js +6 -5
- package/src/js/module/ImageCropOverlay.js +46 -8
- package/src/js/module/ImageResizer.js +7 -0
- package/src/js/module/Mention.js +18 -4
- package/src/js/module/Statusbar.js +2 -1
- package/src/js/module/Toolbar.js +47 -2
- package/types/index.d.ts +54 -1
package/dist/autumnnote.umd.js
CHANGED
|
@@ -428,9 +428,15 @@
|
|
|
428
428
|
execCommand("outdent");
|
|
429
429
|
}
|
|
430
430
|
/**
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
431
|
+
* Convert a checklist <li> into a paragraph and move any following items into a new checklist.
|
|
432
|
+
*
|
|
433
|
+
* Preserves inline markup from the converted item, strips zero-width space anchors,
|
|
434
|
+
* and replaces empty content with a non‑breaking space. If there are list items
|
|
435
|
+
* after the converted item they are moved into a new <ul class="an-checklist">
|
|
436
|
+
* inserted immediately after the original list. The original <li> is removed and
|
|
437
|
+
* the original list is removed if it becomes empty. Attempts to place the caret
|
|
438
|
+
* at the start of the newly created <p>.
|
|
439
|
+
* @param {HTMLElement} checkLi - The checklist `<li>` element to convert to a `<p>`.
|
|
434
440
|
*/
|
|
435
441
|
function _checklistItemToP(checkLi) {
|
|
436
442
|
const checkUl = checkLi.closest(".an-checklist");
|
|
@@ -439,7 +445,15 @@
|
|
|
439
445
|
const liIndex = allLis.indexOf(checkLi);
|
|
440
446
|
const afterLis = allLis.slice(liIndex + 1);
|
|
441
447
|
const p = document.createElement("p");
|
|
442
|
-
|
|
448
|
+
for (const child of checkLi.childNodes) {
|
|
449
|
+
if (child.nodeType === 1 && child.tagName === "INPUT") continue;
|
|
450
|
+
p.appendChild(child.cloneNode(true));
|
|
451
|
+
}
|
|
452
|
+
p.innerHTML = p.innerHTML.replace(/\u200B/g, "");
|
|
453
|
+
if (!p.hasChildNodes() || !p.textContent.trim()) {
|
|
454
|
+
p.innerHTML = "";
|
|
455
|
+
p.appendChild(document.createTextNode("\xA0"));
|
|
456
|
+
}
|
|
443
457
|
if (afterLis.length > 0) {
|
|
444
458
|
const newUl = document.createElement("ul");
|
|
445
459
|
newUl.className = "an-checklist";
|
|
@@ -470,9 +484,12 @@
|
|
|
470
484
|
*/
|
|
471
485
|
var insertOrderedList = () => execCommand("insertOrderedList");
|
|
472
486
|
/**
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
487
|
+
* Set the line-height on every block-level element that intersects the current selection.
|
|
488
|
+
*
|
|
489
|
+
* If the selection is collapsed, the nearest enclosing block element receives the style.
|
|
490
|
+
* For a non-collapsed selection, all unique block ancestors of text nodes that intersect the range are updated;
|
|
491
|
+
* if none are found, the nearest block ancestor of the range's common ancestor is updated.
|
|
492
|
+
* @param {string} value - Line-height value to apply; typically a unitless multiplier (for example, "1.5").
|
|
476
493
|
*/
|
|
477
494
|
function lineHeight(value) {
|
|
478
495
|
const sel = window.getSelection();
|
|
@@ -507,9 +524,9 @@
|
|
|
507
524
|
return;
|
|
508
525
|
}
|
|
509
526
|
const blocks = /* @__PURE__ */ new Set();
|
|
510
|
-
const iter = document.
|
|
527
|
+
const iter = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, { acceptNode: (node) => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP });
|
|
511
528
|
let textNode;
|
|
512
|
-
while (textNode = iter.nextNode())
|
|
529
|
+
while (textNode = iter.nextNode()) {
|
|
513
530
|
const block = nearestBlock(textNode);
|
|
514
531
|
if (block) blocks.add(block);
|
|
515
532
|
}
|
|
@@ -591,9 +608,18 @@
|
|
|
591
608
|
return !!(code && !code.closest("pre"));
|
|
592
609
|
}
|
|
593
610
|
/**
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
611
|
+
* Toggle a checklist at the current selection or caret.
|
|
612
|
+
*
|
|
613
|
+
* When the selection is inside an existing checklist `<ul class="an-checklist">`,
|
|
614
|
+
* converts the selected `<li>` items back into `<p>` paragraphs and places the caret
|
|
615
|
+
* at the start of the first converted paragraph. Otherwise creates a checklist:
|
|
616
|
+
* - If the selection is collapsed, converts the nearest block-level ancestor (or inserts
|
|
617
|
+
* a single checklist item at the editable root) into a checklist with one item containing
|
|
618
|
+
* that block's text and places the caret inside the new item.
|
|
619
|
+
* - If the selection is a range, converts each intersecting block element into one checklist
|
|
620
|
+
* item (preserving textual content) and places the caret at the end of the last item.
|
|
621
|
+
*
|
|
622
|
+
* Empty or whitespace-only selections do not create a checklist.
|
|
597
623
|
*/
|
|
598
624
|
function toggleChecklist() {
|
|
599
625
|
const sel = window.getSelection();
|
|
@@ -652,8 +678,9 @@
|
|
|
652
678
|
ul.appendChild(li);
|
|
653
679
|
if (block && BLOCK_TAGS.has(block.tagName)) block.parentNode.replaceChild(ul, block);
|
|
654
680
|
else {
|
|
655
|
-
|
|
656
|
-
|
|
681
|
+
const nativeRange = sel.getRangeAt(0);
|
|
682
|
+
nativeRange.deleteContents();
|
|
683
|
+
nativeRange.insertNode(ul);
|
|
657
684
|
}
|
|
658
685
|
const textNode = li.lastChild;
|
|
659
686
|
const nr = document.createRange();
|
|
@@ -776,6 +803,34 @@
|
|
|
776
803
|
isDisabled
|
|
777
804
|
};
|
|
778
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* Global registry for custom buttons registered via AutumnNote.registerButton()
|
|
808
|
+
* or via a plugin's `buttons` array. Toolbar resolves string names from here.
|
|
809
|
+
* @type {Map<string, object>}
|
|
810
|
+
*/
|
|
811
|
+
var _buttonRegistry = /* @__PURE__ */ new Map();
|
|
812
|
+
/**
|
|
813
|
+
* Registers a button definition in the global registry so it can be referenced
|
|
814
|
+
* by string name in toolbar configuration: `toolbar: [['myBtn', boldBtn]]`.
|
|
815
|
+
* @param {object} btnDef - Any ToolbarItemDef-compatible object with a `name` string.
|
|
816
|
+
*/
|
|
817
|
+
function registerButton(btnDef) {
|
|
818
|
+
if (!btnDef || typeof btnDef.name !== "string") {
|
|
819
|
+
console.warn("[AutumnNote] registerButton: btnDef must have a string `name` property.");
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (_buttonRegistry.has(btnDef.name)) console.warn(`[AutumnNote] registerButton: overwriting existing button "${btnDef.name}".`);
|
|
823
|
+
_buttonRegistry.set(btnDef.name, btnDef);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Looks up a button definition by name from the global registry.
|
|
827
|
+
* Returns undefined when not found.
|
|
828
|
+
* @param {string} name
|
|
829
|
+
* @returns {object|undefined}
|
|
830
|
+
*/
|
|
831
|
+
function getButton(name) {
|
|
832
|
+
return _buttonRegistry.get(name);
|
|
833
|
+
}
|
|
779
834
|
var boldBtn = btn("bold", "bold", "Bold (Ctrl+B)", () => bold(), () => document.queryCommandState("bold"));
|
|
780
835
|
var italicBtn = btn("italic", "italic", "Italic (Ctrl+I)", () => italic(), () => document.queryCommandState("italic"));
|
|
781
836
|
var underlineBtn = btn("underline", "underline", "Underline (Ctrl+U)", () => underline(), () => {
|
|
@@ -1332,6 +1387,7 @@
|
|
|
1332
1387
|
replaceAriaLabel: "Replace with",
|
|
1333
1388
|
replaceBtn: "Replace",
|
|
1334
1389
|
replaceAllBtn: "Replace All",
|
|
1390
|
+
noResults: "No results",
|
|
1335
1391
|
close: "×"
|
|
1336
1392
|
},
|
|
1337
1393
|
shortcutsDialog: {
|
|
@@ -1684,6 +1740,7 @@
|
|
|
1684
1740
|
replaceAriaLabel: "Thay thế bằng",
|
|
1685
1741
|
replaceBtn: "Thay thế",
|
|
1686
1742
|
replaceAllBtn: "Thay thế tất cả",
|
|
1743
|
+
noResults: "Không có kết quả",
|
|
1687
1744
|
close: "×"
|
|
1688
1745
|
},
|
|
1689
1746
|
shortcutsDialog: {
|
|
@@ -2013,6 +2070,7 @@
|
|
|
2013
2070
|
replacePlaceholder: "置換後…",
|
|
2014
2071
|
replaceAriaLabel: "置換後のテキスト",
|
|
2015
2072
|
replaceBtn: "置換",
|
|
2073
|
+
noResults: "結果なし",
|
|
2016
2074
|
replaceAllBtn: "すべて置換",
|
|
2017
2075
|
close: "×"
|
|
2018
2076
|
},
|
|
@@ -2343,6 +2401,7 @@
|
|
|
2343
2401
|
replacePlaceholder: "替换为…",
|
|
2344
2402
|
replaceAriaLabel: "替换为",
|
|
2345
2403
|
replaceBtn: "替换",
|
|
2404
|
+
noResults: "没有结果",
|
|
2346
2405
|
replaceAllBtn: "全部替换",
|
|
2347
2406
|
close: "×"
|
|
2348
2407
|
},
|
|
@@ -2673,6 +2732,7 @@
|
|
|
2673
2732
|
replacePlaceholder: "Remplacer par…",
|
|
2674
2733
|
replaceAriaLabel: "Remplacer par",
|
|
2675
2734
|
replaceBtn: "Remplacer",
|
|
2735
|
+
noResults: "Aucun résultat",
|
|
2676
2736
|
replaceAllBtn: "Tout remplacer",
|
|
2677
2737
|
close: "×"
|
|
2678
2738
|
},
|
|
@@ -3003,6 +3063,7 @@
|
|
|
3003
3063
|
replacePlaceholder: "Ersetzen durch…",
|
|
3004
3064
|
replaceAriaLabel: "Ersetzen durch",
|
|
3005
3065
|
replaceBtn: "Ersetzen",
|
|
3066
|
+
noResults: "Keine Ergebnisse",
|
|
3006
3067
|
replaceAllBtn: "Alle ersetzen",
|
|
3007
3068
|
close: "×"
|
|
3008
3069
|
},
|
|
@@ -3333,6 +3394,7 @@
|
|
|
3333
3394
|
replacePlaceholder: "Reemplazar con…",
|
|
3334
3395
|
replaceAriaLabel: "Reemplazar con",
|
|
3335
3396
|
replaceBtn: "Reemplazar",
|
|
3397
|
+
noResults: "Sin resultados",
|
|
3336
3398
|
replaceAllBtn: "Reemplazar todo",
|
|
3337
3399
|
close: "×"
|
|
3338
3400
|
},
|
|
@@ -3663,6 +3725,7 @@
|
|
|
3663
3725
|
replacePlaceholder: "바꿀 내용…",
|
|
3664
3726
|
replaceAriaLabel: "바꿀 내용",
|
|
3665
3727
|
replaceBtn: "바꾸기",
|
|
3728
|
+
noResults: "결과 없음",
|
|
3666
3729
|
replaceAllBtn: "모두 바꾸기",
|
|
3667
3730
|
close: "×"
|
|
3668
3731
|
},
|
|
@@ -3883,8 +3946,10 @@
|
|
|
3883
3946
|
"object",
|
|
3884
3947
|
"embed",
|
|
3885
3948
|
"form",
|
|
3886
|
-
"
|
|
3949
|
+
"base"
|
|
3887
3950
|
];
|
|
3951
|
+
/** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
|
|
3952
|
+
var UNWRAP_TAGS = new Set(["button"]);
|
|
3888
3953
|
/** Attributes whose values must be sanitised as URLs. */
|
|
3889
3954
|
var URL_ATTRS = [
|
|
3890
3955
|
"href",
|
|
@@ -3902,60 +3967,61 @@
|
|
|
3902
3967
|
"player.vimeo.com"
|
|
3903
3968
|
]);
|
|
3904
3969
|
/**
|
|
3905
|
-
*
|
|
3906
|
-
* Uses DOMParser so the sanitisation follows normal browser parsing rules —
|
|
3907
|
-
* no regex shortcuts that can be bypassed by encoding tricks.
|
|
3970
|
+
* Produce a sanitized HTML string with dangerous elements and attributes removed.
|
|
3908
3971
|
*
|
|
3909
|
-
*
|
|
3910
|
-
*
|
|
3911
|
-
*
|
|
3912
|
-
* - Rejects javascript: and vbscript: URLs in URL attributes
|
|
3913
|
-
* - Rejects data: URIs everywhere except img[src] (base64 uploads)
|
|
3972
|
+
* Removes disallowed tags and wrappers, strips event-handler attributes, rejects
|
|
3973
|
+
* `javascript:`/`vbscript:` URLs and most `data:` URIs, restricts iframe `src`
|
|
3974
|
+
* to trusted hosts when enabled, and permits only checklist checkboxes as inputs.
|
|
3914
3975
|
*
|
|
3915
|
-
* @param {string} html
|
|
3916
|
-
* @
|
|
3976
|
+
* @param {string} html - HTML fragment to sanitize.
|
|
3977
|
+
* @param {Object} [options]
|
|
3978
|
+
* @param {boolean} [options.allowIframes=false] - If true, `iframe` elements are not removed but their `src` is restricted to trusted hosts and `srcdoc` is removed.
|
|
3979
|
+
* @returns {string} The sanitized HTML fragment.
|
|
3917
3980
|
*/
|
|
3918
3981
|
function sanitiseHTML(html, { allowIframes = false } = {}) {
|
|
3919
3982
|
const doc = new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html");
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3983
|
+
const allElements = Array.from(doc.querySelectorAll("*"));
|
|
3984
|
+
const prohibited = new Set(allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS);
|
|
3985
|
+
for (const el of allElements) {
|
|
3986
|
+
const tag = el.tagName.toLowerCase();
|
|
3987
|
+
if (UNWRAP_TAGS.has(tag)) {
|
|
3988
|
+
el.replaceWith(...el.childNodes);
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
if (prohibited.has(tag)) {
|
|
3992
|
+
el.remove();
|
|
3993
|
+
continue;
|
|
3994
|
+
}
|
|
3995
|
+
for (const attr of Array.from(el.attributes)) {
|
|
3925
3996
|
if (attr.name.startsWith("on")) {
|
|
3926
3997
|
el.removeAttribute(attr.name);
|
|
3927
|
-
|
|
3998
|
+
continue;
|
|
3928
3999
|
}
|
|
3929
4000
|
if (URL_ATTRS.includes(attr.name)) {
|
|
3930
4001
|
const val = attr.value.trim();
|
|
3931
4002
|
if (/^(javascript|vbscript):/i.test(val)) {
|
|
3932
4003
|
el.removeAttribute(attr.name);
|
|
3933
|
-
|
|
4004
|
+
continue;
|
|
3934
4005
|
}
|
|
3935
4006
|
if (/^data:/i.test(val) && !(attr.name === "src" && el.tagName === "IMG")) el.removeAttribute(attr.name);
|
|
3936
4007
|
}
|
|
3937
4008
|
if (el.tagName === "IFRAME") {
|
|
3938
4009
|
if (attr.name === "srcdoc") {
|
|
3939
4010
|
el.removeAttribute(attr.name);
|
|
3940
|
-
|
|
3941
|
-
}
|
|
3942
|
-
if (attr.name === "src") {
|
|
3943
|
-
if (!isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
|
|
3944
|
-
return;
|
|
4011
|
+
continue;
|
|
3945
4012
|
}
|
|
4013
|
+
if (attr.name === "src" && !isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
|
|
3946
4014
|
}
|
|
3947
|
-
}
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
else Array.from(el.attributes).forEach((attr) => {
|
|
3952
|
-
if (![
|
|
4015
|
+
}
|
|
4016
|
+
if (tag === "input") {
|
|
4017
|
+
if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
|
|
4018
|
+
else for (const attr of Array.from(el.attributes)) if (![
|
|
3953
4019
|
"type",
|
|
3954
4020
|
"checked",
|
|
3955
4021
|
"contenteditable"
|
|
3956
4022
|
].includes(attr.name)) el.removeAttribute(attr.name);
|
|
3957
|
-
}
|
|
3958
|
-
}
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
3959
4025
|
return doc.body.innerHTML;
|
|
3960
4026
|
}
|
|
3961
4027
|
/**
|
|
@@ -4156,7 +4222,18 @@
|
|
|
4156
4222
|
const sel = window.getSelection();
|
|
4157
4223
|
sel.removeAllRanges();
|
|
4158
4224
|
sel.addRange(range);
|
|
4159
|
-
} catch (_) {
|
|
4225
|
+
} catch (_) {
|
|
4226
|
+
try {
|
|
4227
|
+
const fb = document.createRange();
|
|
4228
|
+
fb.setStart(this.editable, 0);
|
|
4229
|
+
fb.collapse(true);
|
|
4230
|
+
const s = window.getSelection();
|
|
4231
|
+
if (s) {
|
|
4232
|
+
s.removeAllRanges();
|
|
4233
|
+
s.addRange(fb);
|
|
4234
|
+
}
|
|
4235
|
+
} catch (_2) {}
|
|
4236
|
+
}
|
|
4160
4237
|
}
|
|
4161
4238
|
_savePoint() {
|
|
4162
4239
|
if (this.stackOffset < this.stack.length - 1) this.stack = this.stack.slice(0, this.stackOffset + 1);
|
|
@@ -4183,6 +4260,10 @@
|
|
|
4183
4260
|
* @returns {{ html: string, images: Object<string,string> }}
|
|
4184
4261
|
*/
|
|
4185
4262
|
_tokenizeImages(html) {
|
|
4263
|
+
if (!html.includes("data:")) return {
|
|
4264
|
+
html,
|
|
4265
|
+
images: {}
|
|
4266
|
+
};
|
|
4186
4267
|
const images = {};
|
|
4187
4268
|
let index = 0;
|
|
4188
4269
|
return {
|
|
@@ -4254,11 +4335,12 @@
|
|
|
4254
4335
|
* Inspired by Summernote's table handling
|
|
4255
4336
|
*/
|
|
4256
4337
|
/**
|
|
4257
|
-
*
|
|
4258
|
-
* @param {number} cols
|
|
4259
|
-
* @param {number} rows
|
|
4260
|
-
* @param {{ headerRow?: boolean }} [opts]
|
|
4261
|
-
* @
|
|
4338
|
+
* Build an HTML table with the given number of columns and rows, optionally including a header row.
|
|
4339
|
+
* @param {number} cols - Number of columns in each row.
|
|
4340
|
+
* @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
|
|
4341
|
+
* @param {{ headerRow?: boolean }} [opts] - Options object.
|
|
4342
|
+
* @param {boolean} [opts.headerRow=false] - When true and `rows > 0`, creates a header row (`<thead>`) plus body rows for the remainder.
|
|
4343
|
+
* @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
|
|
4262
4344
|
*/
|
|
4263
4345
|
function createTable(cols, rows, opts = {}) {
|
|
4264
4346
|
const { headerRow = false } = opts;
|
|
@@ -4267,7 +4349,7 @@
|
|
|
4267
4349
|
const thead = createElement("thead");
|
|
4268
4350
|
const tr = createElement("tr");
|
|
4269
4351
|
for (let c = 0; c < cols; c++) {
|
|
4270
|
-
const th = createElement("th", {}, ["
|
|
4352
|
+
const th = createElement("th", {}, [document.createElement("br")]);
|
|
4271
4353
|
tr.appendChild(th);
|
|
4272
4354
|
}
|
|
4273
4355
|
thead.appendChild(tr);
|
|
@@ -4279,7 +4361,7 @@
|
|
|
4279
4361
|
for (let r = 0; r < bodyRows; r++) {
|
|
4280
4362
|
const tr = createElement("tr");
|
|
4281
4363
|
for (let c = 0; c < cols; c++) {
|
|
4282
|
-
const td = createElement("td", {}, ["
|
|
4364
|
+
const td = createElement("td", {}, [document.createElement("br")]);
|
|
4283
4365
|
tr.appendChild(td);
|
|
4284
4366
|
}
|
|
4285
4367
|
tbody.appendChild(tr);
|
|
@@ -4287,13 +4369,52 @@
|
|
|
4287
4369
|
return table;
|
|
4288
4370
|
}
|
|
4289
4371
|
/**
|
|
4290
|
-
*
|
|
4291
|
-
* @param {number} cols
|
|
4292
|
-
* @param {number} rows
|
|
4293
|
-
* @param {{ headerRow?: boolean }} [opts]
|
|
4372
|
+
* Insert a table at the current selection and place the caret into its first cell.
|
|
4373
|
+
* @param {number} cols - Number of columns for the new table.
|
|
4374
|
+
* @param {number} rows - Number of rows for the new table.
|
|
4375
|
+
* @param {{ headerRow?: boolean }} [opts] - Options for table creation.
|
|
4376
|
+
* @param {boolean} [opts.headerRow=false] - If true, include a header row as the first row.
|
|
4294
4377
|
*/
|
|
4295
4378
|
function insertTable(cols, rows, opts = {}) {
|
|
4296
|
-
|
|
4379
|
+
if (cols <= 0 || rows <= 0) return;
|
|
4380
|
+
const table = createTable(cols, rows, opts);
|
|
4381
|
+
const sel = window.getSelection();
|
|
4382
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
4383
|
+
const range = sel.getRangeAt(0);
|
|
4384
|
+
range.deleteContents();
|
|
4385
|
+
const BLOCK = new Set([
|
|
4386
|
+
"P",
|
|
4387
|
+
"DIV",
|
|
4388
|
+
"H1",
|
|
4389
|
+
"H2",
|
|
4390
|
+
"H3",
|
|
4391
|
+
"H4",
|
|
4392
|
+
"H5",
|
|
4393
|
+
"H6",
|
|
4394
|
+
"BLOCKQUOTE",
|
|
4395
|
+
"LI",
|
|
4396
|
+
"PRE"
|
|
4397
|
+
]);
|
|
4398
|
+
let anchor = range.startContainer;
|
|
4399
|
+
if (anchor.nodeType === 3) anchor = anchor.parentElement;
|
|
4400
|
+
while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) anchor = anchor.parentElement;
|
|
4401
|
+
if (anchor && BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentNode) {
|
|
4402
|
+
anchor.after(table);
|
|
4403
|
+
if (!table.nextElementSibling) {
|
|
4404
|
+
const p = document.createElement("p");
|
|
4405
|
+
p.appendChild(document.createElement("br"));
|
|
4406
|
+
table.after(p);
|
|
4407
|
+
}
|
|
4408
|
+
if (!anchor.textContent.trim() && !anchor.querySelector("img, video, table")) anchor.remove();
|
|
4409
|
+
} else range.insertNode(table);
|
|
4410
|
+
const firstCell = table.querySelector("td, th");
|
|
4411
|
+
if (firstCell) {
|
|
4412
|
+
const nr = document.createRange();
|
|
4413
|
+
nr.setStart(firstCell, 0);
|
|
4414
|
+
nr.collapse(true);
|
|
4415
|
+
sel.removeAllRanges();
|
|
4416
|
+
sel.addRange(nr);
|
|
4417
|
+
}
|
|
4297
4418
|
}
|
|
4298
4419
|
//#endregion
|
|
4299
4420
|
//#region src/js/core/key.js
|
|
@@ -4508,7 +4629,7 @@
|
|
|
4508
4629
|
if (para && para.nodeName.toUpperCase() === "PRE") {
|
|
4509
4630
|
if (event.shiftKey) return false;
|
|
4510
4631
|
event.preventDefault();
|
|
4511
|
-
execCommand("insertText", "
|
|
4632
|
+
execCommand("insertText", " ".repeat(options.tabSize || 4));
|
|
4512
4633
|
return true;
|
|
4513
4634
|
}
|
|
4514
4635
|
if (options.tabSize) {
|
|
@@ -4601,6 +4722,11 @@
|
|
|
4601
4722
|
return true;
|
|
4602
4723
|
}
|
|
4603
4724
|
const para = closestPara(range.sc, editable);
|
|
4725
|
+
if (para && para.nodeName.toUpperCase() === "PRE") {
|
|
4726
|
+
event.preventDefault();
|
|
4727
|
+
execCommand("insertText", "\n");
|
|
4728
|
+
return true;
|
|
4729
|
+
}
|
|
4604
4730
|
if (para && para.nodeName.toUpperCase() === "BLOCKQUOTE") {
|
|
4605
4731
|
const native = range.toNativeRange();
|
|
4606
4732
|
native.setEnd(para, para.childNodes.length);
|
|
@@ -4634,11 +4760,22 @@
|
|
|
4634
4760
|
function htmlToMarkdown(html) {
|
|
4635
4761
|
return _domToMd(new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html").body).replace(/\n{3,}/g, "\n\n").trim();
|
|
4636
4762
|
}
|
|
4637
|
-
|
|
4763
|
+
/**
|
|
4764
|
+
* Convert a DOM node subtree into Markdown.
|
|
4765
|
+
*
|
|
4766
|
+
* Recursively produces a Markdown string representing the given DOM node and its descendants,
|
|
4767
|
+
* handling common HTML constructs such as paragraphs, headings, lists (with nested indentation),
|
|
4768
|
+
* blockquotes, fenced and inline code, links, images, tables, horizontal rules, and basic inline emphasis.
|
|
4769
|
+
*
|
|
4770
|
+
* @param {Node} node - The DOM node to convert.
|
|
4771
|
+
* @param {number} [depth=0] - Current nesting depth used to indent nested list items.
|
|
4772
|
+
* @returns {string} The Markdown representation of the node subtree.
|
|
4773
|
+
*/
|
|
4774
|
+
function _domToMd(node, depth = 0) {
|
|
4638
4775
|
if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
|
|
4639
4776
|
if (node.nodeType !== 1) return "";
|
|
4640
4777
|
const tag = node.nodeName.toLowerCase();
|
|
4641
|
-
const inner = () => Array.from(node.childNodes).map(_domToMd).join("");
|
|
4778
|
+
const inner = () => Array.from(node.childNodes).map((n) => _domToMd(n, depth)).join("");
|
|
4642
4779
|
switch (tag) {
|
|
4643
4780
|
case "p":
|
|
4644
4781
|
case "div": return `\n\n${inner()}\n\n`;
|
|
@@ -4678,12 +4815,16 @@
|
|
|
4678
4815
|
case "ul": {
|
|
4679
4816
|
const items = Array.from(node.querySelectorAll(":scope > li"));
|
|
4680
4817
|
if (!items.length) return inner();
|
|
4681
|
-
|
|
4818
|
+
const indent = " ".repeat(depth);
|
|
4819
|
+
const lines = items.map((li) => `${indent}- ${_domToMd(li, depth + 1).trim()}`).join("\n");
|
|
4820
|
+
return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
|
|
4682
4821
|
}
|
|
4683
4822
|
case "ol": {
|
|
4684
4823
|
const items = Array.from(node.querySelectorAll(":scope > li"));
|
|
4685
4824
|
if (!items.length) return inner();
|
|
4686
|
-
|
|
4825
|
+
const indent = " ".repeat(depth);
|
|
4826
|
+
const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
|
|
4827
|
+
return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
|
|
4687
4828
|
}
|
|
4688
4829
|
case "li": return inner();
|
|
4689
4830
|
case "hr": return "\n\n---\n\n";
|
|
@@ -4707,12 +4848,15 @@
|
|
|
4707
4848
|
}
|
|
4708
4849
|
}
|
|
4709
4850
|
/**
|
|
4710
|
-
*
|
|
4711
|
-
*
|
|
4712
|
-
*
|
|
4851
|
+
* Detects whether a string likely contains Markdown syntax.
|
|
4852
|
+
*
|
|
4853
|
+
* Checks for common Markdown constructs such as ATX headings, unordered or
|
|
4854
|
+
* ordered list items, blockquotes, fenced code blocks, and bold emphasis.
|
|
4855
|
+
* @param {string} text - Input text to inspect for Markdown patterns.
|
|
4856
|
+
* @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
|
|
4713
4857
|
*/
|
|
4714
4858
|
function isMarkdown(text) {
|
|
4715
|
-
return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S
|
|
4859
|
+
return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|^```|^\*{2}.+?\*{2}/m.test(text);
|
|
4716
4860
|
}
|
|
4717
4861
|
/**
|
|
4718
4862
|
* Converts a Markdown string to an HTML string.
|
|
@@ -5386,6 +5530,8 @@
|
|
|
5386
5530
|
* Toolbar.js - Builds and manages the editor toolbar UI
|
|
5387
5531
|
* Inspired by Summernote's Toolbar module — rewritten without jQuery
|
|
5388
5532
|
*/
|
|
5533
|
+
/** Resolve a toolbar item: string → registry lookup, object → pass-through. */
|
|
5534
|
+
var _resolveBtn = (item) => typeof item === "string" ? getButton(item) : item;
|
|
5389
5535
|
var _faPageLevelReady = null;
|
|
5390
5536
|
var _S$1 = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
|
|
5391
5537
|
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>`;
|
|
@@ -5475,15 +5621,19 @@
|
|
|
5475
5621
|
this._disposers = [];
|
|
5476
5622
|
/** @type {Array<() => void>} closers for all open color picker popups */
|
|
5477
5623
|
this._colorPickerClosers = [];
|
|
5624
|
+
/** @type {number|null} rAF handle for debounced refresh */
|
|
5625
|
+
this._refreshRaf = null;
|
|
5478
5626
|
}
|
|
5479
5627
|
initialize() {
|
|
5480
5628
|
this.el = createElement("div", { class: "an-toolbar" });
|
|
5481
5629
|
this._faReady = this._detectFontAwesome();
|
|
5482
5630
|
this._buildButtons();
|
|
5483
|
-
this._btnMap = new Map((this.options.toolbar || []).flat().map((b) => [b.name, b]));
|
|
5631
|
+
this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
|
|
5484
5632
|
return this;
|
|
5485
5633
|
}
|
|
5486
5634
|
destroy() {
|
|
5635
|
+
if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
|
|
5636
|
+
this._refreshRaf = null;
|
|
5487
5637
|
this._disposers.forEach((d) => d());
|
|
5488
5638
|
this._disposers = [];
|
|
5489
5639
|
if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
|
|
@@ -5494,7 +5644,12 @@
|
|
|
5494
5644
|
const fragment = document.createDocumentFragment();
|
|
5495
5645
|
toolbar.forEach((group) => {
|
|
5496
5646
|
const groupEl = createElement("div", { class: "an-btn-group" });
|
|
5497
|
-
group.forEach((
|
|
5647
|
+
group.forEach((item) => {
|
|
5648
|
+
const btnDef = _resolveBtn(item);
|
|
5649
|
+
if (!btnDef) {
|
|
5650
|
+
console.warn(`[AutumnNote] Toolbar: button "${item}" not found in registry. Skipped.`);
|
|
5651
|
+
return;
|
|
5652
|
+
}
|
|
5498
5653
|
let el;
|
|
5499
5654
|
if (btnDef.type === "select") el = this._createSelect(btnDef);
|
|
5500
5655
|
else if (btnDef.type === "grid") el = this._createGridPicker(btnDef);
|
|
@@ -5879,6 +6034,13 @@
|
|
|
5879
6034
|
return _faPageLevelReady;
|
|
5880
6035
|
}
|
|
5881
6036
|
refresh() {
|
|
6037
|
+
if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
|
|
6038
|
+
this._refreshRaf = requestAnimationFrame(() => {
|
|
6039
|
+
this._refreshRaf = null;
|
|
6040
|
+
this._doRefresh();
|
|
6041
|
+
});
|
|
6042
|
+
}
|
|
6043
|
+
_doRefresh() {
|
|
5882
6044
|
if (!this.el) return;
|
|
5883
6045
|
const btnMap = this._btnMap || /* @__PURE__ */ new Map();
|
|
5884
6046
|
this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
|
|
@@ -5907,6 +6069,24 @@
|
|
|
5907
6069
|
hide() {
|
|
5908
6070
|
if (this.el) this.el.style.display = "none";
|
|
5909
6071
|
}
|
|
6072
|
+
/**
|
|
6073
|
+
* Tears down and re-renders the toolbar in-place.
|
|
6074
|
+
* Call after registering new buttons post-create via context.use(plugin)
|
|
6075
|
+
* or AutumnNote.registerButton() to make them appear in the toolbar.
|
|
6076
|
+
*/
|
|
6077
|
+
rebuild() {
|
|
6078
|
+
if (this._refreshRaf) {
|
|
6079
|
+
cancelAnimationFrame(this._refreshRaf);
|
|
6080
|
+
this._refreshRaf = null;
|
|
6081
|
+
}
|
|
6082
|
+
this._disposers.forEach((d) => d());
|
|
6083
|
+
this._disposers = [];
|
|
6084
|
+
if (this.el) this.el.innerHTML = "";
|
|
6085
|
+
this._faReady = this._detectFontAwesome();
|
|
6086
|
+
this._buildButtons();
|
|
6087
|
+
this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
|
|
6088
|
+
this.refresh();
|
|
6089
|
+
}
|
|
5910
6090
|
};
|
|
5911
6091
|
//#endregion
|
|
5912
6092
|
//#region src/js/module/Statusbar.js
|
|
@@ -6056,7 +6236,7 @@
|
|
|
6056
6236
|
}
|
|
6057
6237
|
update() {
|
|
6058
6238
|
if (!this._wordCountEl || !this._charCountEl) return;
|
|
6059
|
-
const text = this.context.layoutInfo.editable.
|
|
6239
|
+
const text = this.context.layoutInfo.editable.textContent || "";
|
|
6060
6240
|
const words = _countWords(text);
|
|
6061
6241
|
const chars = text.replace(/\n/g, "").length;
|
|
6062
6242
|
const maxWords = this.options.maxWords || 0;
|
|
@@ -6251,7 +6431,7 @@
|
|
|
6251
6431
|
event.preventDefault();
|
|
6252
6432
|
const raw = clipboardData.getData("text/html");
|
|
6253
6433
|
const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
|
|
6254
|
-
const isSocialContent =
|
|
6434
|
+
const isSocialContent = /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
|
|
6255
6435
|
let html = raw;
|
|
6256
6436
|
if (isWordContent) html = this._cleanWordHtml(html);
|
|
6257
6437
|
else if (isSocialContent) html = this._cleanSocialHtml(html);
|
|
@@ -6353,7 +6533,7 @@
|
|
|
6353
6533
|
*/
|
|
6354
6534
|
_dataUrlToBlob(dataUrl) {
|
|
6355
6535
|
const [header, b64] = dataUrl.split(",");
|
|
6356
|
-
const mime = header.match(/:(.*?);/)[1];
|
|
6536
|
+
const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
|
|
6357
6537
|
const binary = atob(b64);
|
|
6358
6538
|
const arr = new Uint8Array(binary.length);
|
|
6359
6539
|
for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
|
|
@@ -7359,6 +7539,7 @@
|
|
|
7359
7539
|
_select(img) {
|
|
7360
7540
|
if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
|
|
7361
7541
|
this._activeImg = img;
|
|
7542
|
+
this._lastOverlayPos = null;
|
|
7362
7543
|
img.classList.add("an-image-selected");
|
|
7363
7544
|
this._updateOverlayPosition();
|
|
7364
7545
|
this._overlay.style.display = "block";
|
|
@@ -7382,6 +7563,14 @@
|
|
|
7382
7563
|
const offsetParent = this._overlay.offsetParent || this._container;
|
|
7383
7564
|
const containerRect = offsetParent.getBoundingClientRect();
|
|
7384
7565
|
const rect = this._activeImg.getBoundingClientRect();
|
|
7566
|
+
const p = this._lastOverlayPos;
|
|
7567
|
+
if (p && p.l === rect.left && p.t === rect.top && p.w === rect.width && p.h === rect.height) return;
|
|
7568
|
+
this._lastOverlayPos = {
|
|
7569
|
+
l: rect.left,
|
|
7570
|
+
t: rect.top,
|
|
7571
|
+
w: rect.width,
|
|
7572
|
+
h: rect.height
|
|
7573
|
+
};
|
|
7385
7574
|
const left = rect.left - containerRect.left + offsetParent.scrollLeft;
|
|
7386
7575
|
const top = rect.top - containerRect.top + offsetParent.scrollTop;
|
|
7387
7576
|
this._overlay.style.left = `${left}px`;
|
|
@@ -13080,7 +13269,7 @@
|
|
|
13080
13269
|
cells.forEach((cell) => {
|
|
13081
13270
|
cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
|
|
13082
13271
|
});
|
|
13083
|
-
labelEl.textContent = rows && cols ? `${
|
|
13272
|
+
labelEl.textContent = rows && cols ? `${rows} × ${cols}` : this.context.locale.contextMenu.table || "Insert Table";
|
|
13084
13273
|
};
|
|
13085
13274
|
panel.appendChild(gridEl);
|
|
13086
13275
|
panel.appendChild(labelEl);
|
|
@@ -13720,7 +13909,6 @@
|
|
|
13720
13909
|
if (!editable) return;
|
|
13721
13910
|
const rawMatches = this._findRawMatches(query, editable);
|
|
13722
13911
|
if (rawMatches.length === 0) return;
|
|
13723
|
-
this._currentIndex = 0;
|
|
13724
13912
|
for (let i = rawMatches.length - 1; i >= 0; i--) {
|
|
13725
13913
|
const { node, start, end } = rawMatches[i];
|
|
13726
13914
|
try {
|
|
@@ -13737,6 +13925,7 @@
|
|
|
13737
13925
|
}
|
|
13738
13926
|
this._matches.reverse();
|
|
13739
13927
|
this._matches = this._matches.filter((m) => m.mark);
|
|
13928
|
+
this._currentIndex = 0;
|
|
13740
13929
|
if (this._matches.length === 0) return;
|
|
13741
13930
|
if (this._matches[0] && this._matches[0].mark) {
|
|
13742
13931
|
this._matches[0].mark.className = "an-highlight an-highlight-current";
|
|
@@ -13762,12 +13951,13 @@
|
|
|
13762
13951
|
this._lastCaseSensitive = this._caseSensitive;
|
|
13763
13952
|
}
|
|
13764
13953
|
const re = this._queryRegex;
|
|
13954
|
+
const MAX_RESULTS = 500;
|
|
13765
13955
|
const walker = document.createTreeWalker(root, 4);
|
|
13766
13956
|
let node;
|
|
13767
|
-
while (node = walker.nextNode()) {
|
|
13957
|
+
while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
|
|
13768
13958
|
re.lastIndex = 0;
|
|
13769
13959
|
let m;
|
|
13770
|
-
while ((m = re.exec(node.textContent)) !== null) results.push({
|
|
13960
|
+
while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
|
|
13771
13961
|
node,
|
|
13772
13962
|
start: m.index,
|
|
13773
13963
|
end: m.index + m[0].length
|
|
@@ -13854,7 +14044,7 @@
|
|
|
13854
14044
|
const total = this._matches.length;
|
|
13855
14045
|
if (total === 0) {
|
|
13856
14046
|
const query = this._findInput ? this._findInput.value : "";
|
|
13857
|
-
this._counterEl.textContent = query ?
|
|
14047
|
+
this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
|
|
13858
14048
|
} else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
|
|
13859
14049
|
}
|
|
13860
14050
|
};
|
|
@@ -14053,17 +14243,33 @@
|
|
|
14053
14243
|
e.preventDefault();
|
|
14054
14244
|
e.stopPropagation();
|
|
14055
14245
|
this._startHandleDrag(e, id);
|
|
14056
|
-
}))
|
|
14246
|
+
}), on(h, "touchstart", (e) => {
|
|
14247
|
+
e.preventDefault();
|
|
14248
|
+
e.stopPropagation();
|
|
14249
|
+
this._startHandleDrag({
|
|
14250
|
+
clientX: e.touches[0].clientX,
|
|
14251
|
+
clientY: e.touches[0].clientY
|
|
14252
|
+
}, id);
|
|
14253
|
+
}, { passive: false }));
|
|
14057
14254
|
this._handles[id] = h;
|
|
14058
14255
|
cropBox.appendChild(h);
|
|
14059
14256
|
});
|
|
14060
14257
|
this._disposers.push(on(cropBox, "mousedown", (e) => {
|
|
14061
|
-
if (e.target
|
|
14062
|
-
if (e.target
|
|
14258
|
+
if (e.target.classList.contains("an-crop-handle")) return;
|
|
14259
|
+
if (e.target !== cropBox && e.target !== grid) return;
|
|
14063
14260
|
e.preventDefault();
|
|
14064
14261
|
e.stopPropagation();
|
|
14065
14262
|
this._startBoxMove(e);
|
|
14066
|
-
}))
|
|
14263
|
+
}), on(cropBox, "touchstart", (e) => {
|
|
14264
|
+
if (e.target.classList.contains("an-crop-handle")) return;
|
|
14265
|
+
if (e.target !== cropBox && e.target !== grid) return;
|
|
14266
|
+
e.preventDefault();
|
|
14267
|
+
e.stopPropagation();
|
|
14268
|
+
this._startBoxMove({
|
|
14269
|
+
clientX: e.touches[0].clientX,
|
|
14270
|
+
clientY: e.touches[0].clientY
|
|
14271
|
+
});
|
|
14272
|
+
}, { passive: false }));
|
|
14067
14273
|
const infoEl = document.createElement("div");
|
|
14068
14274
|
infoEl.className = "an-crop-info";
|
|
14069
14275
|
infoEl.style.cssText = `
|
|
@@ -14221,15 +14427,26 @@
|
|
|
14221
14427
|
* @param {(e: MouseEvent) => void} onMove
|
|
14222
14428
|
*/
|
|
14223
14429
|
_attachDocDrag(onMove) {
|
|
14430
|
+
const onTouchMove = (e) => {
|
|
14431
|
+
e.preventDefault();
|
|
14432
|
+
onMove({
|
|
14433
|
+
clientX: e.touches[0].clientX,
|
|
14434
|
+
clientY: e.touches[0].clientY
|
|
14435
|
+
});
|
|
14436
|
+
};
|
|
14224
14437
|
const cleanup = () => {
|
|
14225
14438
|
document.removeEventListener("mousemove", onMove);
|
|
14226
14439
|
document.removeEventListener("mouseup", cleanup);
|
|
14440
|
+
document.removeEventListener("touchmove", onTouchMove);
|
|
14441
|
+
document.removeEventListener("touchend", cleanup);
|
|
14227
14442
|
document.body.style.userSelect = "";
|
|
14228
14443
|
document.body.style.cursor = "";
|
|
14229
14444
|
};
|
|
14230
14445
|
document.body.style.userSelect = "none";
|
|
14231
14446
|
document.addEventListener("mousemove", onMove);
|
|
14232
14447
|
document.addEventListener("mouseup", cleanup, { once: true });
|
|
14448
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
14449
|
+
document.addEventListener("touchend", cleanup, { once: true });
|
|
14233
14450
|
}
|
|
14234
14451
|
_bindEsc() {
|
|
14235
14452
|
const handler = (e) => {
|
|
@@ -14270,7 +14487,7 @@
|
|
|
14270
14487
|
height: natH
|
|
14271
14488
|
}, w, h);
|
|
14272
14489
|
if (!canvas) {
|
|
14273
|
-
|
|
14490
|
+
this._showCropError("Cannot crop this image: the image server does not allow cross-origin access. Upload the image directly to use the crop tool.");
|
|
14274
14491
|
this._close(false);
|
|
14275
14492
|
return;
|
|
14276
14493
|
}
|
|
@@ -14287,6 +14504,37 @@
|
|
|
14287
14504
|
this.context.invoke("imageResizer.updateOverlay");
|
|
14288
14505
|
}
|
|
14289
14506
|
/**
|
|
14507
|
+
* Show a non-blocking inline error banner appended to document.body.
|
|
14508
|
+
* Auto-dismisses after 4 seconds.
|
|
14509
|
+
* @param {string} msg
|
|
14510
|
+
*/
|
|
14511
|
+
_showCropError(msg) {
|
|
14512
|
+
const banner = document.createElement("div");
|
|
14513
|
+
banner.setAttribute("role", "alert");
|
|
14514
|
+
banner.style.cssText = [
|
|
14515
|
+
"position:fixed",
|
|
14516
|
+
"bottom:24px",
|
|
14517
|
+
"left:50%",
|
|
14518
|
+
"transform:translateX(-50%)",
|
|
14519
|
+
"z-index:10200",
|
|
14520
|
+
"max-width:420px",
|
|
14521
|
+
"width:max-content",
|
|
14522
|
+
"background:#7f1d1d",
|
|
14523
|
+
"color:#fecaca",
|
|
14524
|
+
"border:1px solid #b91c1c",
|
|
14525
|
+
"border-radius:8px",
|
|
14526
|
+
"padding:12px 18px",
|
|
14527
|
+
"font:13px/1.5 system-ui,sans-serif",
|
|
14528
|
+
"box-shadow:0 4px 16px rgba(0,0,0,.4)",
|
|
14529
|
+
"pointer-events:auto"
|
|
14530
|
+
].join(";");
|
|
14531
|
+
banner.textContent = msg;
|
|
14532
|
+
document.body.appendChild(banner);
|
|
14533
|
+
setTimeout(() => {
|
|
14534
|
+
if (banner.parentNode) banner.parentNode.removeChild(banner);
|
|
14535
|
+
}, 4e3);
|
|
14536
|
+
}
|
|
14537
|
+
/**
|
|
14290
14538
|
* Remove all overlay DOM elements and reset state.
|
|
14291
14539
|
* @param {boolean} _committed - reserved for future use
|
|
14292
14540
|
*/
|
|
@@ -14701,6 +14949,16 @@
|
|
|
14701
14949
|
if (!editable) return;
|
|
14702
14950
|
editable.focus();
|
|
14703
14951
|
document.execCommand("removeFormat");
|
|
14952
|
+
const sel = window.getSelection();
|
|
14953
|
+
if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
|
|
14954
|
+
const range = sel.getRangeAt(0);
|
|
14955
|
+
const ancestor = range.commonAncestorContainer;
|
|
14956
|
+
const root = ancestor.nodeType === 1 ? ancestor : ancestor.parentElement;
|
|
14957
|
+
if (root) {
|
|
14958
|
+
const candidates = [root, ...root.querySelectorAll("[style]")];
|
|
14959
|
+
for (const el of candidates) if (el.hasAttribute("style") && range.intersectsNode(el)) el.removeAttribute("style");
|
|
14960
|
+
}
|
|
14961
|
+
}
|
|
14704
14962
|
ctx.invoke("editor.afterCommand");
|
|
14705
14963
|
},
|
|
14706
14964
|
inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
|
|
@@ -14814,6 +15072,7 @@
|
|
|
14814
15072
|
}
|
|
14815
15073
|
document.body.appendChild(el);
|
|
14816
15074
|
this._el = el;
|
|
15075
|
+
this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
|
|
14817
15076
|
}
|
|
14818
15077
|
_buildColorPicker() {
|
|
14819
15078
|
const picker = document.createElement("div");
|
|
@@ -14899,8 +15158,13 @@
|
|
|
14899
15158
|
editable.focus();
|
|
14900
15159
|
const sel = window.getSelection();
|
|
14901
15160
|
sel.removeAllRanges();
|
|
14902
|
-
|
|
14903
|
-
|
|
15161
|
+
try {
|
|
15162
|
+
sel.addRange(this._savedRange.cloneRange());
|
|
15163
|
+
} catch (_) {
|
|
15164
|
+
return;
|
|
15165
|
+
}
|
|
15166
|
+
const cmd = type === "hiliteColor" ? "hiliteColor" : type;
|
|
15167
|
+
if (!document.execCommand(cmd, false, color) && cmd === "hiliteColor") document.execCommand("backColor", false, color);
|
|
14904
15168
|
this.context.invoke("editor.afterCommand");
|
|
14905
15169
|
const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
|
|
14906
15170
|
const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
|
|
@@ -14935,8 +15199,8 @@
|
|
|
14935
15199
|
this._closeColorPicker();
|
|
14936
15200
|
}
|
|
14937
15201
|
_syncActive() {
|
|
14938
|
-
if (!this.
|
|
14939
|
-
this.
|
|
15202
|
+
if (!this._btnCache) return;
|
|
15203
|
+
this._btnCache.forEach((btn) => {
|
|
14940
15204
|
const activeFn = _ACTIVE[btn.dataset.name];
|
|
14941
15205
|
btn.classList.toggle("an-active", !!(activeFn && activeFn()));
|
|
14942
15206
|
});
|
|
@@ -15083,14 +15347,23 @@
|
|
|
15083
15347
|
const el = document.createElement("div");
|
|
15084
15348
|
el.className = "an-mention-dropdown";
|
|
15085
15349
|
el.setAttribute("role", "listbox");
|
|
15350
|
+
el.addEventListener("mousedown", (e) => e.preventDefault());
|
|
15351
|
+
el.addEventListener("click", (e) => {
|
|
15352
|
+
const item = e.target.closest(".an-mention-item");
|
|
15353
|
+
if (item) this._select(+item.dataset.index);
|
|
15354
|
+
});
|
|
15355
|
+
el.addEventListener("mousemove", (e) => {
|
|
15356
|
+
const item = e.target.closest(".an-mention-item");
|
|
15357
|
+
if (item) this._highlightItem(+item.dataset.index);
|
|
15358
|
+
});
|
|
15086
15359
|
document.body.appendChild(el);
|
|
15087
15360
|
this._dropdown = el;
|
|
15088
15361
|
}
|
|
15089
15362
|
_renderItems(items) {
|
|
15090
15363
|
const dd = this._dropdown;
|
|
15091
|
-
dd.innerHTML = "";
|
|
15092
15364
|
this._items = items.slice(0, this._cfg.maxResults);
|
|
15093
15365
|
this._activeIndex = this._items.length > 0 ? 0 : -1;
|
|
15366
|
+
const frag = document.createDocumentFragment();
|
|
15094
15367
|
this._items.forEach((item, i) => {
|
|
15095
15368
|
const li = document.createElement("div");
|
|
15096
15369
|
li.className = "an-mention-item";
|
|
@@ -15106,10 +15379,10 @@
|
|
|
15106
15379
|
const label = document.createElement("span");
|
|
15107
15380
|
label.textContent = item.label;
|
|
15108
15381
|
li.appendChild(label);
|
|
15109
|
-
|
|
15110
|
-
li.addEventListener("click", () => this._select(i));
|
|
15111
|
-
dd.appendChild(li);
|
|
15382
|
+
frag.appendChild(li);
|
|
15112
15383
|
});
|
|
15384
|
+
dd.innerHTML = "";
|
|
15385
|
+
dd.appendChild(frag);
|
|
15113
15386
|
this._highlightItem(this._activeIndex);
|
|
15114
15387
|
}
|
|
15115
15388
|
_highlightItem(index) {
|
|
@@ -15261,6 +15534,8 @@
|
|
|
15261
15534
|
*/
|
|
15262
15535
|
/** Module registry shared across all Context instances (populated via AutumnNote.registerModule). */
|
|
15263
15536
|
var _customModules = /* @__PURE__ */ new Map();
|
|
15537
|
+
/** Global plugin registry (populated via AutumnNote.use()). Applied to every new Context. */
|
|
15538
|
+
var _globalPlugins = /* @__PURE__ */ new Map();
|
|
15264
15539
|
var Context = class {
|
|
15265
15540
|
/**
|
|
15266
15541
|
* @param {HTMLElement} targetEl - The element to replace with the editor
|
|
@@ -15277,6 +15552,8 @@
|
|
|
15277
15552
|
this._listeners = /* @__PURE__ */ new Map();
|
|
15278
15553
|
/** @type {Map<string, object>} */
|
|
15279
15554
|
this._modules = /* @__PURE__ */ new Map();
|
|
15555
|
+
/** @type {Map<string, { plugin: object, publicApi: * }>} */
|
|
15556
|
+
this._plugins = /* @__PURE__ */ new Map();
|
|
15280
15557
|
this._disposers = [];
|
|
15281
15558
|
this._alive = false;
|
|
15282
15559
|
}
|
|
@@ -15299,6 +15576,7 @@
|
|
|
15299
15576
|
if (this.options.focus) editable.focus();
|
|
15300
15577
|
this._alive = true;
|
|
15301
15578
|
this.invoke("toolbar.refresh");
|
|
15579
|
+
this._applyGlobalPlugins();
|
|
15302
15580
|
if (typeof this.options.onInit === "function") this.options.onInit(this);
|
|
15303
15581
|
return this;
|
|
15304
15582
|
}
|
|
@@ -15350,6 +15628,46 @@
|
|
|
15350
15628
|
this._modules.set(name, instance);
|
|
15351
15629
|
return this;
|
|
15352
15630
|
}
|
|
15631
|
+
/**
|
|
15632
|
+
* Installs a plugin on this editor instance.
|
|
15633
|
+
* If called after create(), buttons are registered immediately but the toolbar
|
|
15634
|
+
* must be rebuilt via ctx.invoke('toolbar.rebuild') to render new buttons.
|
|
15635
|
+
* @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
|
|
15636
|
+
* @param {object} [options] - Forwarded to plugin.install(context, options)
|
|
15637
|
+
* @returns {this}
|
|
15638
|
+
*/
|
|
15639
|
+
use(plugin, options = {}) {
|
|
15640
|
+
if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
|
|
15641
|
+
this._installPlugin(plugin, options);
|
|
15642
|
+
return this;
|
|
15643
|
+
}
|
|
15644
|
+
/**
|
|
15645
|
+
* Returns the public API returned by plugin.install(), or null.
|
|
15646
|
+
* @param {string} name
|
|
15647
|
+
* @returns {*}
|
|
15648
|
+
*/
|
|
15649
|
+
getPlugin(name) {
|
|
15650
|
+
return this._plugins.get(name)?.publicApi ?? null;
|
|
15651
|
+
}
|
|
15652
|
+
_installPlugin(plugin, pluginOptions = {}) {
|
|
15653
|
+
const { name } = plugin;
|
|
15654
|
+
if (!name || typeof name !== "string") {
|
|
15655
|
+
console.warn("[AutumnNote] Plugin must have a string `name` property.");
|
|
15656
|
+
return;
|
|
15657
|
+
}
|
|
15658
|
+
if (this._plugins.has(name)) {
|
|
15659
|
+
console.warn(`[AutumnNote] Plugin "${name}" already installed on this instance. Skipping.`);
|
|
15660
|
+
return;
|
|
15661
|
+
}
|
|
15662
|
+
const publicApi = typeof plugin.install === "function" ? plugin.install(this, pluginOptions) ?? null : null;
|
|
15663
|
+
this._plugins.set(name, {
|
|
15664
|
+
plugin,
|
|
15665
|
+
publicApi
|
|
15666
|
+
});
|
|
15667
|
+
}
|
|
15668
|
+
_applyGlobalPlugins() {
|
|
15669
|
+
for (const { plugin, options } of _globalPlugins.values()) this._installPlugin(plugin, options);
|
|
15670
|
+
}
|
|
15353
15671
|
_bindEditorEvents(editable) {
|
|
15354
15672
|
const d0 = on(editable, "input", () => this._syncToTarget());
|
|
15355
15673
|
const d1 = on(editable, "focus", () => {
|
|
@@ -15600,6 +15918,10 @@
|
|
|
15600
15918
|
if (typeof module.destroy === "function") module.destroy();
|
|
15601
15919
|
});
|
|
15602
15920
|
this._modules.clear();
|
|
15921
|
+
for (const { plugin } of this._plugins.values()) if (typeof plugin.uninstall === "function") try {
|
|
15922
|
+
plugin.uninstall(this);
|
|
15923
|
+
} catch (_) {}
|
|
15924
|
+
this._plugins.clear();
|
|
15603
15925
|
this._disposers.forEach((d) => d());
|
|
15604
15926
|
this._disposers = [];
|
|
15605
15927
|
const container = this.layoutInfo.container;
|
|
@@ -15668,7 +15990,27 @@
|
|
|
15668
15990
|
registerModule(name, ModuleClass) {
|
|
15669
15991
|
_customModules.set(name, ModuleClass);
|
|
15670
15992
|
},
|
|
15671
|
-
|
|
15993
|
+
use(plugin, options = {}) {
|
|
15994
|
+
if (!plugin || typeof plugin.name !== "string") throw new TypeError("[AutumnNote] AutumnNote.use: plugin must have a string `name` property.");
|
|
15995
|
+
if (_globalPlugins.has(plugin.name)) {
|
|
15996
|
+
console.warn(`[AutumnNote] Plugin "${plugin.name}" already registered globally. Skipping.`);
|
|
15997
|
+
return this;
|
|
15998
|
+
}
|
|
15999
|
+
if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
|
|
16000
|
+
_globalPlugins.set(plugin.name, {
|
|
16001
|
+
plugin,
|
|
16002
|
+
options
|
|
16003
|
+
});
|
|
16004
|
+
return this;
|
|
16005
|
+
},
|
|
16006
|
+
hasPlugin(name) {
|
|
16007
|
+
return _globalPlugins.has(name);
|
|
16008
|
+
},
|
|
16009
|
+
registerButton(btnDef) {
|
|
16010
|
+
registerButton(btnDef);
|
|
16011
|
+
return this;
|
|
16012
|
+
},
|
|
16013
|
+
version: "1.4.0"
|
|
15672
16014
|
};
|
|
15673
16015
|
/**
|
|
15674
16016
|
* @param {string|Element|NodeList|Element[]} selector
|