autumnnote 1.2.1 → 1.3.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/dist/autumnnote.es.js +337 -104
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +308 -88
- package/dist/autumnnote.umd.js.map +1 -1
- package/package.json +1 -1
- 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/module/BubbleToolbar.js +26 -7
- 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 +14 -0
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();
|
|
@@ -1614,6 +1654,7 @@ var en = {
|
|
|
1614
1654
|
replaceAriaLabel: "Replace with",
|
|
1615
1655
|
replaceBtn: "Replace",
|
|
1616
1656
|
replaceAllBtn: "Replace All",
|
|
1657
|
+
noResults: "No results",
|
|
1617
1658
|
close: "×"
|
|
1618
1659
|
},
|
|
1619
1660
|
shortcutsDialog: {
|
|
@@ -1966,6 +2007,7 @@ var locales = {
|
|
|
1966
2007
|
replaceAriaLabel: "Thay thế bằng",
|
|
1967
2008
|
replaceBtn: "Thay thế",
|
|
1968
2009
|
replaceAllBtn: "Thay thế tất cả",
|
|
2010
|
+
noResults: "Không có kết quả",
|
|
1969
2011
|
close: "×"
|
|
1970
2012
|
},
|
|
1971
2013
|
shortcutsDialog: {
|
|
@@ -2295,6 +2337,7 @@ var locales = {
|
|
|
2295
2337
|
replacePlaceholder: "置換後…",
|
|
2296
2338
|
replaceAriaLabel: "置換後のテキスト",
|
|
2297
2339
|
replaceBtn: "置換",
|
|
2340
|
+
noResults: "結果なし",
|
|
2298
2341
|
replaceAllBtn: "すべて置換",
|
|
2299
2342
|
close: "×"
|
|
2300
2343
|
},
|
|
@@ -2625,6 +2668,7 @@ var locales = {
|
|
|
2625
2668
|
replacePlaceholder: "替换为…",
|
|
2626
2669
|
replaceAriaLabel: "替换为",
|
|
2627
2670
|
replaceBtn: "替换",
|
|
2671
|
+
noResults: "没有结果",
|
|
2628
2672
|
replaceAllBtn: "全部替换",
|
|
2629
2673
|
close: "×"
|
|
2630
2674
|
},
|
|
@@ -2955,6 +2999,7 @@ var locales = {
|
|
|
2955
2999
|
replacePlaceholder: "Remplacer par…",
|
|
2956
3000
|
replaceAriaLabel: "Remplacer par",
|
|
2957
3001
|
replaceBtn: "Remplacer",
|
|
3002
|
+
noResults: "Aucun résultat",
|
|
2958
3003
|
replaceAllBtn: "Tout remplacer",
|
|
2959
3004
|
close: "×"
|
|
2960
3005
|
},
|
|
@@ -3285,6 +3330,7 @@ var locales = {
|
|
|
3285
3330
|
replacePlaceholder: "Ersetzen durch…",
|
|
3286
3331
|
replaceAriaLabel: "Ersetzen durch",
|
|
3287
3332
|
replaceBtn: "Ersetzen",
|
|
3333
|
+
noResults: "Keine Ergebnisse",
|
|
3288
3334
|
replaceAllBtn: "Alle ersetzen",
|
|
3289
3335
|
close: "×"
|
|
3290
3336
|
},
|
|
@@ -3615,6 +3661,7 @@ var locales = {
|
|
|
3615
3661
|
replacePlaceholder: "Reemplazar con…",
|
|
3616
3662
|
replaceAriaLabel: "Reemplazar con",
|
|
3617
3663
|
replaceBtn: "Reemplazar",
|
|
3664
|
+
noResults: "Sin resultados",
|
|
3618
3665
|
replaceAllBtn: "Reemplazar todo",
|
|
3619
3666
|
close: "×"
|
|
3620
3667
|
},
|
|
@@ -3945,6 +3992,7 @@ var locales = {
|
|
|
3945
3992
|
replacePlaceholder: "바꿀 내용…",
|
|
3946
3993
|
replaceAriaLabel: "바꿀 내용",
|
|
3947
3994
|
replaceBtn: "바꾸기",
|
|
3995
|
+
noResults: "결과 없음",
|
|
3948
3996
|
replaceAllBtn: "모두 바꾸기",
|
|
3949
3997
|
close: "×"
|
|
3950
3998
|
},
|
|
@@ -4165,8 +4213,10 @@ var PROHIBITED_TAGS = [
|
|
|
4165
4213
|
"object",
|
|
4166
4214
|
"embed",
|
|
4167
4215
|
"form",
|
|
4168
|
-
"
|
|
4216
|
+
"base"
|
|
4169
4217
|
];
|
|
4218
|
+
/** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
|
|
4219
|
+
var UNWRAP_TAGS = new Set(["button"]);
|
|
4170
4220
|
/** Attributes whose values must be sanitised as URLs. */
|
|
4171
4221
|
var URL_ATTRS = [
|
|
4172
4222
|
"href",
|
|
@@ -4184,60 +4234,61 @@ var TRUSTED_IFRAME_HOSTS = new Set([
|
|
|
4184
4234
|
"player.vimeo.com"
|
|
4185
4235
|
]);
|
|
4186
4236
|
/**
|
|
4187
|
-
*
|
|
4188
|
-
* Uses DOMParser so the sanitisation follows normal browser parsing rules —
|
|
4189
|
-
* no regex shortcuts that can be bypassed by encoding tricks.
|
|
4237
|
+
* Produce a sanitized HTML string with dangerous elements and attributes removed.
|
|
4190
4238
|
*
|
|
4191
|
-
*
|
|
4192
|
-
*
|
|
4193
|
-
*
|
|
4194
|
-
* - Rejects javascript: and vbscript: URLs in URL attributes
|
|
4195
|
-
* - Rejects data: URIs everywhere except img[src] (base64 uploads)
|
|
4239
|
+
* Removes disallowed tags and wrappers, strips event-handler attributes, rejects
|
|
4240
|
+
* `javascript:`/`vbscript:` URLs and most `data:` URIs, restricts iframe `src`
|
|
4241
|
+
* to trusted hosts when enabled, and permits only checklist checkboxes as inputs.
|
|
4196
4242
|
*
|
|
4197
|
-
* @param {string} html
|
|
4198
|
-
* @
|
|
4243
|
+
* @param {string} html - HTML fragment to sanitize.
|
|
4244
|
+
* @param {Object} [options]
|
|
4245
|
+
* @param {boolean} [options.allowIframes=false] - If true, `iframe` elements are not removed but their `src` is restricted to trusted hosts and `srcdoc` is removed.
|
|
4246
|
+
* @returns {string} The sanitized HTML fragment.
|
|
4199
4247
|
*/
|
|
4200
4248
|
function sanitiseHTML(html, { allowIframes = false } = {}) {
|
|
4201
4249
|
const doc = new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html");
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4250
|
+
const allElements = Array.from(doc.querySelectorAll("*"));
|
|
4251
|
+
const prohibited = new Set(allowIframes ? PROHIBITED_TAGS.filter((t) => t !== "iframe") : PROHIBITED_TAGS);
|
|
4252
|
+
for (const el of allElements) {
|
|
4253
|
+
const tag = el.tagName.toLowerCase();
|
|
4254
|
+
if (UNWRAP_TAGS.has(tag)) {
|
|
4255
|
+
el.replaceWith(...el.childNodes);
|
|
4256
|
+
continue;
|
|
4257
|
+
}
|
|
4258
|
+
if (prohibited.has(tag)) {
|
|
4259
|
+
el.remove();
|
|
4260
|
+
continue;
|
|
4261
|
+
}
|
|
4262
|
+
for (const attr of Array.from(el.attributes)) {
|
|
4207
4263
|
if (attr.name.startsWith("on")) {
|
|
4208
4264
|
el.removeAttribute(attr.name);
|
|
4209
|
-
|
|
4265
|
+
continue;
|
|
4210
4266
|
}
|
|
4211
4267
|
if (URL_ATTRS.includes(attr.name)) {
|
|
4212
4268
|
const val = attr.value.trim();
|
|
4213
4269
|
if (/^(javascript|vbscript):/i.test(val)) {
|
|
4214
4270
|
el.removeAttribute(attr.name);
|
|
4215
|
-
|
|
4271
|
+
continue;
|
|
4216
4272
|
}
|
|
4217
4273
|
if (/^data:/i.test(val) && !(attr.name === "src" && el.tagName === "IMG")) el.removeAttribute(attr.name);
|
|
4218
4274
|
}
|
|
4219
4275
|
if (el.tagName === "IFRAME") {
|
|
4220
4276
|
if (attr.name === "srcdoc") {
|
|
4221
4277
|
el.removeAttribute(attr.name);
|
|
4222
|
-
|
|
4223
|
-
}
|
|
4224
|
-
if (attr.name === "src") {
|
|
4225
|
-
if (!isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
|
|
4226
|
-
return;
|
|
4278
|
+
continue;
|
|
4227
4279
|
}
|
|
4280
|
+
if (attr.name === "src" && !isTrustedIframeSrc(attr.value)) el.removeAttribute(attr.name);
|
|
4228
4281
|
}
|
|
4229
|
-
}
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
else Array.from(el.attributes).forEach((attr) => {
|
|
4234
|
-
if (![
|
|
4282
|
+
}
|
|
4283
|
+
if (tag === "input") {
|
|
4284
|
+
if (!(el.closest("ul.an-checklist") !== null && el.closest("li") !== null) || el.getAttribute("type") !== "checkbox") el.remove();
|
|
4285
|
+
else for (const attr of Array.from(el.attributes)) if (![
|
|
4235
4286
|
"type",
|
|
4236
4287
|
"checked",
|
|
4237
4288
|
"contenteditable"
|
|
4238
4289
|
].includes(attr.name)) el.removeAttribute(attr.name);
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4241
4292
|
return doc.body.innerHTML;
|
|
4242
4293
|
}
|
|
4243
4294
|
/**
|
|
@@ -4438,7 +4489,18 @@ var History = class {
|
|
|
4438
4489
|
const sel = window.getSelection();
|
|
4439
4490
|
sel.removeAllRanges();
|
|
4440
4491
|
sel.addRange(range);
|
|
4441
|
-
} catch (_) {
|
|
4492
|
+
} catch (_) {
|
|
4493
|
+
try {
|
|
4494
|
+
const fb = document.createRange();
|
|
4495
|
+
fb.setStart(this.editable, 0);
|
|
4496
|
+
fb.collapse(true);
|
|
4497
|
+
const s = window.getSelection();
|
|
4498
|
+
if (s) {
|
|
4499
|
+
s.removeAllRanges();
|
|
4500
|
+
s.addRange(fb);
|
|
4501
|
+
}
|
|
4502
|
+
} catch (_2) {}
|
|
4503
|
+
}
|
|
4442
4504
|
}
|
|
4443
4505
|
_savePoint() {
|
|
4444
4506
|
if (this.stackOffset < this.stack.length - 1) this.stack = this.stack.slice(0, this.stackOffset + 1);
|
|
@@ -4465,6 +4527,10 @@ var History = class {
|
|
|
4465
4527
|
* @returns {{ html: string, images: Object<string,string> }}
|
|
4466
4528
|
*/
|
|
4467
4529
|
_tokenizeImages(html) {
|
|
4530
|
+
if (!html.includes("data:")) return {
|
|
4531
|
+
html,
|
|
4532
|
+
images: {}
|
|
4533
|
+
};
|
|
4468
4534
|
const images = {};
|
|
4469
4535
|
let index = 0;
|
|
4470
4536
|
return {
|
|
@@ -4536,11 +4602,12 @@ var History = class {
|
|
|
4536
4602
|
* Inspired by Summernote's table handling
|
|
4537
4603
|
*/
|
|
4538
4604
|
/**
|
|
4539
|
-
*
|
|
4540
|
-
* @param {number} cols
|
|
4541
|
-
* @param {number} rows
|
|
4542
|
-
* @param {{ headerRow?: boolean }} [opts]
|
|
4543
|
-
* @
|
|
4605
|
+
* Build an HTML table with the given number of columns and rows, optionally including a header row.
|
|
4606
|
+
* @param {number} cols - Number of columns in each row.
|
|
4607
|
+
* @param {number} rows - Total number of rows to create (including header when `headerRow` is true).
|
|
4608
|
+
* @param {{ headerRow?: boolean }} [opts] - Options object.
|
|
4609
|
+
* @param {boolean} [opts.headerRow=false] - When true and `rows > 0`, creates a header row (`<thead>`) plus body rows for the remainder.
|
|
4610
|
+
* @returns {HTMLTableElement} The constructed `<table>` element with a `<tbody>` and optional `<thead>`; each cell contains a `<br>` placeholder.
|
|
4544
4611
|
*/
|
|
4545
4612
|
function createTable(cols, rows, opts = {}) {
|
|
4546
4613
|
const { headerRow = false } = opts;
|
|
@@ -4549,7 +4616,7 @@ function createTable(cols, rows, opts = {}) {
|
|
|
4549
4616
|
const thead = createElement("thead");
|
|
4550
4617
|
const tr = createElement("tr");
|
|
4551
4618
|
for (let c = 0; c < cols; c++) {
|
|
4552
|
-
const th = createElement("th", {}, ["
|
|
4619
|
+
const th = createElement("th", {}, [document.createElement("br")]);
|
|
4553
4620
|
tr.appendChild(th);
|
|
4554
4621
|
}
|
|
4555
4622
|
thead.appendChild(tr);
|
|
@@ -4561,7 +4628,7 @@ function createTable(cols, rows, opts = {}) {
|
|
|
4561
4628
|
for (let r = 0; r < bodyRows; r++) {
|
|
4562
4629
|
const tr = createElement("tr");
|
|
4563
4630
|
for (let c = 0; c < cols; c++) {
|
|
4564
|
-
const td = createElement("td", {}, ["
|
|
4631
|
+
const td = createElement("td", {}, [document.createElement("br")]);
|
|
4565
4632
|
tr.appendChild(td);
|
|
4566
4633
|
}
|
|
4567
4634
|
tbody.appendChild(tr);
|
|
@@ -4569,13 +4636,52 @@ function createTable(cols, rows, opts = {}) {
|
|
|
4569
4636
|
return table;
|
|
4570
4637
|
}
|
|
4571
4638
|
/**
|
|
4572
|
-
*
|
|
4573
|
-
* @param {number} cols
|
|
4574
|
-
* @param {number} rows
|
|
4575
|
-
* @param {{ headerRow?: boolean }} [opts]
|
|
4639
|
+
* Insert a table at the current selection and place the caret into its first cell.
|
|
4640
|
+
* @param {number} cols - Number of columns for the new table.
|
|
4641
|
+
* @param {number} rows - Number of rows for the new table.
|
|
4642
|
+
* @param {{ headerRow?: boolean }} [opts] - Options for table creation.
|
|
4643
|
+
* @param {boolean} [opts.headerRow=false] - If true, include a header row as the first row.
|
|
4576
4644
|
*/
|
|
4577
4645
|
function insertTable(cols, rows, opts = {}) {
|
|
4578
|
-
|
|
4646
|
+
if (cols <= 0 || rows <= 0) return;
|
|
4647
|
+
const table = createTable(cols, rows, opts);
|
|
4648
|
+
const sel = window.getSelection();
|
|
4649
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
4650
|
+
const range = sel.getRangeAt(0);
|
|
4651
|
+
range.deleteContents();
|
|
4652
|
+
const BLOCK = new Set([
|
|
4653
|
+
"P",
|
|
4654
|
+
"DIV",
|
|
4655
|
+
"H1",
|
|
4656
|
+
"H2",
|
|
4657
|
+
"H3",
|
|
4658
|
+
"H4",
|
|
4659
|
+
"H5",
|
|
4660
|
+
"H6",
|
|
4661
|
+
"BLOCKQUOTE",
|
|
4662
|
+
"LI",
|
|
4663
|
+
"PRE"
|
|
4664
|
+
]);
|
|
4665
|
+
let anchor = range.startContainer;
|
|
4666
|
+
if (anchor.nodeType === 3) anchor = anchor.parentElement;
|
|
4667
|
+
while (anchor && !BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentElement) anchor = anchor.parentElement;
|
|
4668
|
+
if (anchor && BLOCK.has(anchor.tagName?.toUpperCase()) && anchor.parentNode) {
|
|
4669
|
+
anchor.after(table);
|
|
4670
|
+
if (!table.nextElementSibling) {
|
|
4671
|
+
const p = document.createElement("p");
|
|
4672
|
+
p.appendChild(document.createElement("br"));
|
|
4673
|
+
table.after(p);
|
|
4674
|
+
}
|
|
4675
|
+
if (!anchor.textContent.trim() && !anchor.querySelector("img, video, table")) anchor.remove();
|
|
4676
|
+
} else range.insertNode(table);
|
|
4677
|
+
const firstCell = table.querySelector("td, th");
|
|
4678
|
+
if (firstCell) {
|
|
4679
|
+
const nr = document.createRange();
|
|
4680
|
+
nr.setStart(firstCell, 0);
|
|
4681
|
+
nr.collapse(true);
|
|
4682
|
+
sel.removeAllRanges();
|
|
4683
|
+
sel.addRange(nr);
|
|
4684
|
+
}
|
|
4579
4685
|
}
|
|
4580
4686
|
//#endregion
|
|
4581
4687
|
//#region src/js/core/key.js
|
|
@@ -4790,7 +4896,7 @@ function handleKeydown(event, editable, options = {}) {
|
|
|
4790
4896
|
if (para && para.nodeName.toUpperCase() === "PRE") {
|
|
4791
4897
|
if (event.shiftKey) return false;
|
|
4792
4898
|
event.preventDefault();
|
|
4793
|
-
execCommand("insertText", "
|
|
4899
|
+
execCommand("insertText", " ".repeat(options.tabSize || 4));
|
|
4794
4900
|
return true;
|
|
4795
4901
|
}
|
|
4796
4902
|
if (options.tabSize) {
|
|
@@ -4883,6 +4989,11 @@ function handleKeydown(event, editable, options = {}) {
|
|
|
4883
4989
|
return true;
|
|
4884
4990
|
}
|
|
4885
4991
|
const para = closestPara(range.sc, editable);
|
|
4992
|
+
if (para && para.nodeName.toUpperCase() === "PRE") {
|
|
4993
|
+
event.preventDefault();
|
|
4994
|
+
execCommand("insertText", "\n");
|
|
4995
|
+
return true;
|
|
4996
|
+
}
|
|
4886
4997
|
if (para && para.nodeName.toUpperCase() === "BLOCKQUOTE") {
|
|
4887
4998
|
const native = range.toNativeRange();
|
|
4888
4999
|
native.setEnd(para, para.childNodes.length);
|
|
@@ -4916,11 +5027,22 @@ function handleKeydown(event, editable, options = {}) {
|
|
|
4916
5027
|
function htmlToMarkdown(html) {
|
|
4917
5028
|
return _domToMd(new DOMParser().parseFromString(`<body>${html || ""}</body>`, "text/html").body).replace(/\n{3,}/g, "\n\n").trim();
|
|
4918
5029
|
}
|
|
4919
|
-
|
|
5030
|
+
/**
|
|
5031
|
+
* Convert a DOM node subtree into Markdown.
|
|
5032
|
+
*
|
|
5033
|
+
* Recursively produces a Markdown string representing the given DOM node and its descendants,
|
|
5034
|
+
* handling common HTML constructs such as paragraphs, headings, lists (with nested indentation),
|
|
5035
|
+
* blockquotes, fenced and inline code, links, images, tables, horizontal rules, and basic inline emphasis.
|
|
5036
|
+
*
|
|
5037
|
+
* @param {Node} node - The DOM node to convert.
|
|
5038
|
+
* @param {number} [depth=0] - Current nesting depth used to indent nested list items.
|
|
5039
|
+
* @returns {string} The Markdown representation of the node subtree.
|
|
5040
|
+
*/
|
|
5041
|
+
function _domToMd(node, depth = 0) {
|
|
4920
5042
|
if (node.nodeType === 3) return node.textContent.replace(/\s+/g, " ");
|
|
4921
5043
|
if (node.nodeType !== 1) return "";
|
|
4922
5044
|
const tag = node.nodeName.toLowerCase();
|
|
4923
|
-
const inner = () => Array.from(node.childNodes).map(_domToMd).join("");
|
|
5045
|
+
const inner = () => Array.from(node.childNodes).map((n) => _domToMd(n, depth)).join("");
|
|
4924
5046
|
switch (tag) {
|
|
4925
5047
|
case "p":
|
|
4926
5048
|
case "div": return `\n\n${inner()}\n\n`;
|
|
@@ -4960,12 +5082,16 @@ function _domToMd(node) {
|
|
|
4960
5082
|
case "ul": {
|
|
4961
5083
|
const items = Array.from(node.querySelectorAll(":scope > li"));
|
|
4962
5084
|
if (!items.length) return inner();
|
|
4963
|
-
|
|
5085
|
+
const indent = " ".repeat(depth);
|
|
5086
|
+
const lines = items.map((li) => `${indent}- ${_domToMd(li, depth + 1).trim()}`).join("\n");
|
|
5087
|
+
return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
|
|
4964
5088
|
}
|
|
4965
5089
|
case "ol": {
|
|
4966
5090
|
const items = Array.from(node.querySelectorAll(":scope > li"));
|
|
4967
5091
|
if (!items.length) return inner();
|
|
4968
|
-
|
|
5092
|
+
const indent = " ".repeat(depth);
|
|
5093
|
+
const lines = items.map((li, i) => `${indent}${i + 1}. ${_domToMd(li, depth + 1).trim()}`).join("\n");
|
|
5094
|
+
return depth === 0 ? `\n\n${lines}\n\n` : `\n${lines}`;
|
|
4969
5095
|
}
|
|
4970
5096
|
case "li": return inner();
|
|
4971
5097
|
case "hr": return "\n\n---\n\n";
|
|
@@ -4989,12 +5115,15 @@ function _domToMd(node) {
|
|
|
4989
5115
|
}
|
|
4990
5116
|
}
|
|
4991
5117
|
/**
|
|
4992
|
-
*
|
|
4993
|
-
*
|
|
4994
|
-
*
|
|
5118
|
+
* Detects whether a string likely contains Markdown syntax.
|
|
5119
|
+
*
|
|
5120
|
+
* Checks for common Markdown constructs such as ATX headings, unordered or
|
|
5121
|
+
* ordered list items, blockquotes, fenced code blocks, and bold emphasis.
|
|
5122
|
+
* @param {string} text - Input text to inspect for Markdown patterns.
|
|
5123
|
+
* @returns {boolean} `true` if any Markdown-like pattern is present, `false` otherwise.
|
|
4995
5124
|
*/
|
|
4996
5125
|
function isMarkdown(text) {
|
|
4997
|
-
return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S
|
|
5126
|
+
return /^#{1,6} \S|^\s*[-*+] \S|^\s*\d+\. \S|^> \S|^```|^\*{2}.+?\*{2}/m.test(text);
|
|
4998
5127
|
}
|
|
4999
5128
|
/**
|
|
5000
5129
|
* Converts a Markdown string to an HTML string.
|
|
@@ -5757,6 +5886,8 @@ var Toolbar = class {
|
|
|
5757
5886
|
this._disposers = [];
|
|
5758
5887
|
/** @type {Array<() => void>} closers for all open color picker popups */
|
|
5759
5888
|
this._colorPickerClosers = [];
|
|
5889
|
+
/** @type {number|null} rAF handle for debounced refresh */
|
|
5890
|
+
this._refreshRaf = null;
|
|
5760
5891
|
}
|
|
5761
5892
|
initialize() {
|
|
5762
5893
|
this.el = createElement("div", { class: "an-toolbar" });
|
|
@@ -5766,6 +5897,8 @@ var Toolbar = class {
|
|
|
5766
5897
|
return this;
|
|
5767
5898
|
}
|
|
5768
5899
|
destroy() {
|
|
5900
|
+
if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
|
|
5901
|
+
this._refreshRaf = null;
|
|
5769
5902
|
this._disposers.forEach((d) => d());
|
|
5770
5903
|
this._disposers = [];
|
|
5771
5904
|
if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);
|
|
@@ -6161,6 +6294,13 @@ var Toolbar = class {
|
|
|
6161
6294
|
return _faPageLevelReady;
|
|
6162
6295
|
}
|
|
6163
6296
|
refresh() {
|
|
6297
|
+
if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
|
|
6298
|
+
this._refreshRaf = requestAnimationFrame(() => {
|
|
6299
|
+
this._refreshRaf = null;
|
|
6300
|
+
this._doRefresh();
|
|
6301
|
+
});
|
|
6302
|
+
}
|
|
6303
|
+
_doRefresh() {
|
|
6164
6304
|
if (!this.el) return;
|
|
6165
6305
|
const btnMap = this._btnMap || /* @__PURE__ */ new Map();
|
|
6166
6306
|
this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
|
|
@@ -6338,7 +6478,7 @@ var Statusbar = class {
|
|
|
6338
6478
|
}
|
|
6339
6479
|
update() {
|
|
6340
6480
|
if (!this._wordCountEl || !this._charCountEl) return;
|
|
6341
|
-
const text = this.context.layoutInfo.editable.
|
|
6481
|
+
const text = this.context.layoutInfo.editable.textContent || "";
|
|
6342
6482
|
const words = _countWords(text);
|
|
6343
6483
|
const chars = text.replace(/\n/g, "").length;
|
|
6344
6484
|
const maxWords = this.options.maxWords || 0;
|
|
@@ -6533,7 +6673,7 @@ var Clipboard = class {
|
|
|
6533
6673
|
event.preventDefault();
|
|
6534
6674
|
const raw = clipboardData.getData("text/html");
|
|
6535
6675
|
const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
|
|
6536
|
-
const isSocialContent =
|
|
6676
|
+
const isSocialContent = /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
|
|
6537
6677
|
let html = raw;
|
|
6538
6678
|
if (isWordContent) html = this._cleanWordHtml(html);
|
|
6539
6679
|
else if (isSocialContent) html = this._cleanSocialHtml(html);
|
|
@@ -6635,7 +6775,7 @@ var Clipboard = class {
|
|
|
6635
6775
|
*/
|
|
6636
6776
|
_dataUrlToBlob(dataUrl) {
|
|
6637
6777
|
const [header, b64] = dataUrl.split(",");
|
|
6638
|
-
const mime = header.match(/:(.*?);/)[1];
|
|
6778
|
+
const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
|
|
6639
6779
|
const binary = atob(b64);
|
|
6640
6780
|
const arr = new Uint8Array(binary.length);
|
|
6641
6781
|
for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
|
|
@@ -7641,6 +7781,7 @@ var ImageResizer = class {
|
|
|
7641
7781
|
_select(img) {
|
|
7642
7782
|
if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
|
|
7643
7783
|
this._activeImg = img;
|
|
7784
|
+
this._lastOverlayPos = null;
|
|
7644
7785
|
img.classList.add("an-image-selected");
|
|
7645
7786
|
this._updateOverlayPosition();
|
|
7646
7787
|
this._overlay.style.display = "block";
|
|
@@ -7664,6 +7805,14 @@ var ImageResizer = class {
|
|
|
7664
7805
|
const offsetParent = this._overlay.offsetParent || this._container;
|
|
7665
7806
|
const containerRect = offsetParent.getBoundingClientRect();
|
|
7666
7807
|
const rect = this._activeImg.getBoundingClientRect();
|
|
7808
|
+
const p = this._lastOverlayPos;
|
|
7809
|
+
if (p && p.l === rect.left && p.t === rect.top && p.w === rect.width && p.h === rect.height) return;
|
|
7810
|
+
this._lastOverlayPos = {
|
|
7811
|
+
l: rect.left,
|
|
7812
|
+
t: rect.top,
|
|
7813
|
+
w: rect.width,
|
|
7814
|
+
h: rect.height
|
|
7815
|
+
};
|
|
7667
7816
|
const left = rect.left - containerRect.left + offsetParent.scrollLeft;
|
|
7668
7817
|
const top = rect.top - containerRect.top + offsetParent.scrollTop;
|
|
7669
7818
|
this._overlay.style.left = `${left}px`;
|
|
@@ -13362,7 +13511,7 @@ var ContextMenu = class {
|
|
|
13362
13511
|
cells.forEach((cell) => {
|
|
13363
13512
|
cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
|
|
13364
13513
|
});
|
|
13365
|
-
labelEl.textContent = rows && cols ? `${
|
|
13514
|
+
labelEl.textContent = rows && cols ? `${rows} × ${cols}` : this.context.locale.contextMenu.table || "Insert Table";
|
|
13366
13515
|
};
|
|
13367
13516
|
panel.appendChild(gridEl);
|
|
13368
13517
|
panel.appendChild(labelEl);
|
|
@@ -14002,7 +14151,6 @@ var FindReplace = class {
|
|
|
14002
14151
|
if (!editable) return;
|
|
14003
14152
|
const rawMatches = this._findRawMatches(query, editable);
|
|
14004
14153
|
if (rawMatches.length === 0) return;
|
|
14005
|
-
this._currentIndex = 0;
|
|
14006
14154
|
for (let i = rawMatches.length - 1; i >= 0; i--) {
|
|
14007
14155
|
const { node, start, end } = rawMatches[i];
|
|
14008
14156
|
try {
|
|
@@ -14019,6 +14167,7 @@ var FindReplace = class {
|
|
|
14019
14167
|
}
|
|
14020
14168
|
this._matches.reverse();
|
|
14021
14169
|
this._matches = this._matches.filter((m) => m.mark);
|
|
14170
|
+
this._currentIndex = 0;
|
|
14022
14171
|
if (this._matches.length === 0) return;
|
|
14023
14172
|
if (this._matches[0] && this._matches[0].mark) {
|
|
14024
14173
|
this._matches[0].mark.className = "an-highlight an-highlight-current";
|
|
@@ -14044,12 +14193,13 @@ var FindReplace = class {
|
|
|
14044
14193
|
this._lastCaseSensitive = this._caseSensitive;
|
|
14045
14194
|
}
|
|
14046
14195
|
const re = this._queryRegex;
|
|
14196
|
+
const MAX_RESULTS = 500;
|
|
14047
14197
|
const walker = document.createTreeWalker(root, 4);
|
|
14048
14198
|
let node;
|
|
14049
|
-
while (node = walker.nextNode()) {
|
|
14199
|
+
while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
|
|
14050
14200
|
re.lastIndex = 0;
|
|
14051
14201
|
let m;
|
|
14052
|
-
while ((m = re.exec(node.textContent)) !== null) results.push({
|
|
14202
|
+
while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
|
|
14053
14203
|
node,
|
|
14054
14204
|
start: m.index,
|
|
14055
14205
|
end: m.index + m[0].length
|
|
@@ -14136,7 +14286,7 @@ var FindReplace = class {
|
|
|
14136
14286
|
const total = this._matches.length;
|
|
14137
14287
|
if (total === 0) {
|
|
14138
14288
|
const query = this._findInput ? this._findInput.value : "";
|
|
14139
|
-
this._counterEl.textContent = query ?
|
|
14289
|
+
this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
|
|
14140
14290
|
} else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
|
|
14141
14291
|
}
|
|
14142
14292
|
};
|
|
@@ -14335,17 +14485,33 @@ var ImageCropOverlay = class {
|
|
|
14335
14485
|
e.preventDefault();
|
|
14336
14486
|
e.stopPropagation();
|
|
14337
14487
|
this._startHandleDrag(e, id);
|
|
14338
|
-
}))
|
|
14488
|
+
}), on(h, "touchstart", (e) => {
|
|
14489
|
+
e.preventDefault();
|
|
14490
|
+
e.stopPropagation();
|
|
14491
|
+
this._startHandleDrag({
|
|
14492
|
+
clientX: e.touches[0].clientX,
|
|
14493
|
+
clientY: e.touches[0].clientY
|
|
14494
|
+
}, id);
|
|
14495
|
+
}, { passive: false }));
|
|
14339
14496
|
this._handles[id] = h;
|
|
14340
14497
|
cropBox.appendChild(h);
|
|
14341
14498
|
});
|
|
14342
14499
|
this._disposers.push(on(cropBox, "mousedown", (e) => {
|
|
14343
|
-
if (e.target
|
|
14344
|
-
if (e.target
|
|
14500
|
+
if (e.target.classList.contains("an-crop-handle")) return;
|
|
14501
|
+
if (e.target !== cropBox && e.target !== grid) return;
|
|
14345
14502
|
e.preventDefault();
|
|
14346
14503
|
e.stopPropagation();
|
|
14347
14504
|
this._startBoxMove(e);
|
|
14348
|
-
}))
|
|
14505
|
+
}), on(cropBox, "touchstart", (e) => {
|
|
14506
|
+
if (e.target.classList.contains("an-crop-handle")) return;
|
|
14507
|
+
if (e.target !== cropBox && e.target !== grid) return;
|
|
14508
|
+
e.preventDefault();
|
|
14509
|
+
e.stopPropagation();
|
|
14510
|
+
this._startBoxMove({
|
|
14511
|
+
clientX: e.touches[0].clientX,
|
|
14512
|
+
clientY: e.touches[0].clientY
|
|
14513
|
+
});
|
|
14514
|
+
}, { passive: false }));
|
|
14349
14515
|
const infoEl = document.createElement("div");
|
|
14350
14516
|
infoEl.className = "an-crop-info";
|
|
14351
14517
|
infoEl.style.cssText = `
|
|
@@ -14503,15 +14669,26 @@ var ImageCropOverlay = class {
|
|
|
14503
14669
|
* @param {(e: MouseEvent) => void} onMove
|
|
14504
14670
|
*/
|
|
14505
14671
|
_attachDocDrag(onMove) {
|
|
14672
|
+
const onTouchMove = (e) => {
|
|
14673
|
+
e.preventDefault();
|
|
14674
|
+
onMove({
|
|
14675
|
+
clientX: e.touches[0].clientX,
|
|
14676
|
+
clientY: e.touches[0].clientY
|
|
14677
|
+
});
|
|
14678
|
+
};
|
|
14506
14679
|
const cleanup = () => {
|
|
14507
14680
|
document.removeEventListener("mousemove", onMove);
|
|
14508
14681
|
document.removeEventListener("mouseup", cleanup);
|
|
14682
|
+
document.removeEventListener("touchmove", onTouchMove);
|
|
14683
|
+
document.removeEventListener("touchend", cleanup);
|
|
14509
14684
|
document.body.style.userSelect = "";
|
|
14510
14685
|
document.body.style.cursor = "";
|
|
14511
14686
|
};
|
|
14512
14687
|
document.body.style.userSelect = "none";
|
|
14513
14688
|
document.addEventListener("mousemove", onMove);
|
|
14514
14689
|
document.addEventListener("mouseup", cleanup, { once: true });
|
|
14690
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
14691
|
+
document.addEventListener("touchend", cleanup, { once: true });
|
|
14515
14692
|
}
|
|
14516
14693
|
_bindEsc() {
|
|
14517
14694
|
const handler = (e) => {
|
|
@@ -14552,7 +14729,7 @@ var ImageCropOverlay = class {
|
|
|
14552
14729
|
height: natH
|
|
14553
14730
|
}, w, h);
|
|
14554
14731
|
if (!canvas) {
|
|
14555
|
-
|
|
14732
|
+
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
14733
|
this._close(false);
|
|
14557
14734
|
return;
|
|
14558
14735
|
}
|
|
@@ -14569,6 +14746,37 @@ var ImageCropOverlay = class {
|
|
|
14569
14746
|
this.context.invoke("imageResizer.updateOverlay");
|
|
14570
14747
|
}
|
|
14571
14748
|
/**
|
|
14749
|
+
* Show a non-blocking inline error banner appended to document.body.
|
|
14750
|
+
* Auto-dismisses after 4 seconds.
|
|
14751
|
+
* @param {string} msg
|
|
14752
|
+
*/
|
|
14753
|
+
_showCropError(msg) {
|
|
14754
|
+
const banner = document.createElement("div");
|
|
14755
|
+
banner.setAttribute("role", "alert");
|
|
14756
|
+
banner.style.cssText = [
|
|
14757
|
+
"position:fixed",
|
|
14758
|
+
"bottom:24px",
|
|
14759
|
+
"left:50%",
|
|
14760
|
+
"transform:translateX(-50%)",
|
|
14761
|
+
"z-index:10200",
|
|
14762
|
+
"max-width:420px",
|
|
14763
|
+
"width:max-content",
|
|
14764
|
+
"background:#7f1d1d",
|
|
14765
|
+
"color:#fecaca",
|
|
14766
|
+
"border:1px solid #b91c1c",
|
|
14767
|
+
"border-radius:8px",
|
|
14768
|
+
"padding:12px 18px",
|
|
14769
|
+
"font:13px/1.5 system-ui,sans-serif",
|
|
14770
|
+
"box-shadow:0 4px 16px rgba(0,0,0,.4)",
|
|
14771
|
+
"pointer-events:auto"
|
|
14772
|
+
].join(";");
|
|
14773
|
+
banner.textContent = msg;
|
|
14774
|
+
document.body.appendChild(banner);
|
|
14775
|
+
setTimeout(() => {
|
|
14776
|
+
if (banner.parentNode) banner.parentNode.removeChild(banner);
|
|
14777
|
+
}, 4e3);
|
|
14778
|
+
}
|
|
14779
|
+
/**
|
|
14572
14780
|
* Remove all overlay DOM elements and reset state.
|
|
14573
14781
|
* @param {boolean} _committed - reserved for future use
|
|
14574
14782
|
*/
|
|
@@ -14983,6 +15191,16 @@ var _ACTIONS = {
|
|
|
14983
15191
|
if (!editable) return;
|
|
14984
15192
|
editable.focus();
|
|
14985
15193
|
document.execCommand("removeFormat");
|
|
15194
|
+
const sel = window.getSelection();
|
|
15195
|
+
if (sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed) {
|
|
15196
|
+
const range = sel.getRangeAt(0);
|
|
15197
|
+
const ancestor = range.commonAncestorContainer;
|
|
15198
|
+
const root = ancestor.nodeType === 1 ? ancestor : ancestor.parentElement;
|
|
15199
|
+
if (root) {
|
|
15200
|
+
const candidates = [root, ...root.querySelectorAll("[style]")];
|
|
15201
|
+
for (const el of candidates) if (el.hasAttribute("style") && range.intersectsNode(el)) el.removeAttribute("style");
|
|
15202
|
+
}
|
|
15203
|
+
}
|
|
14986
15204
|
ctx.invoke("editor.afterCommand");
|
|
14987
15205
|
},
|
|
14988
15206
|
inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
|
|
@@ -15096,6 +15314,7 @@ var BubbleToolbar = class {
|
|
|
15096
15314
|
}
|
|
15097
15315
|
document.body.appendChild(el);
|
|
15098
15316
|
this._el = el;
|
|
15317
|
+
this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
|
|
15099
15318
|
}
|
|
15100
15319
|
_buildColorPicker() {
|
|
15101
15320
|
const picker = document.createElement("div");
|
|
@@ -15181,8 +15400,13 @@ var BubbleToolbar = class {
|
|
|
15181
15400
|
editable.focus();
|
|
15182
15401
|
const sel = window.getSelection();
|
|
15183
15402
|
sel.removeAllRanges();
|
|
15184
|
-
|
|
15185
|
-
|
|
15403
|
+
try {
|
|
15404
|
+
sel.addRange(this._savedRange.cloneRange());
|
|
15405
|
+
} catch (_) {
|
|
15406
|
+
return;
|
|
15407
|
+
}
|
|
15408
|
+
const cmd = type === "hiliteColor" ? "hiliteColor" : type;
|
|
15409
|
+
if (!document.execCommand(cmd, false, color) && cmd === "hiliteColor") document.execCommand("backColor", false, color);
|
|
15186
15410
|
this.context.invoke("editor.afterCommand");
|
|
15187
15411
|
const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
|
|
15188
15412
|
const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
|
|
@@ -15217,8 +15441,8 @@ var BubbleToolbar = class {
|
|
|
15217
15441
|
this._closeColorPicker();
|
|
15218
15442
|
}
|
|
15219
15443
|
_syncActive() {
|
|
15220
|
-
if (!this.
|
|
15221
|
-
this.
|
|
15444
|
+
if (!this._btnCache) return;
|
|
15445
|
+
this._btnCache.forEach((btn) => {
|
|
15222
15446
|
const activeFn = _ACTIVE[btn.dataset.name];
|
|
15223
15447
|
btn.classList.toggle("an-active", !!(activeFn && activeFn()));
|
|
15224
15448
|
});
|
|
@@ -15365,14 +15589,23 @@ var Mention = class {
|
|
|
15365
15589
|
const el = document.createElement("div");
|
|
15366
15590
|
el.className = "an-mention-dropdown";
|
|
15367
15591
|
el.setAttribute("role", "listbox");
|
|
15592
|
+
el.addEventListener("mousedown", (e) => e.preventDefault());
|
|
15593
|
+
el.addEventListener("click", (e) => {
|
|
15594
|
+
const item = e.target.closest(".an-mention-item");
|
|
15595
|
+
if (item) this._select(+item.dataset.index);
|
|
15596
|
+
});
|
|
15597
|
+
el.addEventListener("mousemove", (e) => {
|
|
15598
|
+
const item = e.target.closest(".an-mention-item");
|
|
15599
|
+
if (item) this._highlightItem(+item.dataset.index);
|
|
15600
|
+
});
|
|
15368
15601
|
document.body.appendChild(el);
|
|
15369
15602
|
this._dropdown = el;
|
|
15370
15603
|
}
|
|
15371
15604
|
_renderItems(items) {
|
|
15372
15605
|
const dd = this._dropdown;
|
|
15373
|
-
dd.innerHTML = "";
|
|
15374
15606
|
this._items = items.slice(0, this._cfg.maxResults);
|
|
15375
15607
|
this._activeIndex = this._items.length > 0 ? 0 : -1;
|
|
15608
|
+
const frag = document.createDocumentFragment();
|
|
15376
15609
|
this._items.forEach((item, i) => {
|
|
15377
15610
|
const li = document.createElement("div");
|
|
15378
15611
|
li.className = "an-mention-item";
|
|
@@ -15388,10 +15621,10 @@ var Mention = class {
|
|
|
15388
15621
|
const label = document.createElement("span");
|
|
15389
15622
|
label.textContent = item.label;
|
|
15390
15623
|
li.appendChild(label);
|
|
15391
|
-
|
|
15392
|
-
li.addEventListener("click", () => this._select(i));
|
|
15393
|
-
dd.appendChild(li);
|
|
15624
|
+
frag.appendChild(li);
|
|
15394
15625
|
});
|
|
15626
|
+
dd.innerHTML = "";
|
|
15627
|
+
dd.appendChild(frag);
|
|
15395
15628
|
this._highlightItem(this._activeIndex);
|
|
15396
15629
|
}
|
|
15397
15630
|
_highlightItem(index) {
|