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.es.js
CHANGED
|
@@ -27,19 +27,29 @@ function debounce(fn, delay) {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param {Function} fn
|
|
32
|
-
* @param {number} limit - milliseconds
|
|
33
|
-
* @returns {Function}
|
|
30
|
+
* Create a wrapper that limits how often `fn` can be invoked while ensuring the last call in a burst is executed.
|
|
31
|
+
* @param {Function} fn - Function to be throttled.
|
|
32
|
+
* @param {number} limit - Time window in milliseconds during which at most one call is allowed.
|
|
33
|
+
* @returns {Function} A wrapper function that invokes `fn` at most once per `limit` milliseconds; calls preserve `this` and original arguments and schedule a trailing invocation for the final call in a burst.
|
|
34
34
|
*/
|
|
35
35
|
function throttle(fn, limit) {
|
|
36
|
-
let lastCall =
|
|
36
|
+
let lastCall = -Infinity;
|
|
37
|
+
let trailingTimer = null;
|
|
37
38
|
return function(...args) {
|
|
38
|
-
const now =
|
|
39
|
-
|
|
39
|
+
const now = performance.now();
|
|
40
|
+
const elapsed = now - lastCall;
|
|
41
|
+
if (elapsed >= limit) {
|
|
40
42
|
lastCall = now;
|
|
43
|
+
clearTimeout(trailingTimer);
|
|
44
|
+
trailingTimer = null;
|
|
41
45
|
return fn.apply(this, args);
|
|
42
46
|
}
|
|
47
|
+
clearTimeout(trailingTimer);
|
|
48
|
+
trailingTimer = setTimeout(() => {
|
|
49
|
+
lastCall = performance.now();
|
|
50
|
+
trailingTimer = null;
|
|
51
|
+
fn.apply(this, args);
|
|
52
|
+
}, limit - elapsed);
|
|
43
53
|
};
|
|
44
54
|
}
|
|
45
55
|
/**
|
|
@@ -118,12 +128,12 @@ function isPlainObject(val) {
|
|
|
118
128
|
function rect2bnd(rect) {
|
|
119
129
|
if (!rect) return null;
|
|
120
130
|
return {
|
|
121
|
-
top:
|
|
122
|
-
left:
|
|
123
|
-
width:
|
|
124
|
-
height:
|
|
125
|
-
bottom:
|
|
126
|
-
right:
|
|
131
|
+
top: rect.top,
|
|
132
|
+
left: rect.left,
|
|
133
|
+
width: rect.width,
|
|
134
|
+
height: rect.height,
|
|
135
|
+
bottom: rect.bottom,
|
|
136
|
+
right: rect.right
|
|
127
137
|
};
|
|
128
138
|
}
|
|
129
139
|
//#endregion
|
|
@@ -283,13 +293,16 @@ function nodeValue(node) {
|
|
|
283
293
|
return isText(node) ? node.nodeValue : node.textContent || "";
|
|
284
294
|
}
|
|
285
295
|
/**
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
296
|
+
* Determine whether a DOM node contains no visible content.
|
|
297
|
+
*
|
|
298
|
+
* Text nodes are considered empty when their `nodeValue` is empty. Void elements (e.g., `img`, `br`, `input`) are considered non-empty. An element with exactly one `<br>` child is treated as empty. For other elements, emptiness means trimmed `textContent` is empty and there are no descendant `img`, `video`, `hr`, or `table` elements.
|
|
299
|
+
* @param {Node} node - Node to inspect for visible content.
|
|
300
|
+
* @returns {boolean} `true` if the node has no visible content, `false` otherwise.
|
|
289
301
|
*/
|
|
290
302
|
function isEmpty(node) {
|
|
291
303
|
if (isText(node)) return !node.nodeValue;
|
|
292
304
|
if (isVoid(node)) return false;
|
|
305
|
+
if (node.childNodes.length === 1 && node.firstChild?.nodeName === "BR") return true;
|
|
293
306
|
return !node.textContent.trim() && !node.querySelector("img, video, hr, table");
|
|
294
307
|
}
|
|
295
308
|
/**
|
|
@@ -701,9 +714,15 @@ function outdent() {
|
|
|
701
714
|
execCommand("outdent");
|
|
702
715
|
}
|
|
703
716
|
/**
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
717
|
+
* Convert a checklist <li> into a paragraph and move any following items into a new checklist.
|
|
718
|
+
*
|
|
719
|
+
* Preserves inline markup from the converted item, strips zero-width space anchors,
|
|
720
|
+
* and replaces empty content with a non‑breaking space. If there are list items
|
|
721
|
+
* after the converted item they are moved into a new <ul class="an-checklist">
|
|
722
|
+
* inserted immediately after the original list. The original <li> is removed and
|
|
723
|
+
* the original list is removed if it becomes empty. Attempts to place the caret
|
|
724
|
+
* at the start of the newly created <p>.
|
|
725
|
+
* @param {HTMLElement} checkLi - The checklist `<li>` element to convert to a `<p>`.
|
|
707
726
|
*/
|
|
708
727
|
function _checklistItemToP(checkLi) {
|
|
709
728
|
const checkUl = checkLi.closest(".an-checklist");
|
|
@@ -712,7 +731,15 @@ function _checklistItemToP(checkLi) {
|
|
|
712
731
|
const liIndex = allLis.indexOf(checkLi);
|
|
713
732
|
const afterLis = allLis.slice(liIndex + 1);
|
|
714
733
|
const p = document.createElement("p");
|
|
715
|
-
|
|
734
|
+
for (const child of checkLi.childNodes) {
|
|
735
|
+
if (child.nodeType === 1 && child.tagName === "INPUT") continue;
|
|
736
|
+
p.appendChild(child.cloneNode(true));
|
|
737
|
+
}
|
|
738
|
+
p.innerHTML = p.innerHTML.replace(/\u200B/g, "");
|
|
739
|
+
if (!p.hasChildNodes() || !p.textContent.trim()) {
|
|
740
|
+
p.innerHTML = "";
|
|
741
|
+
p.appendChild(document.createTextNode("\xA0"));
|
|
742
|
+
}
|
|
716
743
|
if (afterLis.length > 0) {
|
|
717
744
|
const newUl = document.createElement("ul");
|
|
718
745
|
newUl.className = "an-checklist";
|
|
@@ -743,9 +770,12 @@ var insertUnorderedList = () => execCommand("insertUnorderedList");
|
|
|
743
770
|
*/
|
|
744
771
|
var insertOrderedList = () => execCommand("insertOrderedList");
|
|
745
772
|
/**
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
773
|
+
* Set the line-height on every block-level element that intersects the current selection.
|
|
774
|
+
*
|
|
775
|
+
* If the selection is collapsed, the nearest enclosing block element receives the style.
|
|
776
|
+
* For a non-collapsed selection, all unique block ancestors of text nodes that intersect the range are updated;
|
|
777
|
+
* if none are found, the nearest block ancestor of the range's common ancestor is updated.
|
|
778
|
+
* @param {string} value - Line-height value to apply; typically a unitless multiplier (for example, "1.5").
|
|
749
779
|
*/
|
|
750
780
|
function lineHeight(value) {
|
|
751
781
|
const sel = window.getSelection();
|
|
@@ -780,9 +810,9 @@ function lineHeight(value) {
|
|
|
780
810
|
return;
|
|
781
811
|
}
|
|
782
812
|
const blocks = /* @__PURE__ */ new Set();
|
|
783
|
-
const iter = document.
|
|
813
|
+
const iter = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, { acceptNode: (node) => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP });
|
|
784
814
|
let textNode;
|
|
785
|
-
while (textNode = iter.nextNode())
|
|
815
|
+
while (textNode = iter.nextNode()) {
|
|
786
816
|
const block = nearestBlock(textNode);
|
|
787
817
|
if (block) blocks.add(block);
|
|
788
818
|
}
|
|
@@ -864,9 +894,18 @@ function isInlineCode() {
|
|
|
864
894
|
return !!(code && !code.closest("pre"));
|
|
865
895
|
}
|
|
866
896
|
/**
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
897
|
+
* Toggle a checklist at the current selection or caret.
|
|
898
|
+
*
|
|
899
|
+
* When the selection is inside an existing checklist `<ul class="an-checklist">`,
|
|
900
|
+
* converts the selected `<li>` items back into `<p>` paragraphs and places the caret
|
|
901
|
+
* at the start of the first converted paragraph. Otherwise creates a checklist:
|
|
902
|
+
* - If the selection is collapsed, converts the nearest block-level ancestor (or inserts
|
|
903
|
+
* a single checklist item at the editable root) into a checklist with one item containing
|
|
904
|
+
* that block's text and places the caret inside the new item.
|
|
905
|
+
* - If the selection is a range, converts each intersecting block element into one checklist
|
|
906
|
+
* item (preserving textual content) and places the caret at the end of the last item.
|
|
907
|
+
*
|
|
908
|
+
* Empty or whitespace-only selections do not create a checklist.
|
|
870
909
|
*/
|
|
871
910
|
function toggleChecklist() {
|
|
872
911
|
const sel = window.getSelection();
|
|
@@ -925,8 +964,9 @@ function toggleChecklist() {
|
|
|
925
964
|
ul.appendChild(li);
|
|
926
965
|
if (block && BLOCK_TAGS.has(block.tagName)) block.parentNode.replaceChild(ul, block);
|
|
927
966
|
else {
|
|
928
|
-
|
|
929
|
-
|
|
967
|
+
const nativeRange = sel.getRangeAt(0);
|
|
968
|
+
nativeRange.deleteContents();
|
|
969
|
+
nativeRange.insertNode(ul);
|
|
930
970
|
}
|
|
931
971
|
const textNode = li.lastChild;
|
|
932
972
|
const nr = document.createRange();
|
|
@@ -1049,6 +1089,34 @@ function btn(name, icon, tooltip, action, isActive, isDisabled) {
|
|
|
1049
1089
|
isDisabled
|
|
1050
1090
|
};
|
|
1051
1091
|
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Global registry for custom buttons registered via AutumnNote.registerButton()
|
|
1094
|
+
* or via a plugin's `buttons` array. Toolbar resolves string names from here.
|
|
1095
|
+
* @type {Map<string, object>}
|
|
1096
|
+
*/
|
|
1097
|
+
var _buttonRegistry = /* @__PURE__ */ new Map();
|
|
1098
|
+
/**
|
|
1099
|
+
* Registers a button definition in the global registry so it can be referenced
|
|
1100
|
+
* by string name in toolbar configuration: `toolbar: [['myBtn', boldBtn]]`.
|
|
1101
|
+
* @param {object} btnDef - Any ToolbarItemDef-compatible object with a `name` string.
|
|
1102
|
+
*/
|
|
1103
|
+
function registerButton(btnDef) {
|
|
1104
|
+
if (!btnDef || typeof btnDef.name !== "string") {
|
|
1105
|
+
console.warn("[AutumnNote] registerButton: btnDef must have a string `name` property.");
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (_buttonRegistry.has(btnDef.name)) console.warn(`[AutumnNote] registerButton: overwriting existing button "${btnDef.name}".`);
|
|
1109
|
+
_buttonRegistry.set(btnDef.name, btnDef);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Looks up a button definition by name from the global registry.
|
|
1113
|
+
* Returns undefined when not found.
|
|
1114
|
+
* @param {string} name
|
|
1115
|
+
* @returns {object|undefined}
|
|
1116
|
+
*/
|
|
1117
|
+
function getButton(name) {
|
|
1118
|
+
return _buttonRegistry.get(name);
|
|
1119
|
+
}
|
|
1052
1120
|
var boldBtn = btn("bold", "bold", "Bold (Ctrl+B)", () => bold(), () => document.queryCommandState("bold"));
|
|
1053
1121
|
var italicBtn = btn("italic", "italic", "Italic (Ctrl+I)", () => italic(), () => document.queryCommandState("italic"));
|
|
1054
1122
|
var underlineBtn = btn("underline", "underline", "Underline (Ctrl+U)", () => underline(), () => {
|
|
@@ -1614,6 +1682,7 @@ var en = {
|
|
|
1614
1682
|
replaceAriaLabel: "Replace with",
|
|
1615
1683
|
replaceBtn: "Replace",
|
|
1616
1684
|
replaceAllBtn: "Replace All",
|
|
1685
|
+
noResults: "No results",
|
|
1617
1686
|
close: "×"
|
|
1618
1687
|
},
|
|
1619
1688
|
shortcutsDialog: {
|
|
@@ -1966,6 +2035,7 @@ var locales = {
|
|
|
1966
2035
|
replaceAriaLabel: "Thay thế bằng",
|
|
1967
2036
|
replaceBtn: "Thay thế",
|
|
1968
2037
|
replaceAllBtn: "Thay thế tất cả",
|
|
2038
|
+
noResults: "Không có kết quả",
|
|
1969
2039
|
close: "×"
|
|
1970
2040
|
},
|
|
1971
2041
|
shortcutsDialog: {
|
|
@@ -2295,6 +2365,7 @@ var locales = {
|
|
|
2295
2365
|
replacePlaceholder: "置換後…",
|
|
2296
2366
|
replaceAriaLabel: "置換後のテキスト",
|
|
2297
2367
|
replaceBtn: "置換",
|
|
2368
|
+
noResults: "結果なし",
|
|
2298
2369
|
replaceAllBtn: "すべて置換",
|
|
2299
2370
|
close: "×"
|
|
2300
2371
|
},
|
|
@@ -2625,6 +2696,7 @@ var locales = {
|
|
|
2625
2696
|
replacePlaceholder: "替换为…",
|
|
2626
2697
|
replaceAriaLabel: "替换为",
|
|
2627
2698
|
replaceBtn: "替换",
|
|
2699
|
+
noResults: "没有结果",
|
|
2628
2700
|
replaceAllBtn: "全部替换",
|
|
2629
2701
|
close: "×"
|
|
2630
2702
|
},
|
|
@@ -2955,6 +3027,7 @@ var locales = {
|
|
|
2955
3027
|
replacePlaceholder: "Remplacer par…",
|
|
2956
3028
|
replaceAriaLabel: "Remplacer par",
|
|
2957
3029
|
replaceBtn: "Remplacer",
|
|
3030
|
+
noResults: "Aucun résultat",
|
|
2958
3031
|
replaceAllBtn: "Tout remplacer",
|
|
2959
3032
|
close: "×"
|
|
2960
3033
|
},
|
|
@@ -3285,6 +3358,7 @@ var locales = {
|
|
|
3285
3358
|
replacePlaceholder: "Ersetzen durch…",
|
|
3286
3359
|
replaceAriaLabel: "Ersetzen durch",
|
|
3287
3360
|
replaceBtn: "Ersetzen",
|
|
3361
|
+
noResults: "Keine Ergebnisse",
|
|
3288
3362
|
replaceAllBtn: "Alle ersetzen",
|
|
3289
3363
|
close: "×"
|
|
3290
3364
|
},
|
|
@@ -3615,6 +3689,7 @@ var locales = {
|
|
|
3615
3689
|
replacePlaceholder: "Reemplazar con…",
|
|
3616
3690
|
replaceAriaLabel: "Reemplazar con",
|
|
3617
3691
|
replaceBtn: "Reemplazar",
|
|
3692
|
+
noResults: "Sin resultados",
|
|
3618
3693
|
replaceAllBtn: "Reemplazar todo",
|
|
3619
3694
|
close: "×"
|
|
3620
3695
|
},
|
|
@@ -3945,6 +4020,7 @@ var locales = {
|
|
|
3945
4020
|
replacePlaceholder: "바꿀 내용…",
|
|
3946
4021
|
replaceAriaLabel: "바꿀 내용",
|
|
3947
4022
|
replaceBtn: "바꾸기",
|
|
4023
|
+
noResults: "결과 없음",
|
|
3948
4024
|
replaceAllBtn: "모두 바꾸기",
|
|
3949
4025
|
close: "×"
|
|
3950
4026
|
},
|
|
@@ -4165,8 +4241,10 @@ var PROHIBITED_TAGS = [
|
|
|
4165
4241
|
"object",
|
|
4166
4242
|
"embed",
|
|
4167
4243
|
"form",
|
|
4168
|
-
"
|
|
4244
|
+
"base"
|
|
4169
4245
|
];
|
|
4246
|
+
/** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
|
|
4247
|
+
var UNWRAP_TAGS = new Set(["button"]);
|
|
4170
4248
|
/** Attributes whose values must be sanitised as URLs. */
|
|
4171
4249
|
var URL_ATTRS = [
|
|
4172
4250
|
"href",
|
|
@@ -4184,60 +4262,61 @@ var TRUSTED_IFRAME_HOSTS = new Set([
|
|
|
4184
4262
|
"player.vimeo.com"
|
|
4185
4263
|
]);
|
|
4186
4264
|
/**
|
|
4187
|
-
*
|
|
4188
|
-
* Uses DOMParser so the sanitisation follows normal browser parsing rules —
|
|
4189
|
-
* no regex shortcuts that can be bypassed by encoding tricks.
|
|
4265
|
+
* Produce a sanitized HTML string with dangerous elements and attributes removed.
|
|
4190
4266
|
*
|
|
4191
|
-
*
|
|
4192
|
-
*
|
|
4193
|
-
*
|
|
4194
|
-
* - Rejects javascript: and vbscript: URLs in URL attributes
|
|
4195
|
-
* - Rejects data: URIs everywhere except img[src] (base64 uploads)
|
|
4267
|
+
* Removes disallowed tags and wrappers, strips event-handler attributes, rejects
|
|
4268
|
+
* `javascript:`/`vbscript:` URLs and most `data:` URIs, restricts iframe `src`
|
|
4269
|
+
* to trusted hosts when enabled, and permits only checklist checkboxes as inputs.
|
|
4196
4270
|
*
|
|
4197
|
-
* @param {string} html
|
|
4198
|
-
* @
|
|
4271
|
+
* @param {string} html - HTML fragment to sanitize.
|
|
4272
|
+
* @param {Object} [options]
|
|
4273
|
+
* @param {boolean} [options.allowIframes=false] - If true, `iframe` elements are not removed but their `src` is restricted to trusted hosts and `srcdoc` is removed.
|
|
4274
|
+
* @returns {string} The sanitized HTML fragment.
|
|
4199
4275
|
*/
|
|
4200
4276
|
function sanitiseHTML(html, { allowIframes = false } = {}) {
|
|
4201
4277
|
const doc = new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html");
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4278
|
+
const allElements = Array.from(doc.querySelectorAll("*"));
|
|
4279
|
+
const prohibited = new Set(allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS);
|
|
4280
|
+
for (const el of allElements) {
|
|
4281
|
+
const tag = el.tagName.toLowerCase();
|
|
4282
|
+
if (UNWRAP_TAGS.has(tag)) {
|
|
4283
|
+
el.replaceWith(...el.childNodes);
|
|
4284
|
+
continue;
|
|
4285
|
+
}
|
|
4286
|
+
if (prohibited.has(tag)) {
|
|
4287
|
+
el.remove();
|
|
4288
|
+
continue;
|
|
4289
|
+
}
|
|
4290
|
+
for (const attr of Array.from(el.attributes)) {
|
|
4207
4291
|
if (attr.name.startsWith("on")) {
|
|
4208
4292
|
el.removeAttribute(attr.name);
|
|
4209
|
-
|
|
4293
|
+
continue;
|
|
4210
4294
|
}
|
|
4211
4295
|
if (URL_ATTRS.includes(attr.name)) {
|
|
4212
4296
|
const val = attr.value.trim();
|
|
4213
4297
|
if (/^(javascript|vbscript):/i.test(val)) {
|
|
4214
4298
|
el.removeAttribute(attr.name);
|
|
4215
|
-
|
|
4299
|
+
continue;
|
|
4216
4300
|
}
|
|
4217
4301
|
if (/^data:/i.test(val) && !(attr.name === "src" && el.tagName === "IMG")) el.removeAttribute(attr.name);
|
|
4218
4302
|
}
|
|
4219
4303
|
if (el.tagName === "IFRAME") {
|
|
4220
4304
|
if (attr.name === "srcdoc") {
|
|
4221
4305
|
el.removeAttribute(attr.name);
|
|
4222
|
-
|
|
4223
|
-
}
|
|
4224
|
-
if (attr.name === "src") {
|
|
4225
|
-
if (!isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
|
|
4226
|
-
return;
|
|
4306
|
+
continue;
|
|
4227
4307
|
}
|
|
4308
|
+
if (attr.name === "src" && !isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
|
|
4228
4309
|
}
|
|
4229
|
-
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
else Array.from(el.attributes).forEach((attr) => {
|
|
4234
|
-
if (![
|
|
4310
|
+
}
|
|
4311
|
+
if (tag === "input") {
|
|
4312
|
+
if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
|
|
4313
|
+
else for (const attr of Array.from(el.attributes)) if (![
|
|
4235
4314
|
"type",
|
|
4236
4315
|
"checked",
|
|
4237
4316
|
"contenteditable"
|
|
4238
4317
|
].includes(attr.name)) el.removeAttribute(attr.name);
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4241
4320
|
return doc.body.innerHTML;
|
|
4242
4321
|
}
|
|
4243
4322
|
/**
|
|
@@ -4438,7 +4517,18 @@ var History = class {
|
|
|
4438
4517
|
const sel = window.getSelection();
|
|
4439
4518
|
sel.removeAllRanges();
|
|
4440
4519
|
sel.addRange(range);
|
|
4441
|
-
} catch (_) {
|
|
4520
|
+
} catch (_) {
|
|
4521
|
+
try {
|
|
4522
|
+
const fb = document.createRange();
|
|
4523
|
+
fb.setStart(this.editable, 0);
|
|
4524
|
+
fb.collapse(true);
|
|
4525
|
+
const s = window.getSelection();
|
|
4526
|
+
if (s) {
|
|
4527
|
+
s.removeAllRanges();
|
|
4528
|
+
s.addRange(fb);
|
|
4529
|
+
}
|
|
4530
|
+
} catch (_2) {}
|
|
4531
|
+
}
|
|
4442
4532
|
}
|
|
4443
4533
|
_savePoint() {
|
|
4444
4534
|
if (this.stackOffset < this.stack.length - 1) this.stack = this.stack.slice(0, this.stackOffset + 1);
|
|
@@ -4465,6 +4555,10 @@ var History = class {
|
|
|
4465
4555
|
* @returns {{ html: string, images: Object<string,string> }}
|
|
4466
4556
|
*/
|
|
4467
4557
|
_tokenizeImages(html) {
|
|
4558
|
+
if (!html.includes("data:")) return {
|
|
4559
|
+
html,
|
|
4560
|
+
images: {}
|
|
4561
|
+
};
|
|
4468
4562
|
const images = {};
|
|
4469
4563
|
let index = 0;
|
|
4470
4564
|
return {
|
|
@@ -4536,11 +4630,12 @@ var History = class {
|
|
|
4536
4630
|
* Inspired by Summernote's table handling
|
|
4537
4631
|
*/
|
|
4538
4632
|
/**
|
|
4539
|
-
*
|
|
4540
|
-
* @param {number} cols
|
|
4541
|
-
* @param {number} rows
|
|
4542
|
-
* @param {{ headerRow?: boolean }} [opts]
|
|
4543
|
-
* @
|
|
4633
|
+
* Build an HTML table with the given number of columns and rows, optionally including a header row.
|
|
4634
|
+
* @param {number} cols - Number of columns in each row.
|
|
4635
|
+
* @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
|
|
4636
|
+
* @param {{ headerRow?: boolean }} [opts] - Options object.
|
|
4637
|
+
* @param {boolean} [opts.headerRow=false] - When true and `rows > 0`, creates a header row (`<thead>`) plus body rows for the remainder.
|
|
4638
|
+
* @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
|
|
4544
4639
|
*/
|
|
4545
4640
|
function createTable(cols, rows, opts = {}) {
|
|
4546
4641
|
const { headerRow = false } = opts;
|
|
@@ -4549,7 +4644,7 @@ function createTable(cols, rows, opts = {}) {
|
|
|
4549
4644
|
const thead = createElement("thead");
|
|
4550
4645
|
const tr = createElement("tr");
|
|
4551
4646
|
for (let c = 0; c < cols; c++) {
|
|
4552
|
-
const th = createElement("th", {}, ["
|
|
4647
|
+
const th = createElement("th", {}, [document.createElement("br")]);
|
|
4553
4648
|
tr.appendChild(th);
|
|
4554
4649
|
}
|
|
4555
4650
|
thead.appendChild(tr);
|
|
@@ -4561,7 +4656,7 @@ function createTable(cols, rows, opts = {}) {
|
|
|
4561
4656
|
for (let r = 0; r < bodyRows; r++) {
|
|
4562
4657
|
const tr = createElement("tr");
|
|
4563
4658
|
for (let c = 0; c < cols; c++) {
|
|
4564
|
-
const td = createElement("td", {}, ["
|
|
4659
|
+
const td = createElement("td", {}, [document.createElement("br")]);
|
|
4565
4660
|
tr.appendChild(td);
|
|
4566
4661
|
}
|
|
4567
4662
|
tbody.appendChild(tr);
|
|
@@ -4569,13 +4664,52 @@ function createTable(cols, rows, opts = {}) {
|
|
|
4569
4664
|
return table;
|
|
4570
4665
|
}
|
|
4571
4666
|
/**
|
|
4572
|
-
*
|
|
4573
|
-
* @param {number} cols
|
|
4574
|
-
* @param {number} rows
|
|
4575
|
-
* @param {{ headerRow?: boolean }} [opts]
|
|
4667
|
+
* Insert a table at the current selection and place the caret into its first cell.
|
|
4668
|
+
* @param {number} cols - Number of columns for the new table.
|
|
4669
|
+
* @param {number} rows - Number of rows for the new table.
|
|
4670
|
+
* @param {{ headerRow?: boolean }} [opts] - Options for table creation.
|
|
4671
|
+
* @param {boolean} [opts.headerRow=false] - If true, include a header row as the first row.
|
|
4576
4672
|
*/
|
|
4577
4673
|
function insertTable(cols, rows, opts = {}) {
|
|
4578
|
-
|
|
4674
|
+
if (cols <= 0 || rows <= 0) return;
|
|
4675
|
+
const table = createTable(cols, rows, opts);
|
|
4676
|
+
const sel = window.getSelection();
|
|
4677
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
4678
|
+
const range = sel.getRangeAt(0);
|
|
4679
|
+
range.deleteContents();
|
|
4680
|
+
const BLOCK = new Set([
|
|
4681
|
+
"P",
|
|
4682
|
+
"DIV",
|
|
4683
|
+
"H1",
|
|
4684
|
+
"H2",
|
|
4685
|
+
"H3",
|
|
4686
|
+
"H4",
|
|
4687
|
+
"H5",
|
|
4688
|
+
"H6",
|
|
4689
|
+
"BLOCKQUOTE",
|
|
4690
|
+
"LI",
|
|
4691
|
+
"PRE"
|
|
4692
|
+
]);
|
|
4693
|
+
let anchor = range.startContainer;
|
|
4694
|
+
if (anchor.nodeType === 3) anchor = anchor.parentElement;
|
|
4695
|
+
while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) anchor = anchor.parentElement;
|
|
4696
|
+
if (anchor && BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentNode) {
|
|
4697
|
+
anchor.after(table);
|
|
4698
|
+
if (!table.nextElementSibling) {
|
|
4699
|
+
const p = document.createElement("p");
|
|
4700
|
+
p.appendChild(document.createElement("br"));
|
|
4701
|
+
table.after(p);
|
|
4702
|
+
}
|
|
4703
|
+
if (!anchor.textContent.trim() && !anchor.querySelector("img, video, table")) anchor.remove();
|
|
4704
|
+
} else range.insertNode(table);
|
|
4705
|
+
const firstCell = table.querySelector("td, th");
|
|
4706
|
+
if (firstCell) {
|
|
4707
|
+
const nr = document.createRange();
|
|
4708
|
+
nr.setStart(firstCell, 0);
|
|
4709
|
+
nr.collapse(true);
|
|
4710
|
+
sel.removeAllRanges();
|
|
4711
|
+
sel.addRange(nr);
|
|
4712
|
+
}
|
|
4579
4713
|
}
|
|
4580
4714
|
//#endregion
|
|
4581
4715
|
//#region src/js/core/key.js
|
|
@@ -4790,7 +4924,7 @@ function handleKeydown(event, editable, options = {}) {
|
|
|
4790
4924
|
if (para && para.nodeName.toUpperCase() === "PRE") {
|
|
4791
4925
|
if (event.shiftKey) return false;
|
|
4792
4926
|
event.preventDefault();
|
|
4793
|
-
execCommand("insertText", "
|
|
4927
|
+
execCommand("insertText", " ".repeat(options.tabSize || 4));
|
|
4794
4928
|
return true;
|
|
4795
4929
|
}
|
|
4796
4930
|
if (options.tabSize) {
|
|
@@ -4883,6 +5017,11 @@ function handleKeydown(event, editable, options = {}) {
|
|
|
4883
5017
|
return true;
|
|
4884
5018
|
}
|
|
4885
5019
|
const para = closestPara(range.sc, editable);
|
|
5020
|
+
if (para && para.nodeName.toUpperCase() === "PRE") {
|
|
5021
|
+
event.preventDefault();
|
|
5022
|
+
execCommand("insertText", "\n");
|
|
5023
|
+
return true;
|
|
5024
|
+
}
|
|
4886
5025
|
if (para && para.nodeName.toUpperCase() === "BLOCKQUOTE") {
|
|
4887
5026
|
const native = range.toNativeRange();
|
|
4888
5027
|
native.setEnd(para, para.childNodes.length);
|
|
@@ -4916,11 +5055,22 @@ function handleKeydown(event, editable, options = {}) {
|
|
|
4916
5055
|
function htmlToMarkdown(html) {
|
|
4917
5056
|
return _domToMd(new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html").body).replace(/\n{3,}/g, "\n\n").trim();
|
|
4918
5057
|
}
|
|
4919
|
-
|
|
5058
|
+
/**
|
|
5059
|
+
* Convert a DOM node subtree into Markdown.
|
|
5060
|
+
*
|
|
5061
|
+
* Recursively produces a Markdown string representing the given DOM node and its descendants,
|
|
5062
|
+
* handling common HTML constructs such as paragraphs, headings, lists (with nested indentation),
|
|
5063
|
+
* blockquotes, fenced and inline code, links, images, tables, horizontal rules, and basic inline emphasis.
|
|
5064
|
+
*
|
|
5065
|
+
* @param {Node} node - The DOM node to convert.
|
|
5066
|
+
* @param {number} [depth=0] - Current nesting depth used to indent nested list items.
|
|
5067
|
+
* @returns {string} The Markdown representation of the node subtree.
|
|
5068
|
+
*/
|
|
5069
|
+
function _domToMd(node, depth = 0) {
|
|
4920
5070
|
if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
|
|
4921
5071
|
if (node.nodeType !== 1) return "";
|
|
4922
5072
|
const tag = node.nodeName.toLowerCase();
|
|
4923
|
-
const inner = () => Array.from(node.childNodes).map(_domToMd).join("");
|
|
5073
|
+
const inner = () => Array.from(node.childNodes).map((n) => _domToMd(n, depth)).join("");
|
|
4924
5074
|
switch (tag) {
|
|
4925
5075
|
case "p":
|
|
4926
5076
|
case "div": return `\n\n${inner()}\n\n`;
|
|
@@ -4960,12 +5110,16 @@ function _domToMd(node) {
|
|
|
4960
5110
|
case "ul": {
|
|
4961
5111
|
const items = Array.from(node.querySelectorAll(":scope > li"));
|
|
4962
5112
|
if (!items.length) return inner();
|
|
4963
|
-
|
|
5113
|
+
const indent = " ".repeat(depth);
|
|
5114
|
+
const lines = items.map((li) => `${indent}- ${_domToMd(li, depth + 1).trim()}`).join("\n");
|
|
5115
|
+
return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
|
|
4964
5116
|
}
|
|
4965
5117
|
case "ol": {
|
|
4966
5118
|
const items = Array.from(node.querySelectorAll(":scope > li"));
|
|
4967
5119
|
if (!items.length) return inner();
|
|
4968
|
-
|
|
5120
|
+
const indent = " ".repeat(depth);
|
|
5121
|
+
const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
|
|
5122
|
+
return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
|
|
4969
5123
|
}
|
|
4970
5124
|
case "li": return inner();
|
|
4971
5125
|
case "hr": return "\n\n---\n\n";
|
|
@@ -4989,12 +5143,15 @@ function _domToMd(node) {
|
|
|
4989
5143
|
}
|
|
4990
5144
|
}
|
|
4991
5145
|
/**
|
|
4992
|
-
*
|
|
4993
|
-
*
|
|
4994
|
-
*
|
|
5146
|
+
* Detects whether a string likely contains Markdown syntax.
|
|
5147
|
+
*
|
|
5148
|
+
* Checks for common Markdown constructs such as ATX headings, unordered or
|
|
5149
|
+
* ordered list items, blockquotes, fenced code blocks, and bold emphasis.
|
|
5150
|
+
* @param {string} text - Input text to inspect for Markdown patterns.
|
|
5151
|
+
* @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
|
|
4995
5152
|
*/
|
|
4996
5153
|
function isMarkdown(text) {
|
|
4997
|
-
return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S
|
|
5154
|
+
return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|^```|^\*{2}.+?\*{2}/m.test(text);
|
|
4998
5155
|
}
|
|
4999
5156
|
/**
|
|
5000
5157
|
* Converts a Markdown string to an HTML string.
|
|
@@ -5668,6 +5825,8 @@ var Editor = class {
|
|
|
5668
5825
|
* Toolbar.js - Builds and manages the editor toolbar UI
|
|
5669
5826
|
* Inspired by Summernote's Toolbar module — rewritten without jQuery
|
|
5670
5827
|
*/
|
|
5828
|
+
/** Resolve a toolbar item: string → registry lookup, object → pass-through. */
|
|
5829
|
+
var _resolveBtn = (item) => typeof item === "string" ? getButton(item) : item;
|
|
5671
5830
|
var _faPageLevelReady = null;
|
|
5672
5831
|
var _S$1 = "stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
|
|
5673
5832
|
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>`;
|
|
@@ -5757,15 +5916,19 @@ var Toolbar = class {
|
|
|
5757
5916
|
this._disposers = [];
|
|
5758
5917
|
/** @type {Array<() => void>} closers for all open color picker popups */
|
|
5759
5918
|
this._colorPickerClosers = [];
|
|
5919
|
+
/** @type {number|null} rAF handle for debounced refresh */
|
|
5920
|
+
this._refreshRaf = null;
|
|
5760
5921
|
}
|
|
5761
5922
|
initialize() {
|
|
5762
5923
|
this.el = createElement("div", { class: "an-toolbar" });
|
|
5763
5924
|
this._faReady = this._detectFontAwesome();
|
|
5764
5925
|
this._buildButtons();
|
|
5765
|
-
this._btnMap = new Map((this.options.toolbar || []).flat().map((b) => [b.name, b]));
|
|
5926
|
+
this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
|
|
5766
5927
|
return this;
|
|
5767
5928
|
}
|
|
5768
5929
|
destroy() {
|
|
5930
|
+
if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
|
|
5931
|
+
this._refreshRaf = null;
|
|
5769
5932
|
this._disposers.forEach((d) => d());
|
|
5770
5933
|
this._disposers = [];
|
|
5771
5934
|
if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
|
|
@@ -5776,7 +5939,12 @@ var Toolbar = class {
|
|
|
5776
5939
|
const fragment = document.createDocumentFragment();
|
|
5777
5940
|
toolbar.forEach((group) => {
|
|
5778
5941
|
const groupEl = createElement("div", { class: "an-btn-group" });
|
|
5779
|
-
group.forEach((
|
|
5942
|
+
group.forEach((item) => {
|
|
5943
|
+
const btnDef = _resolveBtn(item);
|
|
5944
|
+
if (!btnDef) {
|
|
5945
|
+
console.warn(`[AutumnNote] Toolbar: button "${item}" not found in registry. Skipped.`);
|
|
5946
|
+
return;
|
|
5947
|
+
}
|
|
5780
5948
|
let el;
|
|
5781
5949
|
if (btnDef.type === "select") el = this._createSelect(btnDef);
|
|
5782
5950
|
else if (btnDef.type === "grid") el = this._createGridPicker(btnDef);
|
|
@@ -6161,6 +6329,13 @@ var Toolbar = class {
|
|
|
6161
6329
|
return _faPageLevelReady;
|
|
6162
6330
|
}
|
|
6163
6331
|
refresh() {
|
|
6332
|
+
if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
|
|
6333
|
+
this._refreshRaf = requestAnimationFrame(() => {
|
|
6334
|
+
this._refreshRaf = null;
|
|
6335
|
+
this._doRefresh();
|
|
6336
|
+
});
|
|
6337
|
+
}
|
|
6338
|
+
_doRefresh() {
|
|
6164
6339
|
if (!this.el) return;
|
|
6165
6340
|
const btnMap = this._btnMap || /* @__PURE__ */ new Map();
|
|
6166
6341
|
this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
|
|
@@ -6189,6 +6364,24 @@ var Toolbar = class {
|
|
|
6189
6364
|
hide() {
|
|
6190
6365
|
if (this.el) this.el.style.display = "none";
|
|
6191
6366
|
}
|
|
6367
|
+
/**
|
|
6368
|
+
* Tears down and re-renders the toolbar in-place.
|
|
6369
|
+
* Call after registering new buttons post-create via context.use(plugin)
|
|
6370
|
+
* or AutumnNote.registerButton() to make them appear in the toolbar.
|
|
6371
|
+
*/
|
|
6372
|
+
rebuild() {
|
|
6373
|
+
if (this._refreshRaf) {
|
|
6374
|
+
cancelAnimationFrame(this._refreshRaf);
|
|
6375
|
+
this._refreshRaf = null;
|
|
6376
|
+
}
|
|
6377
|
+
this._disposers.forEach((d) => d());
|
|
6378
|
+
this._disposers = [];
|
|
6379
|
+
if (this.el) this.el.innerHTML = "";
|
|
6380
|
+
this._faReady = this._detectFontAwesome();
|
|
6381
|
+
this._buildButtons();
|
|
6382
|
+
this._btnMap = new Map((this.options.toolbar || []).flat().map(_resolveBtn).filter(Boolean).map((b) => [b.name, b]));
|
|
6383
|
+
this.refresh();
|
|
6384
|
+
}
|
|
6192
6385
|
};
|
|
6193
6386
|
//#endregion
|
|
6194
6387
|
//#region src/js/module/Statusbar.js
|
|
@@ -6338,7 +6531,7 @@ var Statusbar = class {
|
|
|
6338
6531
|
}
|
|
6339
6532
|
update() {
|
|
6340
6533
|
if (!this._wordCountEl || !this._charCountEl) return;
|
|
6341
|
-
const text = this.context.layoutInfo.editable.
|
|
6534
|
+
const text = this.context.layoutInfo.editable.textContent || "";
|
|
6342
6535
|
const words = _countWords(text);
|
|
6343
6536
|
const chars = text.replace(/\n/g, "").length;
|
|
6344
6537
|
const maxWords = this.options.maxWords || 0;
|
|
@@ -6533,7 +6726,7 @@ var Clipboard = class {
|
|
|
6533
6726
|
event.preventDefault();
|
|
6534
6727
|
const raw = clipboardData.getData("text/html");
|
|
6535
6728
|
const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
|
|
6536
|
-
const isSocialContent =
|
|
6729
|
+
const isSocialContent = /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
|
|
6537
6730
|
let html = raw;
|
|
6538
6731
|
if (isWordContent) html = this._cleanWordHtml(html);
|
|
6539
6732
|
else if (isSocialContent) html = this._cleanSocialHtml(html);
|
|
@@ -6635,7 +6828,7 @@ var Clipboard = class {
|
|
|
6635
6828
|
*/
|
|
6636
6829
|
_dataUrlToBlob(dataUrl) {
|
|
6637
6830
|
const [header, b64] = dataUrl.split(",");
|
|
6638
|
-
const mime = header.match(/:(.*?);/)[1];
|
|
6831
|
+
const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
|
|
6639
6832
|
const binary = atob(b64);
|
|
6640
6833
|
const arr = new Uint8Array(binary.length);
|
|
6641
6834
|
for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
|
|
@@ -7641,6 +7834,7 @@ var ImageResizer = class {
|
|
|
7641
7834
|
_select(img) {
|
|
7642
7835
|
if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
|
|
7643
7836
|
this._activeImg = img;
|
|
7837
|
+
this._lastOverlayPos = null;
|
|
7644
7838
|
img.classList.add("an-image-selected");
|
|
7645
7839
|
this._updateOverlayPosition();
|
|
7646
7840
|
this._overlay.style.display = "block";
|
|
@@ -7664,6 +7858,14 @@ var ImageResizer = class {
|
|
|
7664
7858
|
const offsetParent = this._overlay.offsetParent || this._container;
|
|
7665
7859
|
const containerRect = offsetParent.getBoundingClientRect();
|
|
7666
7860
|
const rect = this._activeImg.getBoundingClientRect();
|
|
7861
|
+
const p = this._lastOverlayPos;
|
|
7862
|
+
if (p && p.l === rect.left && p.t === rect.top && p.w === rect.width && p.h === rect.height) return;
|
|
7863
|
+
this._lastOverlayPos = {
|
|
7864
|
+
l: rect.left,
|
|
7865
|
+
t: rect.top,
|
|
7866
|
+
w: rect.width,
|
|
7867
|
+
h: rect.height
|
|
7868
|
+
};
|
|
7667
7869
|
const left = rect.left - containerRect.left + offsetParent.scrollLeft;
|
|
7668
7870
|
const top = rect.top - containerRect.top + offsetParent.scrollTop;
|
|
7669
7871
|
this._overlay.style.left = `${left}px`;
|
|
@@ -13362,7 +13564,7 @@ var ContextMenu = class {
|
|
|
13362
13564
|
cells.forEach((cell) => {
|
|
13363
13565
|
cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
|
|
13364
13566
|
});
|
|
13365
|
-
labelEl.textContent = rows && cols ? `${
|
|
13567
|
+
labelEl.textContent = rows && cols ? `${rows} × ${cols}` : this.context.locale.contextMenu.table || "Insert Table";
|
|
13366
13568
|
};
|
|
13367
13569
|
panel.appendChild(gridEl);
|
|
13368
13570
|
panel.appendChild(labelEl);
|
|
@@ -14002,7 +14204,6 @@ var FindReplace = class {
|
|
|
14002
14204
|
if (!editable) return;
|
|
14003
14205
|
const rawMatches = this._findRawMatches(query, editable);
|
|
14004
14206
|
if (rawMatches.length === 0) return;
|
|
14005
|
-
this._currentIndex = 0;
|
|
14006
14207
|
for (let i = rawMatches.length - 1; i >= 0; i--) {
|
|
14007
14208
|
const { node, start, end } = rawMatches[i];
|
|
14008
14209
|
try {
|
|
@@ -14019,6 +14220,7 @@ var FindReplace = class {
|
|
|
14019
14220
|
}
|
|
14020
14221
|
this._matches.reverse();
|
|
14021
14222
|
this._matches = this._matches.filter((m) => m.mark);
|
|
14223
|
+
this._currentIndex = 0;
|
|
14022
14224
|
if (this._matches.length === 0) return;
|
|
14023
14225
|
if (this._matches[0] && this._matches[0].mark) {
|
|
14024
14226
|
this._matches[0].mark.className = "an-highlight an-highlight-current";
|
|
@@ -14044,12 +14246,13 @@ var FindReplace = class {
|
|
|
14044
14246
|
this._lastCaseSensitive = this._caseSensitive;
|
|
14045
14247
|
}
|
|
14046
14248
|
const re = this._queryRegex;
|
|
14249
|
+
const MAX_RESULTS = 500;
|
|
14047
14250
|
const walker = document.createTreeWalker(root, 4);
|
|
14048
14251
|
let node;
|
|
14049
|
-
while (node = walker.nextNode()) {
|
|
14252
|
+
while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
|
|
14050
14253
|
re.lastIndex = 0;
|
|
14051
14254
|
let m;
|
|
14052
|
-
while ((m = re.exec(node.textContent)) !== null) results.push({
|
|
14255
|
+
while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
|
|
14053
14256
|
node,
|
|
14054
14257
|
start: m.index,
|
|
14055
14258
|
end: m.index + m[0].length
|
|
@@ -14136,7 +14339,7 @@ var FindReplace = class {
|
|
|
14136
14339
|
const total = this._matches.length;
|
|
14137
14340
|
if (total === 0) {
|
|
14138
14341
|
const query = this._findInput ? this._findInput.value : "";
|
|
14139
|
-
this._counterEl.textContent = query ?
|
|
14342
|
+
this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
|
|
14140
14343
|
} else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
|
|
14141
14344
|
}
|
|
14142
14345
|
};
|
|
@@ -14335,17 +14538,33 @@ var ImageCropOverlay = class {
|
|
|
14335
14538
|
e.preventDefault();
|
|
14336
14539
|
e.stopPropagation();
|
|
14337
14540
|
this._startHandleDrag(e, id);
|
|
14338
|
-
}))
|
|
14541
|
+
}), on(h, "touchstart", (e) => {
|
|
14542
|
+
e.preventDefault();
|
|
14543
|
+
e.stopPropagation();
|
|
14544
|
+
this._startHandleDrag({
|
|
14545
|
+
clientX: e.touches[0].clientX,
|
|
14546
|
+
clientY: e.touches[0].clientY
|
|
14547
|
+
}, id);
|
|
14548
|
+
}, { passive: false }));
|
|
14339
14549
|
this._handles[id] = h;
|
|
14340
14550
|
cropBox.appendChild(h);
|
|
14341
14551
|
});
|
|
14342
14552
|
this._disposers.push(on(cropBox, "mousedown", (e) => {
|
|
14343
|
-
if (e.target
|
|
14344
|
-
if (e.target
|
|
14553
|
+
if (e.target.classList.contains("an-crop-handle")) return;
|
|
14554
|
+
if (e.target !== cropBox && e.target !== grid) return;
|
|
14345
14555
|
e.preventDefault();
|
|
14346
14556
|
e.stopPropagation();
|
|
14347
14557
|
this._startBoxMove(e);
|
|
14348
|
-
}))
|
|
14558
|
+
}), on(cropBox, "touchstart", (e) => {
|
|
14559
|
+
if (e.target.classList.contains("an-crop-handle")) return;
|
|
14560
|
+
if (e.target !== cropBox && e.target !== grid) return;
|
|
14561
|
+
e.preventDefault();
|
|
14562
|
+
e.stopPropagation();
|
|
14563
|
+
this._startBoxMove({
|
|
14564
|
+
clientX: e.touches[0].clientX,
|
|
14565
|
+
clientY: e.touches[0].clientY
|
|
14566
|
+
});
|
|
14567
|
+
}, { passive: false }));
|
|
14349
14568
|
const infoEl = document.createElement("div");
|
|
14350
14569
|
infoEl.className = "an-crop-info";
|
|
14351
14570
|
infoEl.style.cssText = `
|
|
@@ -14503,15 +14722,26 @@ var ImageCropOverlay = class {
|
|
|
14503
14722
|
* @param {(e: MouseEvent) => void} onMove
|
|
14504
14723
|
*/
|
|
14505
14724
|
_attachDocDrag(onMove) {
|
|
14725
|
+
const onTouchMove = (e) => {
|
|
14726
|
+
e.preventDefault();
|
|
14727
|
+
onMove({
|
|
14728
|
+
clientX: e.touches[0].clientX,
|
|
14729
|
+
clientY: e.touches[0].clientY
|
|
14730
|
+
});
|
|
14731
|
+
};
|
|
14506
14732
|
const cleanup = () => {
|
|
14507
14733
|
document.removeEventListener("mousemove", onMove);
|
|
14508
14734
|
document.removeEventListener("mouseup", cleanup);
|
|
14735
|
+
document.removeEventListener("touchmove", onTouchMove);
|
|
14736
|
+
document.removeEventListener("touchend", cleanup);
|
|
14509
14737
|
document.body.style.userSelect = "";
|
|
14510
14738
|
document.body.style.cursor = "";
|
|
14511
14739
|
};
|
|
14512
14740
|
document.body.style.userSelect = "none";
|
|
14513
14741
|
document.addEventListener("mousemove", onMove);
|
|
14514
14742
|
document.addEventListener("mouseup", cleanup, { once: true });
|
|
14743
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
14744
|
+
document.addEventListener("touchend", cleanup, { once: true });
|
|
14515
14745
|
}
|
|
14516
14746
|
_bindEsc() {
|
|
14517
14747
|
const handler = (e) => {
|
|
@@ -14552,7 +14782,7 @@ var ImageCropOverlay = class {
|
|
|
14552
14782
|
height: natH
|
|
14553
14783
|
}, w, h);
|
|
14554
14784
|
if (!canvas) {
|
|
14555
|
-
|
|
14785
|
+
this._showCropError("Cannot crop this image: the image server does not allow cross-origin access. Upload the image directly to use the crop tool.");
|
|
14556
14786
|
this._close(false);
|
|
14557
14787
|
return;
|
|
14558
14788
|
}
|
|
@@ -14569,6 +14799,37 @@ var ImageCropOverlay = class {
|
|
|
14569
14799
|
this.context.invoke("imageResizer.updateOverlay");
|
|
14570
14800
|
}
|
|
14571
14801
|
/**
|
|
14802
|
+
* Show a non-blocking inline error banner appended to document.body.
|
|
14803
|
+
* Auto-dismisses after 4 seconds.
|
|
14804
|
+
* @param {string} msg
|
|
14805
|
+
*/
|
|
14806
|
+
_showCropError(msg) {
|
|
14807
|
+
const banner = document.createElement("div");
|
|
14808
|
+
banner.setAttribute("role", "alert");
|
|
14809
|
+
banner.style.cssText = [
|
|
14810
|
+
"position:fixed",
|
|
14811
|
+
"bottom:24px",
|
|
14812
|
+
"left:50%",
|
|
14813
|
+
"transform:translateX(-50%)",
|
|
14814
|
+
"z-index:10200",
|
|
14815
|
+
"max-width:420px",
|
|
14816
|
+
"width:max-content",
|
|
14817
|
+
"background:#7f1d1d",
|
|
14818
|
+
"color:#fecaca",
|
|
14819
|
+
"border:1px solid #b91c1c",
|
|
14820
|
+
"border-radius:8px",
|
|
14821
|
+
"padding:12px 18px",
|
|
14822
|
+
"font:13px/1.5 system-ui,sans-serif",
|
|
14823
|
+
"box-shadow:0 4px 16px rgba(0,0,0,.4)",
|
|
14824
|
+
"pointer-events:auto"
|
|
14825
|
+
].join(";");
|
|
14826
|
+
banner.textContent = msg;
|
|
14827
|
+
document.body.appendChild(banner);
|
|
14828
|
+
setTimeout(() => {
|
|
14829
|
+
if (banner.parentNode) banner.parentNode.removeChild(banner);
|
|
14830
|
+
}, 4e3);
|
|
14831
|
+
}
|
|
14832
|
+
/**
|
|
14572
14833
|
* Remove all overlay DOM elements and reset state.
|
|
14573
14834
|
* @param {boolean} _committed - reserved for future use
|
|
14574
14835
|
*/
|
|
@@ -14983,6 +15244,16 @@ var _ACTIONS = {
|
|
|
14983
15244
|
if (!editable) return;
|
|
14984
15245
|
editable.focus();
|
|
14985
15246
|
document.execCommand("removeFormat");
|
|
15247
|
+
const sel = window.getSelection();
|
|
15248
|
+
if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
|
|
15249
|
+
const range = sel.getRangeAt(0);
|
|
15250
|
+
const ancestor = range.commonAncestorContainer;
|
|
15251
|
+
const root = ancestor.nodeType === 1 ? ancestor : ancestor.parentElement;
|
|
15252
|
+
if (root) {
|
|
15253
|
+
const candidates = [root, ...root.querySelectorAll("[style]")];
|
|
15254
|
+
for (const el of candidates) if (el.hasAttribute("style") && range.intersectsNode(el)) el.removeAttribute("style");
|
|
15255
|
+
}
|
|
15256
|
+
}
|
|
14986
15257
|
ctx.invoke("editor.afterCommand");
|
|
14987
15258
|
},
|
|
14988
15259
|
inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
|
|
@@ -15096,6 +15367,7 @@ var BubbleToolbar = class {
|
|
|
15096
15367
|
}
|
|
15097
15368
|
document.body.appendChild(el);
|
|
15098
15369
|
this._el = el;
|
|
15370
|
+
this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
|
|
15099
15371
|
}
|
|
15100
15372
|
_buildColorPicker() {
|
|
15101
15373
|
const picker = document.createElement("div");
|
|
@@ -15181,8 +15453,13 @@ var BubbleToolbar = class {
|
|
|
15181
15453
|
editable.focus();
|
|
15182
15454
|
const sel = window.getSelection();
|
|
15183
15455
|
sel.removeAllRanges();
|
|
15184
|
-
|
|
15185
|
-
|
|
15456
|
+
try {
|
|
15457
|
+
sel.addRange(this._savedRange.cloneRange());
|
|
15458
|
+
} catch (_) {
|
|
15459
|
+
return;
|
|
15460
|
+
}
|
|
15461
|
+
const cmd = type === "hiliteColor" ? "hiliteColor" : type;
|
|
15462
|
+
if (!document.execCommand(cmd, false, color) && cmd === "hiliteColor") document.execCommand("backColor", false, color);
|
|
15186
15463
|
this.context.invoke("editor.afterCommand");
|
|
15187
15464
|
const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
|
|
15188
15465
|
const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
|
|
@@ -15217,8 +15494,8 @@ var BubbleToolbar = class {
|
|
|
15217
15494
|
this._closeColorPicker();
|
|
15218
15495
|
}
|
|
15219
15496
|
_syncActive() {
|
|
15220
|
-
if (!this.
|
|
15221
|
-
this.
|
|
15497
|
+
if (!this._btnCache) return;
|
|
15498
|
+
this._btnCache.forEach((btn) => {
|
|
15222
15499
|
const activeFn = _ACTIVE[btn.dataset.name];
|
|
15223
15500
|
btn.classList.toggle("an-active", !!(activeFn && activeFn()));
|
|
15224
15501
|
});
|
|
@@ -15365,14 +15642,23 @@ var Mention = class {
|
|
|
15365
15642
|
const el = document.createElement("div");
|
|
15366
15643
|
el.className = "an-mention-dropdown";
|
|
15367
15644
|
el.setAttribute("role", "listbox");
|
|
15645
|
+
el.addEventListener("mousedown", (e) => e.preventDefault());
|
|
15646
|
+
el.addEventListener("click", (e) => {
|
|
15647
|
+
const item = e.target.closest(".an-mention-item");
|
|
15648
|
+
if (item) this._select(+item.dataset.index);
|
|
15649
|
+
});
|
|
15650
|
+
el.addEventListener("mousemove", (e) => {
|
|
15651
|
+
const item = e.target.closest(".an-mention-item");
|
|
15652
|
+
if (item) this._highlightItem(+item.dataset.index);
|
|
15653
|
+
});
|
|
15368
15654
|
document.body.appendChild(el);
|
|
15369
15655
|
this._dropdown = el;
|
|
15370
15656
|
}
|
|
15371
15657
|
_renderItems(items) {
|
|
15372
15658
|
const dd = this._dropdown;
|
|
15373
|
-
dd.innerHTML = "";
|
|
15374
15659
|
this._items = items.slice(0, this._cfg.maxResults);
|
|
15375
15660
|
this._activeIndex = this._items.length > 0 ? 0 : -1;
|
|
15661
|
+
const frag = document.createDocumentFragment();
|
|
15376
15662
|
this._items.forEach((item, i) => {
|
|
15377
15663
|
const li = document.createElement("div");
|
|
15378
15664
|
li.className = "an-mention-item";
|
|
@@ -15388,10 +15674,10 @@ var Mention = class {
|
|
|
15388
15674
|
const label = document.createElement("span");
|
|
15389
15675
|
label.textContent = item.label;
|
|
15390
15676
|
li.appendChild(label);
|
|
15391
|
-
|
|
15392
|
-
li.addEventListener("click", () => this._select(i));
|
|
15393
|
-
dd.appendChild(li);
|
|
15677
|
+
frag.appendChild(li);
|
|
15394
15678
|
});
|
|
15679
|
+
dd.innerHTML = "";
|
|
15680
|
+
dd.appendChild(frag);
|
|
15395
15681
|
this._highlightItem(this._activeIndex);
|
|
15396
15682
|
}
|
|
15397
15683
|
_highlightItem(index) {
|
|
@@ -15543,6 +15829,8 @@ var Mention = class {
|
|
|
15543
15829
|
*/
|
|
15544
15830
|
/** Module registry shared across all Context instances (populated via AutumnNote.registerModule). */
|
|
15545
15831
|
var _customModules = /* @__PURE__ */ new Map();
|
|
15832
|
+
/** Global plugin registry (populated via AutumnNote.use()). Applied to every new Context. */
|
|
15833
|
+
var _globalPlugins = /* @__PURE__ */ new Map();
|
|
15546
15834
|
var Context = class {
|
|
15547
15835
|
/**
|
|
15548
15836
|
* @param {HTMLElement} targetEl - The element to replace with the editor
|
|
@@ -15559,6 +15847,8 @@ var Context = class {
|
|
|
15559
15847
|
this._listeners = /* @__PURE__ */ new Map();
|
|
15560
15848
|
/** @type {Map<string, object>} */
|
|
15561
15849
|
this._modules = /* @__PURE__ */ new Map();
|
|
15850
|
+
/** @type {Map<string, { plugin: object, publicApi: * }>} */
|
|
15851
|
+
this._plugins = /* @__PURE__ */ new Map();
|
|
15562
15852
|
this._disposers = [];
|
|
15563
15853
|
this._alive = false;
|
|
15564
15854
|
}
|
|
@@ -15581,6 +15871,7 @@ var Context = class {
|
|
|
15581
15871
|
if (this.options.focus) editable.focus();
|
|
15582
15872
|
this._alive = true;
|
|
15583
15873
|
this.invoke("toolbar.refresh");
|
|
15874
|
+
this._applyGlobalPlugins();
|
|
15584
15875
|
if (typeof this.options.onInit === "function") this.options.onInit(this);
|
|
15585
15876
|
return this;
|
|
15586
15877
|
}
|
|
@@ -15632,6 +15923,46 @@ var Context = class {
|
|
|
15632
15923
|
this._modules.set(name, instance);
|
|
15633
15924
|
return this;
|
|
15634
15925
|
}
|
|
15926
|
+
/**
|
|
15927
|
+
* Installs a plugin on this editor instance.
|
|
15928
|
+
* If called after create(), buttons are registered immediately but the toolbar
|
|
15929
|
+
* must be rebuilt via ctx.invoke('toolbar.rebuild') to render new buttons.
|
|
15930
|
+
* @param {object} plugin - { name, version?, buttons?, install?, uninstall? }
|
|
15931
|
+
* @param {object} [options] - Forwarded to plugin.install(context, options)
|
|
15932
|
+
* @returns {this}
|
|
15933
|
+
*/
|
|
15934
|
+
use(plugin, options = {}) {
|
|
15935
|
+
if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
|
|
15936
|
+
this._installPlugin(plugin, options);
|
|
15937
|
+
return this;
|
|
15938
|
+
}
|
|
15939
|
+
/**
|
|
15940
|
+
* Returns the public API returned by plugin.install(), or null.
|
|
15941
|
+
* @param {string} name
|
|
15942
|
+
* @returns {*}
|
|
15943
|
+
*/
|
|
15944
|
+
getPlugin(name) {
|
|
15945
|
+
return this._plugins.get(name)?.publicApi ?? null;
|
|
15946
|
+
}
|
|
15947
|
+
_installPlugin(plugin, pluginOptions = {}) {
|
|
15948
|
+
const { name } = plugin;
|
|
15949
|
+
if (!name || typeof name !== "string") {
|
|
15950
|
+
console.warn("[AutumnNote] Plugin must have a string `name` property.");
|
|
15951
|
+
return;
|
|
15952
|
+
}
|
|
15953
|
+
if (this._plugins.has(name)) {
|
|
15954
|
+
console.warn(`[AutumnNote] Plugin "${name}" already installed on this instance. Skipping.`);
|
|
15955
|
+
return;
|
|
15956
|
+
}
|
|
15957
|
+
const publicApi = typeof plugin.install === "function" ? plugin.install(this, pluginOptions) ?? null : null;
|
|
15958
|
+
this._plugins.set(name, {
|
|
15959
|
+
plugin,
|
|
15960
|
+
publicApi
|
|
15961
|
+
});
|
|
15962
|
+
}
|
|
15963
|
+
_applyGlobalPlugins() {
|
|
15964
|
+
for (const { plugin, options } of _globalPlugins.values()) this._installPlugin(plugin, options);
|
|
15965
|
+
}
|
|
15635
15966
|
_bindEditorEvents(editable) {
|
|
15636
15967
|
const d0 = on(editable, "input", () => this._syncToTarget());
|
|
15637
15968
|
const d1 = on(editable, "focus", () => {
|
|
@@ -15882,6 +16213,10 @@ var Context = class {
|
|
|
15882
16213
|
if (typeof module.destroy === "function") module.destroy();
|
|
15883
16214
|
});
|
|
15884
16215
|
this._modules.clear();
|
|
16216
|
+
for (const { plugin } of this._plugins.values()) if (typeof plugin.uninstall === "function") try {
|
|
16217
|
+
plugin.uninstall(this);
|
|
16218
|
+
} catch (_) {}
|
|
16219
|
+
this._plugins.clear();
|
|
15885
16220
|
this._disposers.forEach((d) => d());
|
|
15886
16221
|
this._disposers = [];
|
|
15887
16222
|
const container = this.layoutInfo.container;
|
|
@@ -16068,7 +16403,27 @@ var AutumnNote = {
|
|
|
16068
16403
|
registerModule(name, ModuleClass) {
|
|
16069
16404
|
_customModules.set(name, ModuleClass);
|
|
16070
16405
|
},
|
|
16071
|
-
|
|
16406
|
+
use(plugin, options = {}) {
|
|
16407
|
+
if (!plugin || typeof plugin.name !== "string") throw new TypeError("[AutumnNote] AutumnNote.use: plugin must have a string `name` property.");
|
|
16408
|
+
if (_globalPlugins.has(plugin.name)) {
|
|
16409
|
+
console.warn(`[AutumnNote] Plugin "${plugin.name}" already registered globally. Skipping.`);
|
|
16410
|
+
return this;
|
|
16411
|
+
}
|
|
16412
|
+
if (Array.isArray(plugin.buttons)) plugin.buttons.forEach((b) => registerButton(b));
|
|
16413
|
+
_globalPlugins.set(plugin.name, {
|
|
16414
|
+
plugin,
|
|
16415
|
+
options
|
|
16416
|
+
});
|
|
16417
|
+
return this;
|
|
16418
|
+
},
|
|
16419
|
+
hasPlugin(name) {
|
|
16420
|
+
return _globalPlugins.has(name);
|
|
16421
|
+
},
|
|
16422
|
+
registerButton(btnDef) {
|
|
16423
|
+
registerButton(btnDef);
|
|
16424
|
+
return this;
|
|
16425
|
+
},
|
|
16426
|
+
version: "1.4.0"
|
|
16072
16427
|
};
|
|
16073
16428
|
/**
|
|
16074
16429
|
* @param {string|Element|NodeList|Element[]} selector
|
|
@@ -16081,6 +16436,6 @@ function resolveElements(selector) {
|
|
|
16081
16436
|
return [];
|
|
16082
16437
|
}
|
|
16083
16438
|
//#endregion
|
|
16084
|
-
export { Context, ELEMENT_NODE, TEXT_NODE, WrappedRange, alignCenterBtn, alignJustifyBtn, alignLeftBtn, alignRightBtn, all, ancestors, any, backColorBtn, boldBtn, checklistBtn, children, chunk, clamp, closest, closestPara, codeviewBtn, collapsedRange, compose, createElement, currentRange, debounce, AutumnNote as default, defaultOptions, defaultToolbar, directionBtn, emojiBtn, env, findBtn, findReplaceBtn, first, flatten, fontFamilyBtn, fontSizeBtn, foreColorBtn, fromNativeRange, fullscreenBtn, groupBy, hrBtn, iconBtn, identity, imageBtn, indentBtn, initial, inlineCodeBtn, insertAfter, isAnchor, isEditable, isElement, isEmpty, isFunction, isImage, isInline, isInsideEditable, isKey, isLi, isList, isModifier, isNil, isPara, isPlainObject, isSelectionInside, isString, isTable, isText, isVoid, italicBtn, key, last, lineHeightBtn, linkBtn, locales, mergeDeep, nextElement, nodeValue, olBtn, on, outdentBtn, outerHtml, paragraphStyleBtn, placeCaret, prevElement, printBtn, rangeFromElement, rect2bnd, redoBtn, remove, removeFormatBtn, resolveLocale, sanitiseHTML, sanitiseUrl, shortcutsBtn, splitText, strikeBtn, subscriptBtn, superscriptBtn, tableBtn, tail, throttle, trapFocus, ulBtn, underlineBtn, undoBtn, unique, unwrap, videoBtn, withSavedRange, wrap };
|
|
16439
|
+
export { Context, ELEMENT_NODE, TEXT_NODE, WrappedRange, _buttonRegistry, alignCenterBtn, alignJustifyBtn, alignLeftBtn, alignRightBtn, all, ancestors, any, backColorBtn, boldBtn, checklistBtn, children, chunk, clamp, closest, closestPara, codeviewBtn, collapsedRange, compose, createElement, currentRange, debounce, AutumnNote as default, defaultOptions, defaultToolbar, directionBtn, emojiBtn, env, findBtn, findReplaceBtn, first, flatten, fontFamilyBtn, fontSizeBtn, foreColorBtn, fromNativeRange, fullscreenBtn, getButton, groupBy, hrBtn, iconBtn, identity, imageBtn, indentBtn, initial, inlineCodeBtn, insertAfter, isAnchor, isEditable, isElement, isEmpty, isFunction, isImage, isInline, isInsideEditable, isKey, isLi, isList, isModifier, isNil, isPara, isPlainObject, isSelectionInside, isString, isTable, isText, isVoid, italicBtn, key, last, lineHeightBtn, linkBtn, locales, mergeDeep, nextElement, nodeValue, olBtn, on, outdentBtn, outerHtml, paragraphStyleBtn, placeCaret, prevElement, printBtn, rangeFromElement, rect2bnd, redoBtn, registerButton, remove, removeFormatBtn, resolveLocale, sanitiseHTML, sanitiseUrl, shortcutsBtn, splitText, strikeBtn, subscriptBtn, superscriptBtn, tableBtn, tail, throttle, trapFocus, ulBtn, underlineBtn, undoBtn, unique, unwrap, videoBtn, withSavedRange, wrap };
|
|
16085
16440
|
|
|
16086
16441
|
//# sourceMappingURL=autumnnote.es.js.map
|