autumnnote 1.2.0 → 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/README.md +1 -1
- package/dist/autumnnote.es.js +343 -108
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.umd.js +314 -92
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/image/banner.png +0 -0
- package/dist/image/favicon.ico +0 -0
- 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 +28 -7
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);
|
|
@@ -5840,6 +5973,7 @@ var Toolbar = class {
|
|
|
5840
5973
|
const openPopup = () => {
|
|
5841
5974
|
isOpen = true;
|
|
5842
5975
|
const rect = btn.getBoundingClientRect();
|
|
5976
|
+
popup.style.visibility = "hidden";
|
|
5843
5977
|
popup.style.display = "block";
|
|
5844
5978
|
const pw = popup.offsetWidth;
|
|
5845
5979
|
const ph = popup.offsetHeight;
|
|
@@ -5849,13 +5983,12 @@ var Toolbar = class {
|
|
|
5849
5983
|
if (top + ph > window.innerHeight - 8) top = rect.top - ph - 4;
|
|
5850
5984
|
popup.style.left = `${left}px`;
|
|
5851
5985
|
popup.style.top = `${top}px`;
|
|
5986
|
+
popup.style.visibility = "";
|
|
5852
5987
|
btn.setAttribute("aria-expanded", "true");
|
|
5853
5988
|
};
|
|
5854
5989
|
const closePopup = () => {
|
|
5855
5990
|
isOpen = false;
|
|
5856
5991
|
popup.style.display = "none";
|
|
5857
|
-
popup.style.top = "";
|
|
5858
|
-
popup.style.left = "";
|
|
5859
5992
|
btn.setAttribute("aria-expanded", "false");
|
|
5860
5993
|
setHighlight(0, 0);
|
|
5861
5994
|
};
|
|
@@ -5882,9 +6015,11 @@ var Toolbar = class {
|
|
|
5882
6015
|
const d5 = on(document, "click", () => {
|
|
5883
6016
|
if (isOpen) closePopup();
|
|
5884
6017
|
});
|
|
5885
|
-
this._disposers.push(d1, d2, d3, d4, d5)
|
|
6018
|
+
this._disposers.push(d1, d2, d3, d4, d5, () => {
|
|
6019
|
+
if (popup.parentNode) popup.parentNode.removeChild(popup);
|
|
6020
|
+
});
|
|
5886
6021
|
wrap.appendChild(btn);
|
|
5887
|
-
|
|
6022
|
+
document.body.appendChild(popup);
|
|
5888
6023
|
return wrap;
|
|
5889
6024
|
}
|
|
5890
6025
|
/**
|
|
@@ -6159,6 +6294,13 @@ var Toolbar = class {
|
|
|
6159
6294
|
return _faPageLevelReady;
|
|
6160
6295
|
}
|
|
6161
6296
|
refresh() {
|
|
6297
|
+
if (this._refreshRaf) cancelAnimationFrame(this._refreshRaf);
|
|
6298
|
+
this._refreshRaf = requestAnimationFrame(() => {
|
|
6299
|
+
this._refreshRaf = null;
|
|
6300
|
+
this._doRefresh();
|
|
6301
|
+
});
|
|
6302
|
+
}
|
|
6303
|
+
_doRefresh() {
|
|
6162
6304
|
if (!this.el) return;
|
|
6163
6305
|
const btnMap = this._btnMap || /* @__PURE__ */ new Map();
|
|
6164
6306
|
this.el.querySelectorAll("button[data-btn]").forEach((btn) => {
|
|
@@ -6336,7 +6478,7 @@ var Statusbar = class {
|
|
|
6336
6478
|
}
|
|
6337
6479
|
update() {
|
|
6338
6480
|
if (!this._wordCountEl || !this._charCountEl) return;
|
|
6339
|
-
const text = this.context.layoutInfo.editable.
|
|
6481
|
+
const text = this.context.layoutInfo.editable.textContent || "";
|
|
6340
6482
|
const words = _countWords(text);
|
|
6341
6483
|
const chars = text.replace(/\n/g, "").length;
|
|
6342
6484
|
const maxWords = this.options.maxWords || 0;
|
|
@@ -6531,7 +6673,7 @@ var Clipboard = class {
|
|
|
6531
6673
|
event.preventDefault();
|
|
6532
6674
|
const raw = clipboardData.getData("text/html");
|
|
6533
6675
|
const isWordContent = /<[a-z]+:[a-z]/i.test(raw) || /class="Mso/i.test(raw) || /\bmso-/i.test(raw);
|
|
6534
|
-
const isSocialContent =
|
|
6676
|
+
const isSocialContent = /class="[^"]*\b(?:x[a-z0-9]{6,}|r-[a-z0-9]{3,})\b/.test(raw);
|
|
6535
6677
|
let html = raw;
|
|
6536
6678
|
if (isWordContent) html = this._cleanWordHtml(html);
|
|
6537
6679
|
else if (isSocialContent) html = this._cleanSocialHtml(html);
|
|
@@ -6633,7 +6775,7 @@ var Clipboard = class {
|
|
|
6633
6775
|
*/
|
|
6634
6776
|
_dataUrlToBlob(dataUrl) {
|
|
6635
6777
|
const [header, b64] = dataUrl.split(",");
|
|
6636
|
-
const mime = header.match(/:(.*?);/)[1];
|
|
6778
|
+
const mime = header.match(/:(.*?);/)?.[1] ?? "image/png";
|
|
6637
6779
|
const binary = atob(b64);
|
|
6638
6780
|
const arr = new Uint8Array(binary.length);
|
|
6639
6781
|
for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i);
|
|
@@ -7639,6 +7781,7 @@ var ImageResizer = class {
|
|
|
7639
7781
|
_select(img) {
|
|
7640
7782
|
if (this._activeImg && this._activeImg !== img) this._activeImg.classList.remove("an-image-selected");
|
|
7641
7783
|
this._activeImg = img;
|
|
7784
|
+
this._lastOverlayPos = null;
|
|
7642
7785
|
img.classList.add("an-image-selected");
|
|
7643
7786
|
this._updateOverlayPosition();
|
|
7644
7787
|
this._overlay.style.display = "block";
|
|
@@ -7662,6 +7805,14 @@ var ImageResizer = class {
|
|
|
7662
7805
|
const offsetParent = this._overlay.offsetParent || this._container;
|
|
7663
7806
|
const containerRect = offsetParent.getBoundingClientRect();
|
|
7664
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
|
+
};
|
|
7665
7816
|
const left = rect.left - containerRect.left + offsetParent.scrollLeft;
|
|
7666
7817
|
const top = rect.top - containerRect.top + offsetParent.scrollTop;
|
|
7667
7818
|
this._overlay.style.left = `${left}px`;
|
|
@@ -13360,7 +13511,7 @@ var ContextMenu = class {
|
|
|
13360
13511
|
cells.forEach((cell) => {
|
|
13361
13512
|
cell.classList.toggle("active", +cell.dataset.row <= rows && +cell.dataset.col <= cols);
|
|
13362
13513
|
});
|
|
13363
|
-
labelEl.textContent = rows && cols ? `${
|
|
13514
|
+
labelEl.textContent = rows && cols ? `${rows} × ${cols}` : this.context.locale.contextMenu.table || "Insert Table";
|
|
13364
13515
|
};
|
|
13365
13516
|
panel.appendChild(gridEl);
|
|
13366
13517
|
panel.appendChild(labelEl);
|
|
@@ -14000,7 +14151,6 @@ var FindReplace = class {
|
|
|
14000
14151
|
if (!editable) return;
|
|
14001
14152
|
const rawMatches = this._findRawMatches(query, editable);
|
|
14002
14153
|
if (rawMatches.length === 0) return;
|
|
14003
|
-
this._currentIndex = 0;
|
|
14004
14154
|
for (let i = rawMatches.length - 1; i >= 0; i--) {
|
|
14005
14155
|
const { node, start, end } = rawMatches[i];
|
|
14006
14156
|
try {
|
|
@@ -14017,6 +14167,7 @@ var FindReplace = class {
|
|
|
14017
14167
|
}
|
|
14018
14168
|
this._matches.reverse();
|
|
14019
14169
|
this._matches = this._matches.filter((m) => m.mark);
|
|
14170
|
+
this._currentIndex = 0;
|
|
14020
14171
|
if (this._matches.length === 0) return;
|
|
14021
14172
|
if (this._matches[0] && this._matches[0].mark) {
|
|
14022
14173
|
this._matches[0].mark.className = "an-highlight an-highlight-current";
|
|
@@ -14042,12 +14193,13 @@ var FindReplace = class {
|
|
|
14042
14193
|
this._lastCaseSensitive = this._caseSensitive;
|
|
14043
14194
|
}
|
|
14044
14195
|
const re = this._queryRegex;
|
|
14196
|
+
const MAX_RESULTS = 500;
|
|
14045
14197
|
const walker = document.createTreeWalker(root, 4);
|
|
14046
14198
|
let node;
|
|
14047
|
-
while (node = walker.nextNode()) {
|
|
14199
|
+
while ((node = walker.nextNode()) && results.length < MAX_RESULTS) {
|
|
14048
14200
|
re.lastIndex = 0;
|
|
14049
14201
|
let m;
|
|
14050
|
-
while ((m = re.exec(node.textContent)) !== null) results.push({
|
|
14202
|
+
while ((m = re.exec(node.textContent)) !== null && results.length < MAX_RESULTS) results.push({
|
|
14051
14203
|
node,
|
|
14052
14204
|
start: m.index,
|
|
14053
14205
|
end: m.index + m[0].length
|
|
@@ -14134,7 +14286,7 @@ var FindReplace = class {
|
|
|
14134
14286
|
const total = this._matches.length;
|
|
14135
14287
|
if (total === 0) {
|
|
14136
14288
|
const query = this._findInput ? this._findInput.value : "";
|
|
14137
|
-
this._counterEl.textContent = query ?
|
|
14289
|
+
this._counterEl.textContent = query ? this.context.locale.findReplace.noResults : "";
|
|
14138
14290
|
} else this._counterEl.textContent = `${this._currentIndex + 1} / ${total}`;
|
|
14139
14291
|
}
|
|
14140
14292
|
};
|
|
@@ -14333,17 +14485,33 @@ var ImageCropOverlay = class {
|
|
|
14333
14485
|
e.preventDefault();
|
|
14334
14486
|
e.stopPropagation();
|
|
14335
14487
|
this._startHandleDrag(e, id);
|
|
14336
|
-
}))
|
|
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 }));
|
|
14337
14496
|
this._handles[id] = h;
|
|
14338
14497
|
cropBox.appendChild(h);
|
|
14339
14498
|
});
|
|
14340
14499
|
this._disposers.push(on(cropBox, "mousedown", (e) => {
|
|
14341
|
-
if (e.target
|
|
14342
|
-
if (e.target
|
|
14500
|
+
if (e.target.classList.contains("an-crop-handle")) return;
|
|
14501
|
+
if (e.target !== cropBox && e.target !== grid) return;
|
|
14343
14502
|
e.preventDefault();
|
|
14344
14503
|
e.stopPropagation();
|
|
14345
14504
|
this._startBoxMove(e);
|
|
14346
|
-
}))
|
|
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 }));
|
|
14347
14515
|
const infoEl = document.createElement("div");
|
|
14348
14516
|
infoEl.className = "an-crop-info";
|
|
14349
14517
|
infoEl.style.cssText = `
|
|
@@ -14501,15 +14669,26 @@ var ImageCropOverlay = class {
|
|
|
14501
14669
|
* @param {(e: MouseEvent) => void} onMove
|
|
14502
14670
|
*/
|
|
14503
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
|
+
};
|
|
14504
14679
|
const cleanup = () => {
|
|
14505
14680
|
document.removeEventListener("mousemove", onMove);
|
|
14506
14681
|
document.removeEventListener("mouseup", cleanup);
|
|
14682
|
+
document.removeEventListener("touchmove", onTouchMove);
|
|
14683
|
+
document.removeEventListener("touchend", cleanup);
|
|
14507
14684
|
document.body.style.userSelect = "";
|
|
14508
14685
|
document.body.style.cursor = "";
|
|
14509
14686
|
};
|
|
14510
14687
|
document.body.style.userSelect = "none";
|
|
14511
14688
|
document.addEventListener("mousemove", onMove);
|
|
14512
14689
|
document.addEventListener("mouseup", cleanup, { once: true });
|
|
14690
|
+
document.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
14691
|
+
document.addEventListener("touchend", cleanup, { once: true });
|
|
14513
14692
|
}
|
|
14514
14693
|
_bindEsc() {
|
|
14515
14694
|
const handler = (e) => {
|
|
@@ -14550,7 +14729,7 @@ var ImageCropOverlay = class {
|
|
|
14550
14729
|
height: natH
|
|
14551
14730
|
}, w, h);
|
|
14552
14731
|
if (!canvas) {
|
|
14553
|
-
|
|
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.");
|
|
14554
14733
|
this._close(false);
|
|
14555
14734
|
return;
|
|
14556
14735
|
}
|
|
@@ -14567,6 +14746,37 @@ var ImageCropOverlay = class {
|
|
|
14567
14746
|
this.context.invoke("imageResizer.updateOverlay");
|
|
14568
14747
|
}
|
|
14569
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
|
+
/**
|
|
14570
14780
|
* Remove all overlay DOM elements and reset state.
|
|
14571
14781
|
* @param {boolean} _committed - reserved for future use
|
|
14572
14782
|
*/
|
|
@@ -14981,6 +15191,16 @@ var _ACTIONS = {
|
|
|
14981
15191
|
if (!editable) return;
|
|
14982
15192
|
editable.focus();
|
|
14983
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
|
+
}
|
|
14984
15204
|
ctx.invoke("editor.afterCommand");
|
|
14985
15205
|
},
|
|
14986
15206
|
inlineCode: (ctx) => ctx.invoke("editor.inlineCode")
|
|
@@ -15094,6 +15314,7 @@ var BubbleToolbar = class {
|
|
|
15094
15314
|
}
|
|
15095
15315
|
document.body.appendChild(el);
|
|
15096
15316
|
this._el = el;
|
|
15317
|
+
this._btnCache = Array.from(el.querySelectorAll(".an-bubble-btn"));
|
|
15097
15318
|
}
|
|
15098
15319
|
_buildColorPicker() {
|
|
15099
15320
|
const picker = document.createElement("div");
|
|
@@ -15179,8 +15400,13 @@ var BubbleToolbar = class {
|
|
|
15179
15400
|
editable.focus();
|
|
15180
15401
|
const sel = window.getSelection();
|
|
15181
15402
|
sel.removeAllRanges();
|
|
15182
|
-
|
|
15183
|
-
|
|
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);
|
|
15184
15410
|
this.context.invoke("editor.afterCommand");
|
|
15185
15411
|
const name = type === "hiliteColor" ? "hiliteColor" : "foreColor";
|
|
15186
15412
|
const btn = this._el && this._el.querySelector(`[data-name="${name}"]`);
|
|
@@ -15215,8 +15441,8 @@ var BubbleToolbar = class {
|
|
|
15215
15441
|
this._closeColorPicker();
|
|
15216
15442
|
}
|
|
15217
15443
|
_syncActive() {
|
|
15218
|
-
if (!this.
|
|
15219
|
-
this.
|
|
15444
|
+
if (!this._btnCache) return;
|
|
15445
|
+
this._btnCache.forEach((btn) => {
|
|
15220
15446
|
const activeFn = _ACTIVE[btn.dataset.name];
|
|
15221
15447
|
btn.classList.toggle("an-active", !!(activeFn && activeFn()));
|
|
15222
15448
|
});
|
|
@@ -15363,14 +15589,23 @@ var Mention = class {
|
|
|
15363
15589
|
const el = document.createElement("div");
|
|
15364
15590
|
el.className = "an-mention-dropdown";
|
|
15365
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
|
+
});
|
|
15366
15601
|
document.body.appendChild(el);
|
|
15367
15602
|
this._dropdown = el;
|
|
15368
15603
|
}
|
|
15369
15604
|
_renderItems(items) {
|
|
15370
15605
|
const dd = this._dropdown;
|
|
15371
|
-
dd.innerHTML = "";
|
|
15372
15606
|
this._items = items.slice(0, this._cfg.maxResults);
|
|
15373
15607
|
this._activeIndex = this._items.length > 0 ? 0 : -1;
|
|
15608
|
+
const frag = document.createDocumentFragment();
|
|
15374
15609
|
this._items.forEach((item, i) => {
|
|
15375
15610
|
const li = document.createElement("div");
|
|
15376
15611
|
li.className = "an-mention-item";
|
|
@@ -15386,10 +15621,10 @@ var Mention = class {
|
|
|
15386
15621
|
const label = document.createElement("span");
|
|
15387
15622
|
label.textContent = item.label;
|
|
15388
15623
|
li.appendChild(label);
|
|
15389
|
-
|
|
15390
|
-
li.addEventListener("click", () => this._select(i));
|
|
15391
|
-
dd.appendChild(li);
|
|
15624
|
+
frag.appendChild(li);
|
|
15392
15625
|
});
|
|
15626
|
+
dd.innerHTML = "";
|
|
15627
|
+
dd.appendChild(frag);
|
|
15393
15628
|
this._highlightItem(this._activeIndex);
|
|
15394
15629
|
}
|
|
15395
15630
|
_highlightItem(index) {
|