@atri-editor/core 0.3.0 → 0.5.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/CHANGELOG.md +155 -0
- package/dist/AtriEditor.d.ts +20 -1
- package/dist/AtriEditor.d.ts.map +1 -1
- package/dist/AtriEditorElement.d.ts +26 -2
- package/dist/AtriEditorElement.d.ts.map +1 -1
- package/dist/ai/AICommandMenu.d.ts +1 -0
- package/dist/ai/AICommandMenu.d.ts.map +1 -1
- package/dist/ai/AIInterceptor.d.ts.map +1 -1
- package/dist/ai/AIService.d.ts.map +1 -1
- package/dist/core/CoreEditor.d.ts +9 -0
- package/dist/core/CoreEditor.d.ts.map +1 -1
- package/dist/core/I18nManager.d.ts +25 -0
- package/dist/core/I18nManager.d.ts.map +1 -1
- package/dist/core/LinkPanel.d.ts +1 -0
- package/dist/core/LinkPanel.d.ts.map +1 -1
- package/dist/core/MarkdownService.d.ts +12 -1
- package/dist/core/MarkdownService.d.ts.map +1 -1
- package/dist/core/StyleOptionPanel.d.ts +73 -0
- package/dist/core/StyleOptionPanel.d.ts.map +1 -0
- package/dist/core/TableGridPanel.d.ts +1 -0
- package/dist/core/TableGridPanel.d.ts.map +1 -1
- package/dist/core/TableMenu.d.ts +1 -0
- package/dist/core/TableMenu.d.ts.map +1 -1
- package/dist/core/ThemeManager.d.ts.map +1 -1
- package/dist/core/ToolbarManager.d.ts +50 -1
- package/dist/core/ToolbarManager.d.ts.map +1 -1
- package/dist/core/ToolbarMenuPanel.d.ts +58 -0
- package/dist/core/ToolbarMenuPanel.d.ts.map +1 -0
- package/dist/core/bubble-toolbar.d.ts +8 -2
- package/dist/core/bubble-toolbar.d.ts.map +1 -1
- package/dist/core/floating-panel.d.ts +24 -1
- package/dist/core/floating-panel.d.ts.map +1 -1
- package/dist/core/icons.d.ts.map +1 -1
- package/dist/core/markdown-losses.d.ts +26 -0
- package/dist/core/markdown-losses.d.ts.map +1 -0
- package/dist/core/style-presets.d.ts +47 -0
- package/dist/core/style-presets.d.ts.map +1 -0
- package/dist/extensions/Attachment.d.ts.map +1 -1
- package/dist/extensions/AttachmentLink.d.ts.map +1 -1
- package/dist/extensions/attachment-attrs.d.ts.map +1 -1
- package/dist/extensions/formatting.d.ts +24 -0
- package/dist/extensions/formatting.d.ts.map +1 -0
- package/dist/extensions/markdown-inline-html.d.ts +73 -0
- package/dist/extensions/markdown-inline-html.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1529 -199
- package/dist/index.js.map +1 -1
- package/dist/media/InsertPanel.d.ts +1 -0
- package/dist/media/InsertPanel.d.ts.map +1 -1
- package/dist/media/MediaRuntime.d.ts +13 -0
- package/dist/media/MediaRuntime.d.ts.map +1 -1
- package/dist/media/MediaStatusStrip.d.ts.map +1 -1
- package/dist/media/Uploader.d.ts.map +1 -1
- package/dist/style.css +186 -0
- package/dist/types/ai.d.ts +5 -0
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/editor.d.ts +23 -2
- package/dist/types/editor.d.ts.map +1 -1
- package/dist/types/markdown.d.ts +32 -0
- package/dist/types/markdown.d.ts.map +1 -1
- package/dist/types/media.d.ts +5 -0
- package/dist/types/media.d.ts.map +1 -1
- package/dist/utils/escape.d.ts +30 -0
- package/dist/utils/escape.d.ts.map +1 -0
- package/dist/utils/formatting.d.ts +18 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/markdown.d.ts +5 -3
- package/dist/utils/markdown.d.ts.map +1 -1
- package/dist/utils/safe-call.d.ts +15 -0
- package/dist/utils/safe-call.d.ts.map +1 -0
- package/dist/utils/url.d.ts +20 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/package.json +16 -12
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Node, mergeAttributes, Extension, isNodeSelection, Editor, getHTMLFromFragment, elementFromString, getTextBetween } from "@tiptap/core";
|
|
1
|
+
import { Node, mergeAttributes, Extension, isNodeSelection, Editor, getExtensionField, getHTMLFromFragment, elementFromString, getTextBetween } from "@tiptap/core";
|
|
2
2
|
import StarterKit from "@tiptap/starter-kit";
|
|
3
3
|
import { Markdown } from "@tiptap/markdown";
|
|
4
4
|
import { Placeholder } from "@tiptap/extensions";
|
|
@@ -8,9 +8,13 @@ import { TaskList, TaskItem } from "@tiptap/extension-list";
|
|
|
8
8
|
import { TableRow, TableHeader, TableCell, Table } from "@tiptap/extension-table";
|
|
9
9
|
import Image from "@tiptap/extension-image";
|
|
10
10
|
import { NodeSelection, Plugin, PluginKey } from "@tiptap/pm/state";
|
|
11
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
12
|
+
import Subscript from "@tiptap/extension-subscript";
|
|
13
|
+
import Superscript from "@tiptap/extension-superscript";
|
|
14
|
+
import { TextStyle, Color, BackgroundColor, FontFamily, FontSize } from "@tiptap/extension-text-style";
|
|
11
15
|
import { Node as Node$1, DOMParser } from "@tiptap/pm/model";
|
|
12
16
|
import i18next from "i18next";
|
|
13
|
-
import {
|
|
17
|
+
import { offset, flip, shift, autoUpdate, computePosition } from "@floating-ui/dom";
|
|
14
18
|
function isBuiltinTableActive(editor) {
|
|
15
19
|
var _a;
|
|
16
20
|
const spec = (_a = editor.schema.nodes.table) == null ? void 0 : _a.spec;
|
|
@@ -126,6 +130,32 @@ function validateFile(file, options) {
|
|
|
126
130
|
function isImageFile(file) {
|
|
127
131
|
return (file.type || "").toLowerCase().startsWith("image/");
|
|
128
132
|
}
|
|
133
|
+
const HREF_SCHEMES = ["http", "https", "mailto", "tel", "ftp", "ftps"];
|
|
134
|
+
const MEDIA_SCHEMES = ["http", "https", "data", "blob"];
|
|
135
|
+
function stripUrlNoise(value) {
|
|
136
|
+
const collapsed = value.replace(/[\t\n\r]/g, "");
|
|
137
|
+
let start = 0;
|
|
138
|
+
let end = collapsed.length;
|
|
139
|
+
while (start < end && isStripped(collapsed.charCodeAt(start))) start += 1;
|
|
140
|
+
while (end > start && isStripped(collapsed.charCodeAt(end - 1))) end -= 1;
|
|
141
|
+
return collapsed.slice(start, end);
|
|
142
|
+
}
|
|
143
|
+
function isStripped(code) {
|
|
144
|
+
return code <= 32 || code === 127;
|
|
145
|
+
}
|
|
146
|
+
const SCHEME_PREFIX = /^([a-z][a-z0-9+.-]*):/i;
|
|
147
|
+
function safeHref(value, schemes = HREF_SCHEMES) {
|
|
148
|
+
var _a;
|
|
149
|
+
if (typeof value !== "string") return null;
|
|
150
|
+
const url = stripUrlNoise(value);
|
|
151
|
+
if (!url) return null;
|
|
152
|
+
const scheme = (_a = SCHEME_PREFIX.exec(url)) == null ? void 0 : _a[1];
|
|
153
|
+
if (!scheme) return url;
|
|
154
|
+
return schemes.includes(scheme.toLowerCase()) ? url : null;
|
|
155
|
+
}
|
|
156
|
+
function safeMediaSrc(value) {
|
|
157
|
+
return safeHref(value, MEDIA_SCHEMES);
|
|
158
|
+
}
|
|
129
159
|
function isAttachmentForm(name) {
|
|
130
160
|
return name === "attachment" || name === "attachmentLink";
|
|
131
161
|
}
|
|
@@ -150,6 +180,12 @@ function insertAttachmentContent(props, typeName, options) {
|
|
|
150
180
|
const { state, commands, tr, dispatch } = props;
|
|
151
181
|
const target = mediaInsertTarget(state.selection);
|
|
152
182
|
const anchor = typeof target === "number" ? target : target.to;
|
|
183
|
+
if (safeHref(options.src) === null) {
|
|
184
|
+
console.warn(
|
|
185
|
+
`[Atri Editor] insertAttachment rejected: "${options.src}" is not a clickable address.`
|
|
186
|
+
);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
153
189
|
if (!commands.insertContentAt(target, {
|
|
154
190
|
type: typeName,
|
|
155
191
|
attrs: {
|
|
@@ -182,7 +218,10 @@ function attachmentAttributes() {
|
|
|
182
218
|
return {
|
|
183
219
|
src: {
|
|
184
220
|
default: null,
|
|
185
|
-
|
|
221
|
+
// 粘贴进来的 data-src 也洗一遍:不洗的话它绕开 AttachmentLink 那处 getAttrs,
|
|
222
|
+
// 直接落进节点属性,再由渲染层的 safeHref 挡掉——属性里仍留着地址,
|
|
223
|
+
// 而 setNodeMarkup / JSON 回灌都不经过渲染层
|
|
224
|
+
parseHTML: (element) => safeMediaSrc(element.getAttribute("data-src")),
|
|
186
225
|
renderHTML: (attributes) => dataAttr("data-src", attributes.src)
|
|
187
226
|
},
|
|
188
227
|
name: {
|
|
@@ -332,7 +371,19 @@ const Attachment = Node.create({
|
|
|
332
371
|
["span", { class: "atri-attachment-icon", "aria-hidden": "true" }]
|
|
333
372
|
];
|
|
334
373
|
const body = [
|
|
335
|
-
|
|
374
|
+
// src 有五条来路(宿主 insertAttachment、上传响应、外部粘贴、setNodeMarkup、
|
|
375
|
+
// setJSON 回灌),逐条在入口校验拦不全,渲染层是唯一都经过的地方。
|
|
376
|
+
// 不过 dataAttr 的"空值即省略键"是关键:传 null 会被 mergeAttributes 留下,
|
|
377
|
+
// 序列化出来就成了 href="null",反而多一枚假链接
|
|
378
|
+
[
|
|
379
|
+
"a",
|
|
380
|
+
{
|
|
381
|
+
class: "atri-attachment-name",
|
|
382
|
+
...dataAttr("href", safeHref(src)),
|
|
383
|
+
rel: "noopener noreferrer"
|
|
384
|
+
},
|
|
385
|
+
label
|
|
386
|
+
]
|
|
336
387
|
];
|
|
337
388
|
if (size) {
|
|
338
389
|
body.push(["span", { class: "atri-attachment-size" }, formatFileSize(size)]);
|
|
@@ -444,8 +495,9 @@ const AttachmentLink = Node.create({
|
|
|
444
495
|
{
|
|
445
496
|
tag: "a[data-atri-attachment-link]",
|
|
446
497
|
priority: 100,
|
|
447
|
-
// 手写或从别处粘来的 <a> 只带 href,没有 data-src
|
|
448
|
-
|
|
498
|
+
// 手写或从别处粘来的 <a> 只带 href,没有 data-src:认下来,别留一枚空链接。
|
|
499
|
+
// data-src 那条来路在 attachmentAttributes().src.parseHTML 里已经洗过
|
|
500
|
+
getAttrs: (element) => element.getAttribute("data-src") ? {} : { src: safeHref(element.getAttribute("href")) }
|
|
449
501
|
}
|
|
450
502
|
];
|
|
451
503
|
},
|
|
@@ -456,9 +508,11 @@ const AttachmentLink = Node.create({
|
|
|
456
508
|
mergeAttributes(HTMLAttributes, {
|
|
457
509
|
"data-atri-attachment-link": "",
|
|
458
510
|
class: "atri-attachment-link",
|
|
459
|
-
href
|
|
511
|
+
// 协议不合法就整枚 href 都不写,退化成一枚没有地址的文字,
|
|
512
|
+
// 而不是留着 href="javascript:…" 等用户点
|
|
513
|
+
...dataAttr("href", safeHref(src)),
|
|
460
514
|
// download 让同源地址点击即下载(跨域浏览器会退化成导航);
|
|
461
|
-
//
|
|
515
|
+
// 值为 null 时 PM 序列化会跳过,没名字就只剩普通链接
|
|
462
516
|
download: name,
|
|
463
517
|
rel: "noopener noreferrer"
|
|
464
518
|
}),
|
|
@@ -562,6 +616,204 @@ function createMediaExtensions(media, runtime) {
|
|
|
562
616
|
MediaFiles.configure({ runtime: runtime ?? null })
|
|
563
617
|
];
|
|
564
618
|
}
|
|
619
|
+
const TEXT_CHARS = {
|
|
620
|
+
"&": "&",
|
|
621
|
+
"<": "<",
|
|
622
|
+
">": ">"
|
|
623
|
+
};
|
|
624
|
+
const QUOTE_CHARS = {
|
|
625
|
+
'"': """,
|
|
626
|
+
"'": "'"
|
|
627
|
+
};
|
|
628
|
+
function replaceAll(value, table, pattern) {
|
|
629
|
+
return value.replace(pattern, (char) => table[char] ?? char);
|
|
630
|
+
}
|
|
631
|
+
function escapeHtml(value) {
|
|
632
|
+
return replaceAll(value, TEXT_CHARS, /[&<>]/g);
|
|
633
|
+
}
|
|
634
|
+
function escapeQuotes(value) {
|
|
635
|
+
return replaceAll(value, QUOTE_CHARS, /["']/g);
|
|
636
|
+
}
|
|
637
|
+
const DECODED = {
|
|
638
|
+
"&": "&",
|
|
639
|
+
"<": "<",
|
|
640
|
+
">": ">",
|
|
641
|
+
""": '"',
|
|
642
|
+
"'": "'"
|
|
643
|
+
};
|
|
644
|
+
function decodeEntities(value) {
|
|
645
|
+
return value.replace(/&(?:amp|lt|gt|quot|#39);/g, (entity) => DECODED[entity] ?? entity);
|
|
646
|
+
}
|
|
647
|
+
const STYLE_TO_ATTR = {
|
|
648
|
+
color: "color",
|
|
649
|
+
"background-color": "backgroundColor",
|
|
650
|
+
"font-size": "fontSize",
|
|
651
|
+
"font-family": "fontFamily"
|
|
652
|
+
};
|
|
653
|
+
const ATTR_TO_STYLE = {
|
|
654
|
+
color: "color",
|
|
655
|
+
backgroundColor: "background-color",
|
|
656
|
+
fontSize: "font-size",
|
|
657
|
+
fontFamily: "font-family"
|
|
658
|
+
};
|
|
659
|
+
function hasValue(value) {
|
|
660
|
+
return typeof value === "string" && value.trim() !== "";
|
|
661
|
+
}
|
|
662
|
+
function styleAttrToAttrs(styleText) {
|
|
663
|
+
const attrs = {};
|
|
664
|
+
for (const declaration of styleText.split(";")) {
|
|
665
|
+
const separator = declaration.indexOf(":");
|
|
666
|
+
if (separator < 1) continue;
|
|
667
|
+
const prop = declaration.slice(0, separator).trim().toLowerCase();
|
|
668
|
+
const value = declaration.slice(separator + 1).trim();
|
|
669
|
+
const mapped = STYLE_TO_ATTR[prop];
|
|
670
|
+
if (mapped && hasValue(value)) attrs[mapped] = value;
|
|
671
|
+
}
|
|
672
|
+
return attrs;
|
|
673
|
+
}
|
|
674
|
+
function attrsToStyleCss(attrs = {}) {
|
|
675
|
+
const declarations = [];
|
|
676
|
+
for (const attr of Object.keys(ATTR_TO_STYLE)) {
|
|
677
|
+
const value = attrs[attr];
|
|
678
|
+
if (hasValue(value)) declarations.push(`${ATTR_TO_STYLE[attr]}: ${value}`);
|
|
679
|
+
}
|
|
680
|
+
return declarations.join("; ");
|
|
681
|
+
}
|
|
682
|
+
function makeInlineHtmlReader(options) {
|
|
683
|
+
const { tokenName, pattern, begins, resolve } = options;
|
|
684
|
+
return {
|
|
685
|
+
markdownTokenName: tokenName,
|
|
686
|
+
markdownTokenizer: {
|
|
687
|
+
name: tokenName,
|
|
688
|
+
level: "inline",
|
|
689
|
+
start: (src) => {
|
|
690
|
+
let earliest = -1;
|
|
691
|
+
for (const needle of begins) {
|
|
692
|
+
const at = src.indexOf(needle);
|
|
693
|
+
if (at >= 0 && (earliest < 0 || at < earliest)) earliest = at;
|
|
694
|
+
}
|
|
695
|
+
return earliest;
|
|
696
|
+
},
|
|
697
|
+
tokenize: (src, _tokens, lexer) => {
|
|
698
|
+
const match = pattern.exec(src);
|
|
699
|
+
if (!match) return void 0;
|
|
700
|
+
return {
|
|
701
|
+
type: tokenName,
|
|
702
|
+
raw: match[0],
|
|
703
|
+
text: match[2] ?? "",
|
|
704
|
+
openAttrs: match[1] ?? "",
|
|
705
|
+
// 这一步是整个设计的关键:不在这里重分词,内层的 **粗体** 与转义就会
|
|
706
|
+
// 在 parse 阶段被当成字面文本,进而每导出一次转义翻一倍
|
|
707
|
+
tokens: lexer.inlineTokens(match[2] ?? "")
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
/**
|
|
712
|
+
* token.tokens 已经是分好词的内层,直接 parseInline。
|
|
713
|
+
*
|
|
714
|
+
* resolve 认不出属性时(`<span class="x">` 这种)照样解析内层、只是不套 mark——
|
|
715
|
+
* 把整个 token 丢给 parseInline 会让里面的文字一起消失。
|
|
716
|
+
*/
|
|
717
|
+
parseMarkdown: (token, helpers) => {
|
|
718
|
+
const content = helpers.parseInline(token.tokens ?? []);
|
|
719
|
+
const resolved = resolve(token);
|
|
720
|
+
return resolved ? helpers.applyMark(resolved.markName, content, resolved.attrs) : content;
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function emitAttrValue(value) {
|
|
725
|
+
return escapeQuotes(escapeHtml(value));
|
|
726
|
+
}
|
|
727
|
+
function renderStyleSpan(node, inner) {
|
|
728
|
+
const css = attrsToStyleCss(node.attrs);
|
|
729
|
+
if (!css) return inner;
|
|
730
|
+
return `<span style="${emitAttrValue(css)}">${inner}</span>`;
|
|
731
|
+
}
|
|
732
|
+
const StyledTextStyle = TextStyle.extend({
|
|
733
|
+
renderMarkdown: (node, helpers) => renderStyleSpan(node, helpers.renderChildren(node))
|
|
734
|
+
});
|
|
735
|
+
const StyledSubscript = Subscript.extend({
|
|
736
|
+
renderMarkdown: (_node, helpers) => `<sub>${helpers.renderChildren(_node)}</sub>`
|
|
737
|
+
});
|
|
738
|
+
const StyledSuperscript = Superscript.extend({
|
|
739
|
+
renderMarkdown: (_node, helpers) => `<sup>${helpers.renderChildren(_node)}</sup>`
|
|
740
|
+
});
|
|
741
|
+
const StyledHighlight = Highlight.configure({ multicolor: true }).extend({
|
|
742
|
+
renderMarkdown: (node, helpers) => {
|
|
743
|
+
var _a;
|
|
744
|
+
const inner = helpers.renderChildren(node);
|
|
745
|
+
const color = (_a = node.attrs) == null ? void 0 : _a.color;
|
|
746
|
+
if (typeof color !== "string" || color.trim() === "") return `==${inner}==`;
|
|
747
|
+
return `<mark data-color="${emitAttrValue(color)}" style="background-color: ${emitAttrValue(color)}; color: inherit">${inner}</mark>`;
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
const StyledSpanReader = Extension.create({
|
|
751
|
+
// Reader 也得有名有姓:Extension.create 不给 name 就会落成空串,
|
|
752
|
+
// 三个匿名扩展同名会撞上 tiptap 的 Duplicate extension names 警告
|
|
753
|
+
name: "atriStyledSpanReader",
|
|
754
|
+
...makeInlineHtmlReader({
|
|
755
|
+
tokenName: "atriStyledSpan",
|
|
756
|
+
// 非贪婪:我们的发出侧不会产出血缘嵌套的 span(同一个 mark 只出一个标签),
|
|
757
|
+
// 遇到外来的嵌套就退回内层文本,不抛
|
|
758
|
+
pattern: /^<span([^>]*)>([\s\S]*?)<\/span>/,
|
|
759
|
+
begins: ["<span"],
|
|
760
|
+
resolve: (token) => {
|
|
761
|
+
var _a;
|
|
762
|
+
const style = (_a = /(?:^|\s)style="([^"]*)"/i.exec(token.openAttrs ?? "")) == null ? void 0 : _a[1];
|
|
763
|
+
if (!style) return null;
|
|
764
|
+
const attrs = styleAttrToAttrs(decodeEntities(style));
|
|
765
|
+
return Object.keys(attrs).length > 0 ? { markName: "textStyle", attrs } : null;
|
|
766
|
+
}
|
|
767
|
+
})
|
|
768
|
+
});
|
|
769
|
+
const SubSupReader = Extension.create({
|
|
770
|
+
name: "atriInlineTagReader",
|
|
771
|
+
...makeInlineHtmlReader({
|
|
772
|
+
tokenName: "atriInlineTag",
|
|
773
|
+
pattern: /^<(sub|sup)>([\s\S]*?)<\/\1>/,
|
|
774
|
+
begins: ["<sub>", "<sup>"],
|
|
775
|
+
resolve: (token) => {
|
|
776
|
+
const tag = (token.openAttrs ?? "").toLowerCase();
|
|
777
|
+
if (tag !== "sub" && tag !== "sup") return null;
|
|
778
|
+
return { markName: tag === "sup" ? "superscript" : "subscript", attrs: {} };
|
|
779
|
+
}
|
|
780
|
+
})
|
|
781
|
+
});
|
|
782
|
+
const HighlightReader = Extension.create({
|
|
783
|
+
name: "atriStyledMarkReader",
|
|
784
|
+
...makeInlineHtmlReader({
|
|
785
|
+
tokenName: "atriStyledMark",
|
|
786
|
+
pattern: /^<mark([^>]*)>([\s\S]*?)<\/mark>/,
|
|
787
|
+
begins: ["<mark"],
|
|
788
|
+
resolve: (token) => {
|
|
789
|
+
var _a;
|
|
790
|
+
const color = (_a = /data-color="([^"]*)"/i.exec(token.openAttrs ?? "")) == null ? void 0 : _a[1];
|
|
791
|
+
if (!color) return null;
|
|
792
|
+
return { markName: "highlight", attrs: { color: decodeEntities(color) } };
|
|
793
|
+
}
|
|
794
|
+
})
|
|
795
|
+
});
|
|
796
|
+
function createFormattingExtensions(options = {}) {
|
|
797
|
+
const emit = options.inlineStyles !== "drop";
|
|
798
|
+
return [
|
|
799
|
+
emit ? StyledTextStyle : TextStyle,
|
|
800
|
+
Color,
|
|
801
|
+
BackgroundColor,
|
|
802
|
+
FontFamily,
|
|
803
|
+
FontSize,
|
|
804
|
+
emit ? StyledHighlight : Highlight.configure({ multicolor: true }),
|
|
805
|
+
emit ? StyledSubscript : Subscript,
|
|
806
|
+
emit ? StyledSuperscript : Superscript,
|
|
807
|
+
StyledSpanReader,
|
|
808
|
+
SubSupReader,
|
|
809
|
+
HighlightReader
|
|
810
|
+
];
|
|
811
|
+
}
|
|
812
|
+
function usableUrl(candidate) {
|
|
813
|
+
const value = candidate.trim();
|
|
814
|
+
if (!value || /[<>\n\r]/.test(value)) return null;
|
|
815
|
+
return safeMediaSrc(value);
|
|
816
|
+
}
|
|
565
817
|
function pickUrl(body) {
|
|
566
818
|
if (!body || typeof body !== "object") return void 0;
|
|
567
819
|
const record = body;
|
|
@@ -605,10 +857,29 @@ function uploadViaEndpoint(config, file, context) {
|
|
|
605
857
|
body = xhr.responseText;
|
|
606
858
|
}
|
|
607
859
|
if (config.transformResult) {
|
|
608
|
-
|
|
860
|
+
let transformed;
|
|
861
|
+
try {
|
|
862
|
+
transformed = config.transformResult(body, file);
|
|
863
|
+
} catch (cause) {
|
|
864
|
+
reject(new Error(`上传失败:upload.transformResult 抛出了异常(${String(cause)})`));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const result = transformed;
|
|
868
|
+
const picked = result && typeof result === "object" ? result.url : void 0;
|
|
869
|
+
const url2 = typeof picked === "string" ? usableUrl(picked) : null;
|
|
870
|
+
if (!url2) {
|
|
871
|
+
reject(
|
|
872
|
+
new Error(
|
|
873
|
+
`上传失败:upload.transformResult 没有返回带可用 url 的 UploadResult(拿到 ${JSON.stringify(picked)})`
|
|
874
|
+
)
|
|
875
|
+
);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
resolve({ ...result, url: url2 });
|
|
609
879
|
return;
|
|
610
880
|
}
|
|
611
|
-
const
|
|
881
|
+
const candidate = typeof body === "string" ? body : pickUrl(body);
|
|
882
|
+
const url = candidate ? usableUrl(candidate) : null;
|
|
612
883
|
if (!url) {
|
|
613
884
|
reject(new Error("上传失败:响应里找不到文件地址,请用 upload.transformResult 指定"));
|
|
614
885
|
return;
|
|
@@ -617,6 +888,8 @@ function uploadViaEndpoint(config, file, context) {
|
|
|
617
888
|
};
|
|
618
889
|
xhr.onerror = () => reject(new Error("上传失败:网络错误"));
|
|
619
890
|
xhr.onabort = () => reject(new DOMException("Aborted", "AbortError"));
|
|
891
|
+
xhr.ontimeout = () => reject(new Error(`上传失败:超过 ${config.timeout}ms 未响应`));
|
|
892
|
+
xhr.timeout = config.timeout ?? 0;
|
|
620
893
|
if (context.signal.aborted) {
|
|
621
894
|
xhr.abort();
|
|
622
895
|
return;
|
|
@@ -635,6 +908,17 @@ function createUploader(upload) {
|
|
|
635
908
|
if (!upload.endpoint) return null;
|
|
636
909
|
return { upload: (file, context) => uploadViaEndpoint(upload, file, context) };
|
|
637
910
|
}
|
|
911
|
+
const reported = /* @__PURE__ */ new WeakSet();
|
|
912
|
+
function runSafely(callback, label, args = []) {
|
|
913
|
+
if (typeof callback !== "function") return;
|
|
914
|
+
try {
|
|
915
|
+
callback(...args);
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (reported.has(callback)) return;
|
|
918
|
+
reported.add(callback);
|
|
919
|
+
console.error(`[Atri Editor] The "${label}" callback threw and was skipped:`, error);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
638
922
|
function readFileAsDataUrl(file) {
|
|
639
923
|
return new Promise((resolve, reject) => {
|
|
640
924
|
const reader = new FileReader();
|
|
@@ -975,14 +1259,7 @@ class MediaRuntime {
|
|
|
975
1259
|
findPosition(task) {
|
|
976
1260
|
const editor = this.editor;
|
|
977
1261
|
if (!editor) return null;
|
|
978
|
-
|
|
979
|
-
editor.state.doc.descendants((node, position) => {
|
|
980
|
-
if (found !== null) return;
|
|
981
|
-
if (node.attrs.uploadId === task.id || node.attrs.src === task.previewUrl) {
|
|
982
|
-
found = position;
|
|
983
|
-
}
|
|
984
|
-
});
|
|
985
|
-
return found;
|
|
1262
|
+
return findTaskPosition(editor.state.doc, task);
|
|
986
1263
|
}
|
|
987
1264
|
taskByPreviewUrl(src) {
|
|
988
1265
|
if (typeof src !== "string" || !src) return null;
|
|
@@ -1002,7 +1279,7 @@ class MediaRuntime {
|
|
|
1002
1279
|
}
|
|
1003
1280
|
reject(file, reason, cause) {
|
|
1004
1281
|
if (this.config.onError) {
|
|
1005
|
-
this.config.onError
|
|
1282
|
+
runSafely(this.config.onError, "media.onError", [{ file, reason, cause }]);
|
|
1006
1283
|
return;
|
|
1007
1284
|
}
|
|
1008
1285
|
console.warn(`[Atri Editor] File "${file.name}" was rejected (${reason}).`);
|
|
@@ -1016,6 +1293,17 @@ class MediaRuntime {
|
|
|
1016
1293
|
function clampPercent(percent) {
|
|
1017
1294
|
return Math.round(Math.min(100, Math.max(0, percent)));
|
|
1018
1295
|
}
|
|
1296
|
+
function findTaskPosition(doc, task) {
|
|
1297
|
+
let byUploadId = null;
|
|
1298
|
+
let byPreviewUrl = null;
|
|
1299
|
+
doc.descendants((node, position) => {
|
|
1300
|
+
if (byUploadId === null && node.attrs.uploadId === task.id) byUploadId = position;
|
|
1301
|
+
if (byPreviewUrl === null && task.previewUrl !== null && node.attrs.src === task.previewUrl) {
|
|
1302
|
+
byPreviewUrl = position;
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
return byUploadId ?? byPreviewUrl;
|
|
1306
|
+
}
|
|
1019
1307
|
function createPreviewUrl(file) {
|
|
1020
1308
|
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return null;
|
|
1021
1309
|
try {
|
|
@@ -1031,7 +1319,16 @@ function revokePreviewUrl(task) {
|
|
|
1031
1319
|
URL.revokeObjectURL(previewUrl);
|
|
1032
1320
|
task.previewUrl = null;
|
|
1033
1321
|
}
|
|
1034
|
-
const BUBBLE_TEXT_ITEMS = [
|
|
1322
|
+
const BUBBLE_TEXT_ITEMS = [
|
|
1323
|
+
"bold",
|
|
1324
|
+
"italic",
|
|
1325
|
+
"underline",
|
|
1326
|
+
"strike",
|
|
1327
|
+
"code",
|
|
1328
|
+
"highlight",
|
|
1329
|
+
"subscript",
|
|
1330
|
+
"superscript"
|
|
1331
|
+
];
|
|
1035
1332
|
const BUBBLE_NODE_ITEMS = ["attachmentDisplay", "delete"];
|
|
1036
1333
|
const BUBBLE_SELECTABLE_NODES = ["image", "attachment", "attachmentLink"];
|
|
1037
1334
|
function bubbleNodeNameFor(state) {
|
|
@@ -1068,13 +1365,15 @@ const LINK_HTML_ATTRIBUTES = {
|
|
|
1068
1365
|
};
|
|
1069
1366
|
class CoreEditor {
|
|
1070
1367
|
constructor(config) {
|
|
1368
|
+
this.ownsMediaRuntime = false;
|
|
1071
1369
|
this.config = config;
|
|
1072
1370
|
this.placeholderText = config.placeholder ?? "";
|
|
1073
1371
|
this.mediaRuntime = config.media === false ? null : config.mediaRuntime ?? new MediaRuntime(config.media);
|
|
1372
|
+
this.ownsMediaRuntime = this.mediaRuntime !== null && !config.mediaRuntime;
|
|
1074
1373
|
this.editor = this.createEditor();
|
|
1075
1374
|
}
|
|
1076
1375
|
createEditor() {
|
|
1077
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1376
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
1078
1377
|
const { element, content, contentFormat, editable, extensions } = this.config;
|
|
1079
1378
|
const editorExtensions = [
|
|
1080
1379
|
StarterKit.configure({
|
|
@@ -1089,7 +1388,14 @@ class CoreEditor {
|
|
|
1089
1388
|
...LINK_HTML_ATTRIBUTES,
|
|
1090
1389
|
target: ((_c = this.config.link) == null ? void 0 : _c.target) ?? LINK_HTML_ATTRIBUTES.target
|
|
1091
1390
|
}
|
|
1092
|
-
}
|
|
1391
|
+
},
|
|
1392
|
+
// 关掉内置代码块,让接入方(通常就是 @atri-editor/code-highlight)的实现独占这个节点名。
|
|
1393
|
+
// 为什么提供一个开关而不是让宿主直接传同名扩展顶掉:实测同名确实赢(后注册的赢,
|
|
1394
|
+
// 节点与标记都是),所以不关也能工作——但 tiptap 会打一句
|
|
1395
|
+
// "Duplicate extension names found: ['codeBlock']" 的 warning,而且这件事靠的是
|
|
1396
|
+
// ExtensionManager 的去重顺序这个内部行为,不是对外承诺。
|
|
1397
|
+
// 多一个包就多一处"看起来正常但踩在实现细节上"的配置,不如把意图写成显式的一档
|
|
1398
|
+
codeBlock: this.config.codeBlock === false ? false : void 0
|
|
1093
1399
|
})
|
|
1094
1400
|
];
|
|
1095
1401
|
editorExtensions.push(
|
|
@@ -1134,7 +1440,12 @@ class CoreEditor {
|
|
|
1134
1440
|
);
|
|
1135
1441
|
}
|
|
1136
1442
|
editorExtensions.push(TaskList, TaskItem);
|
|
1137
|
-
|
|
1443
|
+
if (this.config.formatting !== false) {
|
|
1444
|
+
editorExtensions.push(
|
|
1445
|
+
...createFormattingExtensions({ inlineStyles: (_g = this.config.markdown) == null ? void 0 : _g.inlineStyles })
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
const enableInputRules = ((_h = this.config.markdown) == null ? void 0 : _h.shortcuts) !== false;
|
|
1138
1449
|
if (extensions) {
|
|
1139
1450
|
editorExtensions.push(...extensions);
|
|
1140
1451
|
}
|
|
@@ -1151,27 +1462,22 @@ class CoreEditor {
|
|
|
1151
1462
|
}
|
|
1152
1463
|
},
|
|
1153
1464
|
onCreate: () => {
|
|
1154
|
-
|
|
1155
|
-
(_b2 = (_a2 = this.config).onCreate) == null ? void 0 : _b2.call(_a2);
|
|
1465
|
+
runSafely(this.config.onCreate, "onCreate");
|
|
1156
1466
|
},
|
|
1157
1467
|
onUpdate: () => {
|
|
1158
|
-
|
|
1159
|
-
(_b2 = (_a2 = this.config).onUpdate) == null ? void 0 : _b2.call(_a2);
|
|
1468
|
+
runSafely(this.config.onUpdate, "onUpdate");
|
|
1160
1469
|
},
|
|
1161
1470
|
onFocus: () => {
|
|
1162
|
-
|
|
1163
|
-
(_b2 = (_a2 = this.config).onFocus) == null ? void 0 : _b2.call(_a2);
|
|
1471
|
+
runSafely(this.config.onFocus, "onFocus");
|
|
1164
1472
|
},
|
|
1165
1473
|
onBlur: () => {
|
|
1166
|
-
|
|
1167
|
-
(_b2 = (_a2 = this.config).onBlur) == null ? void 0 : _b2.call(_a2);
|
|
1474
|
+
runSafely(this.config.onBlur, "onBlur");
|
|
1168
1475
|
},
|
|
1169
1476
|
onDestroy: () => {
|
|
1170
|
-
|
|
1171
|
-
(_b2 = (_a2 = this.config).onDestroy) == null ? void 0 : _b2.call(_a2);
|
|
1477
|
+
runSafely(this.config.onDestroy, "onDestroy");
|
|
1172
1478
|
}
|
|
1173
1479
|
});
|
|
1174
|
-
(
|
|
1480
|
+
(_i = this.mediaRuntime) == null ? void 0 : _i.bind(editor);
|
|
1175
1481
|
return editor;
|
|
1176
1482
|
}
|
|
1177
1483
|
/**
|
|
@@ -1271,11 +1577,17 @@ class CoreEditor {
|
|
|
1271
1577
|
* 销毁编辑器
|
|
1272
1578
|
*/
|
|
1273
1579
|
destroy() {
|
|
1580
|
+
var _a;
|
|
1274
1581
|
this.editor.destroy();
|
|
1582
|
+
if (this.ownsMediaRuntime) (_a = this.mediaRuntime) == null ? void 0 : _a.destroy();
|
|
1583
|
+
this.mediaRuntime = null;
|
|
1275
1584
|
}
|
|
1276
1585
|
}
|
|
1277
1586
|
function simpleMarkdownToHtml(markdown2) {
|
|
1278
|
-
let html = markdown2;
|
|
1587
|
+
let html = escapeHtml(markdown2);
|
|
1588
|
+
html = html.replace(/^###### (.+)$/gm, "<h6>$1</h6>");
|
|
1589
|
+
html = html.replace(/^##### (.+)$/gm, "<h5>$1</h5>");
|
|
1590
|
+
html = html.replace(/^#### (.+)$/gm, "<h4>$1</h4>");
|
|
1279
1591
|
html = html.replace(/^### (.+)$/gm, "<h3>$1</h3>");
|
|
1280
1592
|
html = html.replace(/^## (.+)$/gm, "<h2>$1</h2>");
|
|
1281
1593
|
html = html.replace(/^# (.+)$/gm, "<h1>$1</h1>");
|
|
@@ -1286,9 +1598,21 @@ function simpleMarkdownToHtml(markdown2) {
|
|
|
1286
1598
|
html = html.replace(/`(.+?)`/g, "<code>$1</code>");
|
|
1287
1599
|
html = html.replace(
|
|
1288
1600
|
/!\[([^\]]*)\]\(\s*([^\s)]+)(?:\s+"([^"]*)")?\s*\)/g,
|
|
1289
|
-
(_all, alt, src, title) =>
|
|
1601
|
+
(_all, alt, src, title) => {
|
|
1602
|
+
const url = safeMediaSrc(src);
|
|
1603
|
+
if (url === null) return alt;
|
|
1604
|
+
const titleAttr = title ? ` title="${escapeQuotes(title)}"` : "";
|
|
1605
|
+
return `<img src="${escapeQuotes(url)}" alt="${escapeQuotes(alt)}"${titleAttr}>`;
|
|
1606
|
+
}
|
|
1607
|
+
);
|
|
1608
|
+
html = html.replace(
|
|
1609
|
+
/\[([^\]]*)\]\(\s*([^\s)]+)(?:\s+"[^"]*")?\s*\)/g,
|
|
1610
|
+
(_all, text, href) => {
|
|
1611
|
+
const url = safeHref(href);
|
|
1612
|
+
if (url === null) return text;
|
|
1613
|
+
return `<a href="${escapeQuotes(url)}">${text}</a>`;
|
|
1614
|
+
}
|
|
1290
1615
|
);
|
|
1291
|
-
html = html.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
|
|
1292
1616
|
html = html.replace(/^\* (.+)$/gm, "<li>$1</li>");
|
|
1293
1617
|
html = html.replace(/^- (.+)$/gm, "<li>$1</li>");
|
|
1294
1618
|
html = html.replace(/^(\d+)\. (.+)$/gm, "<li>$2</li>");
|
|
@@ -1308,6 +1632,9 @@ function simpleHtmlToMarkdown(html) {
|
|
|
1308
1632
|
md = md.replace(/<h1[^>]*>(.+?)<\/h1>/gi, "# $1\n\n");
|
|
1309
1633
|
md = md.replace(/<h2[^>]*>(.+?)<\/h2>/gi, "## $1\n\n");
|
|
1310
1634
|
md = md.replace(/<h3[^>]*>(.+?)<\/h3>/gi, "### $1\n\n");
|
|
1635
|
+
md = md.replace(/<h4[^>]*>(.+?)<\/h4>/gi, "#### $1\n\n");
|
|
1636
|
+
md = md.replace(/<h5[^>]*>(.+?)<\/h5>/gi, "##### $1\n\n");
|
|
1637
|
+
md = md.replace(/<h6[^>]*>(.+?)<\/h6>/gi, "###### $1\n\n");
|
|
1311
1638
|
md = md.replace(/<strong[^>]*>(.+?)<\/strong>/gi, "**$1**");
|
|
1312
1639
|
md = md.replace(/<b[^>]*>(.+?)<\/b>/gi, "**$1**");
|
|
1313
1640
|
md = md.replace(/<em[^>]*>(.+?)<\/em>/gi, "*$1*");
|
|
@@ -1337,6 +1664,100 @@ const markdown = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProp
|
|
|
1337
1664
|
simpleHtmlToMarkdown,
|
|
1338
1665
|
simpleMarkdownToHtml
|
|
1339
1666
|
}, Symbol.toStringTag, { value: "Module" }));
|
|
1667
|
+
const RENDERED_BY_PARENT = ["tableRow", "tableHeader", "tableCell"];
|
|
1668
|
+
const LOSSY_ATTRIBUTES = {
|
|
1669
|
+
paragraph: ["textAlign"],
|
|
1670
|
+
heading: ["textAlign"],
|
|
1671
|
+
tableCell: ["colspan", "rowspan", "colwidth"],
|
|
1672
|
+
tableHeader: ["colspan", "rowspan", "colwidth"]
|
|
1673
|
+
};
|
|
1674
|
+
const LOSSY_ATTRIBUTES_WHEN_STYLES_DROPPED = {
|
|
1675
|
+
highlight: ["color"]
|
|
1676
|
+
};
|
|
1677
|
+
const RENDERING_NEUTRAL_VALUES = {
|
|
1678
|
+
textAlign: ["left"]
|
|
1679
|
+
};
|
|
1680
|
+
function isSignificant(value, defaultValue, attribute) {
|
|
1681
|
+
var _a;
|
|
1682
|
+
if (value === defaultValue || value === null || value === void 0) return false;
|
|
1683
|
+
if (Array.isArray(value)) {
|
|
1684
|
+
return value.some((item) => typeof item === "number" && item > 0);
|
|
1685
|
+
}
|
|
1686
|
+
if (typeof value === "string") {
|
|
1687
|
+
if (value.trim() === "") return false;
|
|
1688
|
+
if ((_a = RENDERING_NEUTRAL_VALUES[attribute]) == null ? void 0 : _a.includes(value)) return false;
|
|
1689
|
+
}
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
function buildRenderableTypes(editor) {
|
|
1693
|
+
const schemaTypes = /* @__PURE__ */ new Set([
|
|
1694
|
+
...Object.keys(editor.schema.nodes),
|
|
1695
|
+
...Object.keys(editor.schema.marks)
|
|
1696
|
+
]);
|
|
1697
|
+
const renderable = /* @__PURE__ */ new Set();
|
|
1698
|
+
for (const extension of editor.extensionManager.extensions) {
|
|
1699
|
+
if (!schemaTypes.has(extension.name)) continue;
|
|
1700
|
+
if (getExtensionField(extension, "renderMarkdown")) renderable.add(extension.name);
|
|
1701
|
+
}
|
|
1702
|
+
return renderable;
|
|
1703
|
+
}
|
|
1704
|
+
function collectMarkdownLosses(editor, options) {
|
|
1705
|
+
const renderable = buildRenderableTypes(editor);
|
|
1706
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1707
|
+
const marks = /* @__PURE__ */ new Map();
|
|
1708
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
1709
|
+
const countAttribute = (type, attribute, count) => {
|
|
1710
|
+
const key = `${type}/${attribute}`;
|
|
1711
|
+
const entry = attributes.get(key);
|
|
1712
|
+
if (entry) entry.count += count;
|
|
1713
|
+
else attributes.set(key, { loss: { kind: "attribute", type, attribute, count }, count });
|
|
1714
|
+
};
|
|
1715
|
+
let previousMarkNames = /* @__PURE__ */ new Set();
|
|
1716
|
+
const checkAttributes = (owner, attrs, specAttrs) => {
|
|
1717
|
+
var _a;
|
|
1718
|
+
const defaults = specAttrs ?? {};
|
|
1719
|
+
const tables = options.inlineStyles === "drop" ? [LOSSY_ATTRIBUTES, LOSSY_ATTRIBUTES_WHEN_STYLES_DROPPED] : [LOSSY_ATTRIBUTES];
|
|
1720
|
+
for (const table of tables) {
|
|
1721
|
+
for (const attribute of table[owner] ?? []) {
|
|
1722
|
+
if (isSignificant(attrs[attribute], (_a = defaults[attribute]) == null ? void 0 : _a.default, attribute)) {
|
|
1723
|
+
countAttribute(owner, attribute, 1);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
editor.state.doc.descendants((node, _position, parent) => {
|
|
1729
|
+
const name = node.type.name;
|
|
1730
|
+
if (name === "text") {
|
|
1731
|
+
const fresh = node.marks.filter((mark) => !previousMarkNames.has(mark.type.name));
|
|
1732
|
+
previousMarkNames = new Set(node.marks.map((mark) => mark.type.name));
|
|
1733
|
+
for (const mark of fresh) {
|
|
1734
|
+
if (!renderable.has(mark.type.name)) {
|
|
1735
|
+
marks.set(mark.type.name, (marks.get(mark.type.name) ?? 0) + 1);
|
|
1736
|
+
}
|
|
1737
|
+
checkAttributes(mark.type.name, mark.attrs, mark.type.spec.attrs);
|
|
1738
|
+
}
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
checkAttributes(name, node.attrs, node.type.spec.attrs);
|
|
1742
|
+
if (parent && !renderable.has(name) && !RENDERED_BY_PARENT.includes(name)) {
|
|
1743
|
+
nodes.set(name, (nodes.get(name) ?? 0) + 1);
|
|
1744
|
+
previousMarkNames = /* @__PURE__ */ new Set();
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
previousMarkNames = /* @__PURE__ */ new Set();
|
|
1748
|
+
return true;
|
|
1749
|
+
});
|
|
1750
|
+
const byCountThenName = (a, b) => b[1] - a[1] || a[0].localeCompare(b[0]);
|
|
1751
|
+
const ofKind = (kind) => (entry) => ({ kind, type: entry[0], count: entry[1] });
|
|
1752
|
+
const losses = [
|
|
1753
|
+
...Array.from(nodes.entries()).sort(byCountThenName).map(ofKind("node")),
|
|
1754
|
+
...Array.from(marks.entries()).sort(byCountThenName).map(ofKind("mark"))
|
|
1755
|
+
];
|
|
1756
|
+
for (const entry of attributes.values()) {
|
|
1757
|
+
losses.push(entry.loss);
|
|
1758
|
+
}
|
|
1759
|
+
return losses;
|
|
1760
|
+
}
|
|
1340
1761
|
class MarkdownService {
|
|
1341
1762
|
constructor(editor, config = {}) {
|
|
1342
1763
|
this.editor = editor;
|
|
@@ -1419,6 +1840,26 @@ class MarkdownService {
|
|
|
1419
1840
|
]
|
|
1420
1841
|
};
|
|
1421
1842
|
}
|
|
1843
|
+
/**
|
|
1844
|
+
* 这一趟 getMarkdown() 会丢掉什么,判据见 markdown-losses.ts
|
|
1845
|
+
*
|
|
1846
|
+
* Markdown 关闭时返回空数组:那时 getMarkdown() 走的是整篇 HTML 的降级转换,
|
|
1847
|
+
* 不按类型分派,也就没有"哪个类型没有 renderer"可问。
|
|
1848
|
+
*/
|
|
1849
|
+
getLosses() {
|
|
1850
|
+
if (!this.manager) {
|
|
1851
|
+
return [];
|
|
1852
|
+
}
|
|
1853
|
+
return collectMarkdownLosses(this.editor, {
|
|
1854
|
+
inlineStyles: this.config.inlineStyles ?? "html"
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* 有没有内容在导出 Markdown 时会丢,命名对齐 hasPendingUploads()
|
|
1859
|
+
*/
|
|
1860
|
+
hasLossyContent() {
|
|
1861
|
+
return this.getLosses().length > 0;
|
|
1862
|
+
}
|
|
1422
1863
|
/**
|
|
1423
1864
|
* 是否启用
|
|
1424
1865
|
*/
|
|
@@ -1426,6 +1867,7 @@ class MarkdownService {
|
|
|
1426
1867
|
return this.config.enabled !== false;
|
|
1427
1868
|
}
|
|
1428
1869
|
}
|
|
1870
|
+
const THEME_PREFIX$1 = "atri-theme-";
|
|
1429
1871
|
class ThemeManager {
|
|
1430
1872
|
constructor(container, theme = "light") {
|
|
1431
1873
|
this.container = container;
|
|
@@ -1436,8 +1878,8 @@ class ThemeManager {
|
|
|
1436
1878
|
* 应用当前主题:内置主题与自定义主题走同一条 class 规则,只是名字不同
|
|
1437
1879
|
*/
|
|
1438
1880
|
applyTheme() {
|
|
1439
|
-
this.container.classList.
|
|
1440
|
-
this.container.classList.add(
|
|
1881
|
+
Array.from(this.container.classList).filter((name) => name.startsWith(THEME_PREFIX$1)).forEach((name) => this.container.classList.remove(name));
|
|
1882
|
+
this.container.classList.add(`${THEME_PREFIX$1}${this.currentTheme}`);
|
|
1441
1883
|
this.container.setAttribute("data-atri-theme", this.currentTheme);
|
|
1442
1884
|
}
|
|
1443
1885
|
/**
|
|
@@ -1472,6 +1914,9 @@ const zhResources = {
|
|
|
1472
1914
|
heading1: "标题 1",
|
|
1473
1915
|
heading2: "标题 2",
|
|
1474
1916
|
heading3: "标题 3",
|
|
1917
|
+
heading4: "标题 4",
|
|
1918
|
+
heading5: "标题 5",
|
|
1919
|
+
heading6: "标题 6",
|
|
1475
1920
|
paragraph: "正文",
|
|
1476
1921
|
bulletList: "无序列表",
|
|
1477
1922
|
orderedList: "有序列表",
|
|
@@ -1488,10 +1933,27 @@ const zhResources = {
|
|
|
1488
1933
|
table: "表格",
|
|
1489
1934
|
undo: "撤销",
|
|
1490
1935
|
redo: "重做",
|
|
1936
|
+
align: "对齐",
|
|
1491
1937
|
alignLeft: "左对齐",
|
|
1492
1938
|
alignCenter: "居中",
|
|
1493
1939
|
alignRight: "右对齐",
|
|
1494
|
-
alignJustify: "两端对齐"
|
|
1940
|
+
alignJustify: "两端对齐",
|
|
1941
|
+
textColor: "文字颜色",
|
|
1942
|
+
highlight: "高亮",
|
|
1943
|
+
highlightColor: "高亮色",
|
|
1944
|
+
fontSize: "字号",
|
|
1945
|
+
fontFamily: "字体",
|
|
1946
|
+
subscript: "下标",
|
|
1947
|
+
superscript: "上标",
|
|
1948
|
+
clearFormat: "清除格式"
|
|
1949
|
+
},
|
|
1950
|
+
formatting: {
|
|
1951
|
+
customColor: "其他颜色…",
|
|
1952
|
+
noColor: "无",
|
|
1953
|
+
defaultValue: "默认",
|
|
1954
|
+
fontSans: "无衬线",
|
|
1955
|
+
fontSerif: "衬线",
|
|
1956
|
+
fontMono: "等宽"
|
|
1495
1957
|
},
|
|
1496
1958
|
media: {
|
|
1497
1959
|
uploading: "上传中 {{files}} 个 · {{percent}}%",
|
|
@@ -1549,6 +2011,9 @@ const enResources = {
|
|
|
1549
2011
|
heading1: "Heading 1",
|
|
1550
2012
|
heading2: "Heading 2",
|
|
1551
2013
|
heading3: "Heading 3",
|
|
2014
|
+
heading4: "Heading 4",
|
|
2015
|
+
heading5: "Heading 5",
|
|
2016
|
+
heading6: "Heading 6",
|
|
1552
2017
|
paragraph: "Paragraph",
|
|
1553
2018
|
bulletList: "Bullet List",
|
|
1554
2019
|
orderedList: "Ordered List",
|
|
@@ -1565,10 +2030,27 @@ const enResources = {
|
|
|
1565
2030
|
table: "Table",
|
|
1566
2031
|
undo: "Undo",
|
|
1567
2032
|
redo: "Redo",
|
|
2033
|
+
align: "Align",
|
|
1568
2034
|
alignLeft: "Align Left",
|
|
1569
2035
|
alignCenter: "Align Center",
|
|
1570
2036
|
alignRight: "Align Right",
|
|
1571
|
-
alignJustify: "Justify"
|
|
2037
|
+
alignJustify: "Justify",
|
|
2038
|
+
textColor: "Text Color",
|
|
2039
|
+
highlight: "Highlight",
|
|
2040
|
+
highlightColor: "Highlight Color",
|
|
2041
|
+
fontSize: "Font Size",
|
|
2042
|
+
fontFamily: "Font",
|
|
2043
|
+
subscript: "Subscript",
|
|
2044
|
+
superscript: "Superscript",
|
|
2045
|
+
clearFormat: "Clear Formatting"
|
|
2046
|
+
},
|
|
2047
|
+
formatting: {
|
|
2048
|
+
customColor: "Custom color…",
|
|
2049
|
+
noColor: "None",
|
|
2050
|
+
defaultValue: "Default",
|
|
2051
|
+
fontSans: "Sans",
|
|
2052
|
+
fontSerif: "Serif",
|
|
2053
|
+
fontMono: "Monospace"
|
|
1572
2054
|
},
|
|
1573
2055
|
media: {
|
|
1574
2056
|
uploading: "Uploading {{files}} · {{percent}}%",
|
|
@@ -1614,6 +2096,9 @@ const enResources = {
|
|
|
1614
2096
|
error: "AI Error"
|
|
1615
2097
|
}
|
|
1616
2098
|
};
|
|
2099
|
+
function cloneResources(value) {
|
|
2100
|
+
return JSON.parse(JSON.stringify(value));
|
|
2101
|
+
}
|
|
1617
2102
|
class I18nManager {
|
|
1618
2103
|
constructor(lang = "zh") {
|
|
1619
2104
|
this.instance = i18next.createInstance();
|
|
@@ -1622,8 +2107,8 @@ class I18nManager {
|
|
|
1622
2107
|
fallbackLng: "zh",
|
|
1623
2108
|
initImmediate: false,
|
|
1624
2109
|
resources: {
|
|
1625
|
-
zh: { translation: zhResources },
|
|
1626
|
-
en: { translation: enResources }
|
|
2110
|
+
zh: { translation: cloneResources(zhResources) },
|
|
2111
|
+
en: { translation: cloneResources(enResources) }
|
|
1627
2112
|
}
|
|
1628
2113
|
});
|
|
1629
2114
|
}
|
|
@@ -1646,6 +2131,30 @@ class I18nManager {
|
|
|
1646
2131
|
async changeLanguage(lang) {
|
|
1647
2132
|
await this.instance.changeLanguage(lang);
|
|
1648
2133
|
}
|
|
2134
|
+
/**
|
|
2135
|
+
* 注册语言包:给已存在的语言补词条,或引入一门内置清单之外的新语言。
|
|
2136
|
+
*
|
|
2137
|
+
* 内置词条是源码里的两个字面量对象,接入方改不到,于是 lang: 'ja' 只能静默回退中文
|
|
2138
|
+
* ——看起来像"支持可扩展",实际没有入口。这里补上那个入口。
|
|
2139
|
+
*
|
|
2140
|
+
* overwrite 传 false:同名 key 以先注册的一方为准,防止后加载的包把内置文案悄悄换掉。
|
|
2141
|
+
* 要自定义措辞走 toolbar.items 的 tooltip,那本来就是优先级更高的显式入口。
|
|
2142
|
+
* 注册当前语言时补发一次 languageChanged,让已挂载的界面重取文案;
|
|
2143
|
+
* 不是当前语言就不发,白重刷一遍界面没有意义。
|
|
2144
|
+
*/
|
|
2145
|
+
addResources(lang, resources) {
|
|
2146
|
+
this.instance.addResourceBundle(lang, "translation", resources, true, false);
|
|
2147
|
+
if (lang === this.instance.language) {
|
|
2148
|
+
this.instance.emit("languageChanged", lang);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* 该语言是否已有语言包。未注册的语言不会抛错,只会按 fallbackLng 回退,
|
|
2153
|
+
* 所以想知道有没有真的生效得问这里。
|
|
2154
|
+
*/
|
|
2155
|
+
hasLanguage(lang) {
|
|
2156
|
+
return this.instance.hasResourceBundle(lang, "translation");
|
|
2157
|
+
}
|
|
1649
2158
|
/**
|
|
1650
2159
|
* 监听语言变更,返回取消订阅的函数
|
|
1651
2160
|
*/
|
|
@@ -1663,6 +2172,13 @@ class I18nManager {
|
|
|
1663
2172
|
function tOrFallback(i18n, key, fallback) {
|
|
1664
2173
|
return (i18n == null ? void 0 : i18n.tOr(key, fallback)) ?? fallback;
|
|
1665
2174
|
}
|
|
2175
|
+
function zhText(key) {
|
|
2176
|
+
const found = key.split(".").reduce(
|
|
2177
|
+
(node, part) => node && typeof node === "object" ? node[part] : void 0,
|
|
2178
|
+
zhResources
|
|
2179
|
+
);
|
|
2180
|
+
return typeof found === "string" ? found : "";
|
|
2181
|
+
}
|
|
1666
2182
|
class ExtensionManager {
|
|
1667
2183
|
constructor() {
|
|
1668
2184
|
this.extensions = /* @__PURE__ */ new Map();
|
|
@@ -1742,6 +2258,16 @@ class ExtensionManager {
|
|
|
1742
2258
|
return this.extensions.has(name);
|
|
1743
2259
|
}
|
|
1744
2260
|
}
|
|
2261
|
+
const PANEL_OFFSET = 6;
|
|
2262
|
+
const PANEL_SHIFT_PADDING = 8;
|
|
2263
|
+
function panelMiddleware() {
|
|
2264
|
+
return [offset(PANEL_OFFSET), flip(), shift({ padding: PANEL_SHIFT_PADDING })];
|
|
2265
|
+
}
|
|
2266
|
+
function placePanel(panel, x, y) {
|
|
2267
|
+
panel.style.position = "fixed";
|
|
2268
|
+
panel.style.left = `${x}px`;
|
|
2269
|
+
panel.style.top = `${y}px`;
|
|
2270
|
+
}
|
|
1745
2271
|
function positionPanel(trigger, panel, isCurrent) {
|
|
1746
2272
|
let stopAutoUpdate = null;
|
|
1747
2273
|
const update = async () => {
|
|
@@ -1749,12 +2275,10 @@ function positionPanel(trigger, panel, isCurrent) {
|
|
|
1749
2275
|
try {
|
|
1750
2276
|
const { x, y } = await computePosition(trigger, panel, {
|
|
1751
2277
|
placement: "bottom-start",
|
|
1752
|
-
middleware:
|
|
2278
|
+
middleware: panelMiddleware()
|
|
1753
2279
|
});
|
|
1754
2280
|
if (!isCurrent()) return;
|
|
1755
|
-
panel
|
|
1756
|
-
panel.style.left = `${x}px`;
|
|
1757
|
-
panel.style.top = `${y}px`;
|
|
2281
|
+
placePanel(panel, x, y);
|
|
1758
2282
|
} catch {
|
|
1759
2283
|
}
|
|
1760
2284
|
};
|
|
@@ -1789,10 +2313,20 @@ function listenPanelDismiss(trigger, panel, onClose) {
|
|
|
1789
2313
|
document.removeEventListener("keydown", onKeyDown, true);
|
|
1790
2314
|
};
|
|
1791
2315
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2316
|
+
const THEME_PREFIX = "atri-theme-";
|
|
2317
|
+
function syncThemeClasses(target, root) {
|
|
2318
|
+
Array.from(target.classList).filter((name) => name.startsWith(THEME_PREFIX)).forEach((name) => target.classList.remove(name));
|
|
2319
|
+
const themes = Array.from(root.classList).filter((name) => name.startsWith(THEME_PREFIX));
|
|
2320
|
+
target.classList.add(...themes);
|
|
2321
|
+
}
|
|
2322
|
+
function followThemeClasses(source, panel) {
|
|
2323
|
+
const root = (source == null ? void 0 : source.closest(".atri-editor")) ?? null;
|
|
2324
|
+
if (root) syncThemeClasses(panel, root);
|
|
2325
|
+
if (!root || typeof MutationObserver === "undefined") return () => {
|
|
2326
|
+
};
|
|
2327
|
+
const observer = new MutationObserver(() => syncThemeClasses(panel, root));
|
|
2328
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
2329
|
+
return () => observer.disconnect();
|
|
1796
2330
|
}
|
|
1797
2331
|
const FIELDS = {
|
|
1798
2332
|
image: { url: true, alt: true, kind: "image" },
|
|
@@ -1805,6 +2339,7 @@ class InsertPanel {
|
|
|
1805
2339
|
this.mode = null;
|
|
1806
2340
|
this.floating = null;
|
|
1807
2341
|
this.detachDismiss = null;
|
|
2342
|
+
this.detachTheme = null;
|
|
1808
2343
|
this.labelled = [];
|
|
1809
2344
|
this.editor = options.editor;
|
|
1810
2345
|
this.runtime = options.runtime;
|
|
@@ -1832,17 +2367,19 @@ class InsertPanel {
|
|
|
1832
2367
|
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, mode);
|
|
1833
2368
|
}
|
|
1834
2369
|
close() {
|
|
1835
|
-
var _a, _b, _c, _d;
|
|
2370
|
+
var _a, _b, _c, _d, _e;
|
|
1836
2371
|
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
1837
2372
|
this.detachDismiss = null;
|
|
1838
|
-
(_b = this.
|
|
2373
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
2374
|
+
this.detachTheme = null;
|
|
2375
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
1839
2376
|
this.floating = null;
|
|
1840
|
-
(
|
|
2377
|
+
(_d = this.element) == null ? void 0 : _d.remove();
|
|
1841
2378
|
this.element = null;
|
|
1842
2379
|
this.labelled = [];
|
|
1843
2380
|
if (this.mode === null) return;
|
|
1844
2381
|
this.mode = null;
|
|
1845
|
-
(
|
|
2382
|
+
(_e = this.onOpenChange) == null ? void 0 : _e.call(this, null);
|
|
1846
2383
|
}
|
|
1847
2384
|
destroy() {
|
|
1848
2385
|
var _a;
|
|
@@ -1937,7 +2474,7 @@ class InsertPanel {
|
|
|
1937
2474
|
}
|
|
1938
2475
|
document.body.appendChild(panel);
|
|
1939
2476
|
this.element = panel;
|
|
1940
|
-
|
|
2477
|
+
this.detachTheme = followThemeClasses(this.editor.view.dom, panel);
|
|
1941
2478
|
(urlInput ?? altInput ?? browse).focus();
|
|
1942
2479
|
}
|
|
1943
2480
|
/**
|
|
@@ -1968,6 +2505,232 @@ class InsertPanel {
|
|
|
1968
2505
|
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
1969
2506
|
}
|
|
1970
2507
|
}
|
|
2508
|
+
class StyleOptionPanel {
|
|
2509
|
+
constructor(options) {
|
|
2510
|
+
this.element = null;
|
|
2511
|
+
this.floating = null;
|
|
2512
|
+
this.detachDismiss = null;
|
|
2513
|
+
this.detachTheme = null;
|
|
2514
|
+
this.anchor = null;
|
|
2515
|
+
this.currentKey = null;
|
|
2516
|
+
this.rows = [];
|
|
2517
|
+
this.editor = options.editor;
|
|
2518
|
+
this.i18n = options.i18n;
|
|
2519
|
+
this.onOpenChange = options.onOpenChange;
|
|
2520
|
+
}
|
|
2521
|
+
get isOpen() {
|
|
2522
|
+
return this.element !== null;
|
|
2523
|
+
}
|
|
2524
|
+
get openKey() {
|
|
2525
|
+
return this.currentKey;
|
|
2526
|
+
}
|
|
2527
|
+
/** 面板是否正开在某个指定触发器上——工具栏据此决定按钮的按下态给谁 */
|
|
2528
|
+
isOpenFor(anchor) {
|
|
2529
|
+
return this.isOpen && this.anchor === anchor;
|
|
2530
|
+
}
|
|
2531
|
+
toggle(anchor, spec) {
|
|
2532
|
+
if (this.isOpen) {
|
|
2533
|
+
const switching = this.anchor !== anchor;
|
|
2534
|
+
this.close();
|
|
2535
|
+
if (switching) this.open(anchor, spec);
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
this.open(anchor, spec);
|
|
2539
|
+
}
|
|
2540
|
+
close() {
|
|
2541
|
+
var _a, _b, _c, _d;
|
|
2542
|
+
if (!this.element) return;
|
|
2543
|
+
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
2544
|
+
this.detachDismiss = null;
|
|
2545
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
2546
|
+
this.detachTheme = null;
|
|
2547
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
2548
|
+
this.floating = null;
|
|
2549
|
+
this.element.remove();
|
|
2550
|
+
this.element = null;
|
|
2551
|
+
this.anchor = null;
|
|
2552
|
+
this.currentKey = null;
|
|
2553
|
+
this.rows = [];
|
|
2554
|
+
(_d = this.onOpenChange) == null ? void 0 : _d.call(this, false);
|
|
2555
|
+
}
|
|
2556
|
+
destroy() {
|
|
2557
|
+
this.close();
|
|
2558
|
+
}
|
|
2559
|
+
open(anchor, spec) {
|
|
2560
|
+
var _a;
|
|
2561
|
+
const panel = this.build(spec);
|
|
2562
|
+
document.body.appendChild(panel);
|
|
2563
|
+
this.detachTheme = followThemeClasses(this.editor.view.dom, panel);
|
|
2564
|
+
this.element = panel;
|
|
2565
|
+
this.anchor = anchor;
|
|
2566
|
+
this.currentKey = spec.key;
|
|
2567
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
2568
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
2569
|
+
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
2570
|
+
panel.focus();
|
|
2571
|
+
}
|
|
2572
|
+
labelOf(option) {
|
|
2573
|
+
if (!option.i18nKey) return option.label;
|
|
2574
|
+
return tOrFallback(this.i18n, option.i18nKey, option.label);
|
|
2575
|
+
}
|
|
2576
|
+
build(spec) {
|
|
2577
|
+
const panel = document.createElement("div");
|
|
2578
|
+
panel.className = `atri-style-panel atri-style-panel--${spec.shape}`;
|
|
2579
|
+
panel.setAttribute("role", "menu");
|
|
2580
|
+
panel.setAttribute("aria-label", spec.label);
|
|
2581
|
+
panel.tabIndex = -1;
|
|
2582
|
+
panel.addEventListener("keydown", (event) => this.onKeyDown(event));
|
|
2583
|
+
const body = document.createElement("div");
|
|
2584
|
+
body.className = "atri-style-panel-body";
|
|
2585
|
+
this.rows = [];
|
|
2586
|
+
spec.options.forEach((option) => {
|
|
2587
|
+
body.appendChild(this.makeRow(option, spec));
|
|
2588
|
+
});
|
|
2589
|
+
if (spec.customColorLabel) {
|
|
2590
|
+
body.appendChild(this.makeCustomColorRow(spec));
|
|
2591
|
+
}
|
|
2592
|
+
if (spec.clearLabel) {
|
|
2593
|
+
const clear = {
|
|
2594
|
+
value: "",
|
|
2595
|
+
label: spec.clearLabel,
|
|
2596
|
+
isClear: true
|
|
2597
|
+
};
|
|
2598
|
+
body.appendChild(this.makeRow(clear, spec));
|
|
2599
|
+
}
|
|
2600
|
+
panel.appendChild(body);
|
|
2601
|
+
return panel;
|
|
2602
|
+
}
|
|
2603
|
+
makeRow(option, spec) {
|
|
2604
|
+
const row = document.createElement("button");
|
|
2605
|
+
row.type = "button";
|
|
2606
|
+
row.className = "atri-style-panel-row";
|
|
2607
|
+
row.setAttribute("role", "menuitem");
|
|
2608
|
+
row.dataset.styleValue = option.value;
|
|
2609
|
+
if (option.isClear) row.classList.add("atri-style-panel-row--clear");
|
|
2610
|
+
const label = this.labelOf(option);
|
|
2611
|
+
row.title = label;
|
|
2612
|
+
row.setAttribute("aria-label", label);
|
|
2613
|
+
if (option.swatch) {
|
|
2614
|
+
const swatch = document.createElement("span");
|
|
2615
|
+
swatch.className = "atri-style-panel-swatch";
|
|
2616
|
+
swatch.style.backgroundColor = option.swatch;
|
|
2617
|
+
swatch.setAttribute("aria-hidden", "true");
|
|
2618
|
+
row.appendChild(swatch);
|
|
2619
|
+
if (spec.shape === "grid") row.classList.add("atri-style-panel-row--swatch-only");
|
|
2620
|
+
}
|
|
2621
|
+
if (spec.shape === "list" || !option.swatch) {
|
|
2622
|
+
const text = document.createElement("span");
|
|
2623
|
+
text.className = "atri-style-panel-text";
|
|
2624
|
+
text.textContent = label;
|
|
2625
|
+
if (option.preview) text.style.fontFamily = option.preview;
|
|
2626
|
+
row.appendChild(text);
|
|
2627
|
+
}
|
|
2628
|
+
row.addEventListener("click", () => {
|
|
2629
|
+
if (option.isClear) {
|
|
2630
|
+
spec.onPick("");
|
|
2631
|
+
} else {
|
|
2632
|
+
spec.onPick(option.value);
|
|
2633
|
+
}
|
|
2634
|
+
this.close();
|
|
2635
|
+
});
|
|
2636
|
+
this.rows.push(row);
|
|
2637
|
+
return row;
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* 原生取色器:不在预设色板里时用它。
|
|
2641
|
+
*
|
|
2642
|
+
* 监听 change 而不是 input——input 在拖动取色器的过程中每移一像素就触发一次,
|
|
2643
|
+
* 每一次都是一条进撤销历史的事务,用户按一次撤销只能退一格,得按上百次才回到起点。
|
|
2644
|
+
*/
|
|
2645
|
+
makeCustomColorRow(spec) {
|
|
2646
|
+
const row = document.createElement("label");
|
|
2647
|
+
row.className = "atri-style-panel-custom";
|
|
2648
|
+
const input = document.createElement("input");
|
|
2649
|
+
input.type = "color";
|
|
2650
|
+
input.className = "atri-style-panel-custom-input";
|
|
2651
|
+
input.value = "#1890ff";
|
|
2652
|
+
input.addEventListener("change", () => {
|
|
2653
|
+
spec.onPick(input.value);
|
|
2654
|
+
this.close();
|
|
2655
|
+
});
|
|
2656
|
+
const text = document.createElement("span");
|
|
2657
|
+
text.textContent = spec.customColorLabel ?? "";
|
|
2658
|
+
row.appendChild(input);
|
|
2659
|
+
row.appendChild(text);
|
|
2660
|
+
return row;
|
|
2661
|
+
}
|
|
2662
|
+
onKeyDown(event) {
|
|
2663
|
+
const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
|
|
2664
|
+
if (event.key === "Enter" || event.key === " ") return;
|
|
2665
|
+
if (step === void 0) return;
|
|
2666
|
+
event.preventDefault();
|
|
2667
|
+
if (this.rows.length === 0) return;
|
|
2668
|
+
const focused = document.activeElement;
|
|
2669
|
+
const current = this.rows.indexOf(focused);
|
|
2670
|
+
const next = current < 0 ? step > 0 ? 0 : this.rows.length - 1 : (current + step + this.rows.length) % this.rows.length;
|
|
2671
|
+
this.rows[next].focus();
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
const TEXT_COLORS = [
|
|
2675
|
+
"rgb(31, 31, 31)",
|
|
2676
|
+
"rgb(89, 89, 89)",
|
|
2677
|
+
"rgb(24, 144, 255)",
|
|
2678
|
+
"rgb(82, 196, 26)",
|
|
2679
|
+
"rgb(250, 173, 20)",
|
|
2680
|
+
"rgb(245, 34, 45)",
|
|
2681
|
+
"rgb(114, 46, 209)",
|
|
2682
|
+
"rgb(235, 47, 150)",
|
|
2683
|
+
"rgb(19, 194, 194)",
|
|
2684
|
+
"rgb(47, 84, 235)",
|
|
2685
|
+
"rgb(160, 97, 42)",
|
|
2686
|
+
"rgb(120, 120, 120)"
|
|
2687
|
+
].map((value) => ({ value, label: value, swatch: value }));
|
|
2688
|
+
const HIGHLIGHT_COLORS = [
|
|
2689
|
+
"rgb(255, 241, 184)",
|
|
2690
|
+
"rgb(200, 250, 170)",
|
|
2691
|
+
"rgb(168, 223, 255)",
|
|
2692
|
+
"rgb(255, 192, 203)",
|
|
2693
|
+
"rgb(223, 190, 255)",
|
|
2694
|
+
"rgb(255, 214, 168)",
|
|
2695
|
+
"rgb(178, 235, 242)",
|
|
2696
|
+
"rgb(230, 230, 230)"
|
|
2697
|
+
].map((value) => ({ value, label: value, swatch: value }));
|
|
2698
|
+
const FONT_SIZES = [
|
|
2699
|
+
"12px",
|
|
2700
|
+
"14px",
|
|
2701
|
+
"16px",
|
|
2702
|
+
"18px",
|
|
2703
|
+
"20px",
|
|
2704
|
+
"24px",
|
|
2705
|
+
"28px",
|
|
2706
|
+
"32px",
|
|
2707
|
+
"40px",
|
|
2708
|
+
"48px"
|
|
2709
|
+
].map((value) => ({ value, label: value }));
|
|
2710
|
+
const FONT_FAMILIES = [
|
|
2711
|
+
{
|
|
2712
|
+
value: '-apple-system, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
2713
|
+
label: "Sans",
|
|
2714
|
+
i18nKey: "formatting.fontSans",
|
|
2715
|
+
preview: "sans-serif"
|
|
2716
|
+
},
|
|
2717
|
+
{
|
|
2718
|
+
value: 'Georgia, "Times New Roman", "Songti SC", SimSun, serif',
|
|
2719
|
+
label: "Serif",
|
|
2720
|
+
i18nKey: "formatting.fontSerif",
|
|
2721
|
+
preview: "serif"
|
|
2722
|
+
},
|
|
2723
|
+
{
|
|
2724
|
+
value: '"SFMono-Regular", Consolas, Menlo, monospace',
|
|
2725
|
+
label: "Monospace",
|
|
2726
|
+
i18nKey: "formatting.fontMono",
|
|
2727
|
+
preview: "monospace"
|
|
2728
|
+
},
|
|
2729
|
+
{ value: '"PingFang SC", "Microsoft YaHei", sans-serif', label: "PingFang / YaHei" },
|
|
2730
|
+
{ value: 'SimSun, "Songti SC", serif', label: "SimSun" },
|
|
2731
|
+
{ value: 'SimHei, "Heiti SC", sans-serif', label: "SimHei" },
|
|
2732
|
+
{ value: 'KaiTi, "Kaiti SC", serif', label: "KaiTi" }
|
|
2733
|
+
];
|
|
1971
2734
|
const MAX_COLS = 8;
|
|
1972
2735
|
const MAX_ROWS = 6;
|
|
1973
2736
|
class TableGridPanel {
|
|
@@ -1975,6 +2738,7 @@ class TableGridPanel {
|
|
|
1975
2738
|
this.element = null;
|
|
1976
2739
|
this.floating = null;
|
|
1977
2740
|
this.detachDismiss = null;
|
|
2741
|
+
this.detachTheme = null;
|
|
1978
2742
|
this.cursorRow = -1;
|
|
1979
2743
|
this.cursorCol = -1;
|
|
1980
2744
|
this.cells = [];
|
|
@@ -1999,18 +2763,20 @@ class TableGridPanel {
|
|
|
1999
2763
|
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
2000
2764
|
}
|
|
2001
2765
|
close() {
|
|
2002
|
-
var _a, _b, _c;
|
|
2766
|
+
var _a, _b, _c, _d;
|
|
2003
2767
|
if (!this.element) return;
|
|
2004
2768
|
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
2005
2769
|
this.detachDismiss = null;
|
|
2006
|
-
(_b = this.
|
|
2770
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
2771
|
+
this.detachTheme = null;
|
|
2772
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
2007
2773
|
this.floating = null;
|
|
2008
2774
|
this.element.remove();
|
|
2009
2775
|
this.element = null;
|
|
2010
2776
|
this.cells = [];
|
|
2011
2777
|
this.cursorRow = -1;
|
|
2012
2778
|
this.cursorCol = -1;
|
|
2013
|
-
(
|
|
2779
|
+
(_d = this.onOpenChange) == null ? void 0 : _d.call(this, false);
|
|
2014
2780
|
}
|
|
2015
2781
|
destroy() {
|
|
2016
2782
|
this.close();
|
|
@@ -2048,7 +2814,7 @@ class TableGridPanel {
|
|
|
2048
2814
|
panel.appendChild(hint);
|
|
2049
2815
|
document.body.appendChild(panel);
|
|
2050
2816
|
this.element = panel;
|
|
2051
|
-
|
|
2817
|
+
this.detachTheme = followThemeClasses(this.editor.view.dom, panel);
|
|
2052
2818
|
panel.focus();
|
|
2053
2819
|
}
|
|
2054
2820
|
/**
|
|
@@ -2101,6 +2867,127 @@ class TableGridPanel {
|
|
|
2101
2867
|
this.highlight(row + 1, col + 1);
|
|
2102
2868
|
}
|
|
2103
2869
|
}
|
|
2870
|
+
class ToolbarMenuPanel {
|
|
2871
|
+
constructor(options) {
|
|
2872
|
+
this.element = null;
|
|
2873
|
+
this.floating = null;
|
|
2874
|
+
this.detachDismiss = null;
|
|
2875
|
+
this.detachTheme = null;
|
|
2876
|
+
this.anchor = null;
|
|
2877
|
+
this.currentKey = null;
|
|
2878
|
+
this.rows = [];
|
|
2879
|
+
this.editor = options.editor;
|
|
2880
|
+
this.onOpenChange = options.onOpenChange;
|
|
2881
|
+
}
|
|
2882
|
+
get isOpen() {
|
|
2883
|
+
return this.element !== null;
|
|
2884
|
+
}
|
|
2885
|
+
get openKey() {
|
|
2886
|
+
return this.currentKey;
|
|
2887
|
+
}
|
|
2888
|
+
toggle(anchor, spec) {
|
|
2889
|
+
if (this.isOpen) {
|
|
2890
|
+
const switching = this.anchor !== anchor;
|
|
2891
|
+
this.close();
|
|
2892
|
+
if (switching) this.open(anchor, spec);
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
this.open(anchor, spec);
|
|
2896
|
+
}
|
|
2897
|
+
close() {
|
|
2898
|
+
var _a, _b, _c, _d;
|
|
2899
|
+
if (!this.element) return;
|
|
2900
|
+
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
2901
|
+
this.detachDismiss = null;
|
|
2902
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
2903
|
+
this.detachTheme = null;
|
|
2904
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
2905
|
+
this.floating = null;
|
|
2906
|
+
this.element.remove();
|
|
2907
|
+
this.element = null;
|
|
2908
|
+
this.anchor = null;
|
|
2909
|
+
this.currentKey = null;
|
|
2910
|
+
this.rows = [];
|
|
2911
|
+
(_d = this.onOpenChange) == null ? void 0 : _d.call(this, false);
|
|
2912
|
+
}
|
|
2913
|
+
destroy() {
|
|
2914
|
+
this.close();
|
|
2915
|
+
}
|
|
2916
|
+
open(anchor, spec) {
|
|
2917
|
+
var _a;
|
|
2918
|
+
const panel = this.build(spec);
|
|
2919
|
+
document.body.appendChild(panel);
|
|
2920
|
+
this.detachTheme = followThemeClasses(this.editorViewDom(anchor), panel);
|
|
2921
|
+
this.element = panel;
|
|
2922
|
+
this.anchor = anchor;
|
|
2923
|
+
this.currentKey = spec.key;
|
|
2924
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
2925
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
2926
|
+
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
2927
|
+
panel.focus();
|
|
2928
|
+
}
|
|
2929
|
+
/**
|
|
2930
|
+
* 主题类要从编辑器根上取,而这里只拿得到一个锚在工具栏上的按钮:
|
|
2931
|
+
* 沿 DOM 往上找 .atri-editor 比让调用方再多传一个参数省事,且工具栏就在那个根里面
|
|
2932
|
+
*/
|
|
2933
|
+
editorViewDom(anchor) {
|
|
2934
|
+
return anchor.closest(".atri-editor");
|
|
2935
|
+
}
|
|
2936
|
+
build(spec) {
|
|
2937
|
+
const panel = document.createElement("div");
|
|
2938
|
+
panel.className = "atri-toolbar-menu";
|
|
2939
|
+
panel.setAttribute("role", "menu");
|
|
2940
|
+
panel.setAttribute("aria-label", spec.label);
|
|
2941
|
+
panel.tabIndex = -1;
|
|
2942
|
+
panel.addEventListener("keydown", (event) => this.onKeyDown(event));
|
|
2943
|
+
this.rows = [];
|
|
2944
|
+
spec.rows.forEach((row) => panel.appendChild(this.makeRow(row)));
|
|
2945
|
+
return panel;
|
|
2946
|
+
}
|
|
2947
|
+
makeRow(row) {
|
|
2948
|
+
const button = document.createElement("button");
|
|
2949
|
+
button.type = "button";
|
|
2950
|
+
button.className = "atri-toolbar-menu-item";
|
|
2951
|
+
button.setAttribute("role", "menuitemradio");
|
|
2952
|
+
button.setAttribute("aria-checked", String(row.active));
|
|
2953
|
+
button.dataset.menuItem = row.id;
|
|
2954
|
+
button.textContent = row.label;
|
|
2955
|
+
button.disabled = row.disabled;
|
|
2956
|
+
if (row.active) button.classList.add("active");
|
|
2957
|
+
button.addEventListener("click", () => {
|
|
2958
|
+
row.run();
|
|
2959
|
+
this.close();
|
|
2960
|
+
this.editor.commands.focus();
|
|
2961
|
+
});
|
|
2962
|
+
this.rows.push(button);
|
|
2963
|
+
return button;
|
|
2964
|
+
}
|
|
2965
|
+
onKeyDown(event) {
|
|
2966
|
+
if (event.key === "Enter" || event.key === " ") return;
|
|
2967
|
+
const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
|
|
2968
|
+
if (step === void 0) return;
|
|
2969
|
+
event.preventDefault();
|
|
2970
|
+
if (this.rows.length === 0) return;
|
|
2971
|
+
const current = this.rows.indexOf(document.activeElement);
|
|
2972
|
+
const next = current < 0 ? step > 0 ? 0 : this.rows.length - 1 : (current + step + this.rows.length) % this.rows.length;
|
|
2973
|
+
this.rows[next].focus();
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
const SET_COMMAND = {
|
|
2977
|
+
color: "setColor",
|
|
2978
|
+
backgroundColor: "setBackgroundColor",
|
|
2979
|
+
fontSize: "setFontSize",
|
|
2980
|
+
fontFamily: "setFontFamily"
|
|
2981
|
+
};
|
|
2982
|
+
function canUseTextStyle(editor, attr) {
|
|
2983
|
+
var _a, _b;
|
|
2984
|
+
const command = editor.commands[SET_COMMAND[attr]];
|
|
2985
|
+
if (typeof command !== "function") return false;
|
|
2986
|
+
return !!((_b = (_a = editor.schema.marks.textStyle) == null ? void 0 : _a.spec.attrs) == null ? void 0 : _b[attr]);
|
|
2987
|
+
}
|
|
2988
|
+
function hasMark(editor, name) {
|
|
2989
|
+
return !!editor.schema.marks[name];
|
|
2990
|
+
}
|
|
2104
2991
|
const icons = {
|
|
2105
2992
|
undo: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13"/></svg>',
|
|
2106
2993
|
redo: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"/></svg>',
|
|
@@ -2139,7 +3026,20 @@ const icons = {
|
|
|
2139
3026
|
tableColumnDelete: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/><path d="M10.5 12h3"/></svg>',
|
|
2140
3027
|
tableMergeCells: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="m10.5 9.5-2.5 2.5 2.5 2.5"/><path d="m13.5 9.5 2.5 2.5-2.5 2.5"/></svg>',
|
|
2141
3028
|
tableSplitCell: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/><path d="M9 12H6"/><path d="M15 12h3"/></svg>',
|
|
2142
|
-
tableHeaderRow: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M7 6h4"/></svg>'
|
|
3029
|
+
tableHeaderRow: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M7 6h4"/></svg>',
|
|
3030
|
+
textColor: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.436-.652-.436-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2Z"/></svg>',
|
|
3031
|
+
highlight: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"/></svg>',
|
|
3032
|
+
highlightColor: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14 7 3-3a2.12 2.12 0 0 1 3 3l-3 3"/><path d="m13 6 5 5"/><path d="M14.7 8.3 8 15v4h4l6.7-6.7a2 2 0 0 0-4-4"/></svg>',
|
|
3033
|
+
fontSize: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m2 18 4.5-11L11 18"/><path d="M3.6 14h5.8"/><path d="m14 18 3-7 3 7"/><path d="M14.9 15.6h4.2"/></svg>',
|
|
3034
|
+
fontFamily: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/></svg>',
|
|
3035
|
+
subscript: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m4 5 6 8"/><path d="m10 5-6 8"/><path d="M14 14.5c.7-1.4 2.8-1.4 3.4 0 .5 1.2-.5 2-1.7 2.6-1.4.7-2.2 1.4-2.2 2.4h4.5"/></svg>',
|
|
3036
|
+
superscript: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m4 9 6 8"/><path d="m10 9-6 8"/><path d="M14 5.5c.7-1.4 2.8-1.4 3.4 0 .5 1.2-.5 2-1.7 2.6-1.4.7-2.2 1.4-2.2 2.4h4.5"/></svg>',
|
|
3037
|
+
clearFormat: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/></svg>',
|
|
3038
|
+
// 下拉触发器用的通用字形:标题菜单与对齐菜单在"当前没有选中任何一级/一种对齐"时用它,
|
|
3039
|
+
// 有当前值时触发器换成那一项自己的图标,所见即所得
|
|
3040
|
+
heading: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 4v16"/><path d="M18 4v16"/><path d="M6 12h12"/></svg>',
|
|
3041
|
+
align: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16"/><path d="M4 10h10"/><path d="M4 14h16"/><path d="M4 18h8"/></svg>',
|
|
3042
|
+
chevronDown: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>'
|
|
2143
3043
|
};
|
|
2144
3044
|
const TOOLTIP_KEYS = {
|
|
2145
3045
|
undo: "editor.undo",
|
|
@@ -2147,6 +3047,13 @@ const TOOLTIP_KEYS = {
|
|
|
2147
3047
|
heading1: "editor.heading1",
|
|
2148
3048
|
heading2: "editor.heading2",
|
|
2149
3049
|
heading3: "editor.heading3",
|
|
3050
|
+
heading4: "editor.heading4",
|
|
3051
|
+
heading5: "editor.heading5",
|
|
3052
|
+
heading6: "editor.heading6",
|
|
3053
|
+
// 两个下拉触发器。'editor.heading' 这条词条躺了很久、背后没有任何功能,
|
|
3054
|
+
// 这批终于有了真正读它的人
|
|
3055
|
+
heading: "editor.heading",
|
|
3056
|
+
align: "editor.align",
|
|
2150
3057
|
paragraph: "editor.paragraph",
|
|
2151
3058
|
bold: "editor.bold",
|
|
2152
3059
|
italic: "editor.italic",
|
|
@@ -2168,7 +3075,15 @@ const TOOLTIP_KEYS = {
|
|
|
2168
3075
|
attachmentDisplay: "editor.attachmentDisplay",
|
|
2169
3076
|
insertTable: "editor.table",
|
|
2170
3077
|
insertLink: "editor.link",
|
|
2171
|
-
delete: "editor.delete"
|
|
3078
|
+
delete: "editor.delete",
|
|
3079
|
+
textColor: "editor.textColor",
|
|
3080
|
+
highlight: "editor.highlight",
|
|
3081
|
+
highlightColor: "editor.highlightColor",
|
|
3082
|
+
fontSize: "editor.fontSize",
|
|
3083
|
+
fontFamily: "editor.fontFamily",
|
|
3084
|
+
subscript: "editor.subscript",
|
|
3085
|
+
superscript: "editor.superscript",
|
|
3086
|
+
clearFormat: "editor.clearFormat"
|
|
2172
3087
|
};
|
|
2173
3088
|
class ToolbarManager {
|
|
2174
3089
|
constructor(editor, container, config, i18n, mediaRuntime, linkPanel) {
|
|
@@ -2178,6 +3093,8 @@ class ToolbarManager {
|
|
|
2178
3093
|
this.createdElements = [];
|
|
2179
3094
|
this.panel = null;
|
|
2180
3095
|
this.tablePanel = null;
|
|
3096
|
+
this.stylePanel = null;
|
|
3097
|
+
this.menuPanel = null;
|
|
2181
3098
|
this.editor = editor;
|
|
2182
3099
|
this.container = container;
|
|
2183
3100
|
this.config = config;
|
|
@@ -2256,6 +3173,150 @@ class ToolbarManager {
|
|
|
2256
3173
|
get tableExtensionRegistered() {
|
|
2257
3174
|
return canInsertTable(this.editor);
|
|
2258
3175
|
}
|
|
3176
|
+
/** 格式面板按需创建:没点开过就不该有一个实例挂在编辑器上 */
|
|
3177
|
+
get styleOptionPanel() {
|
|
3178
|
+
this.stylePanel ?? (this.stylePanel = new StyleOptionPanel({
|
|
3179
|
+
editor: this.editor,
|
|
3180
|
+
i18n: this.i18n,
|
|
3181
|
+
// 开合不改文档,等不到 transaction,得主动刷一次按钮状态
|
|
3182
|
+
onOpenChange: () => this.updateButtonStates()
|
|
3183
|
+
}));
|
|
3184
|
+
return this.stylePanel;
|
|
3185
|
+
}
|
|
3186
|
+
tOr(key, fallback) {
|
|
3187
|
+
var _a;
|
|
3188
|
+
return ((_a = this.i18n) == null ? void 0 : _a.tOr(key, fallback)) ?? fallback;
|
|
3189
|
+
}
|
|
3190
|
+
/** 下拉菜单面板按需创建:没点开过就不该有一个实例挂在编辑器上 */
|
|
3191
|
+
get menu() {
|
|
3192
|
+
this.menuPanel ?? (this.menuPanel = new ToolbarMenuPanel({
|
|
3193
|
+
editor: this.editor,
|
|
3194
|
+
// 开合不改文档,等不到 transaction,得主动刷一次按钮状态
|
|
3195
|
+
onOpenChange: () => this.updateButtonStates()
|
|
3196
|
+
}));
|
|
3197
|
+
return this.menuPanel;
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* 把一个项做成下拉触发器。
|
|
3201
|
+
* 子项走的仍是同一张 itemDefs 表,所以它们和顶栏项一样受能力门控——
|
|
3202
|
+
* 表格被顶掉时 insertTable 就不会出现在菜单里,而不是摆一行按了没反应的
|
|
3203
|
+
*/
|
|
3204
|
+
menuItem(key, icon, children) {
|
|
3205
|
+
const def = {
|
|
3206
|
+
id: key,
|
|
3207
|
+
icon,
|
|
3208
|
+
popup: "menu",
|
|
3209
|
+
children,
|
|
3210
|
+
command: (_editor, button) => {
|
|
3211
|
+
var _a;
|
|
3212
|
+
const live = ((_a = this.itemDefs.get(key)) == null ? void 0 : _a.children) ?? children;
|
|
3213
|
+
this.menu.toggle(button, this.menuSpec(key, live, button));
|
|
3214
|
+
},
|
|
3215
|
+
isActive: () => {
|
|
3216
|
+
var _a;
|
|
3217
|
+
return ((_a = this.menuPanel) == null ? void 0 : _a.openKey) === key;
|
|
3218
|
+
},
|
|
3219
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3220
|
+
};
|
|
3221
|
+
return def;
|
|
3222
|
+
}
|
|
3223
|
+
/** 当前命中的子项:触发器的图标与提示都跟着它走,用户不必点开就知道现在的级别 */
|
|
3224
|
+
activeChild(def) {
|
|
3225
|
+
for (const childId of def.children ?? []) {
|
|
3226
|
+
const child = this.itemDefs.get(childId);
|
|
3227
|
+
if (child && !child.popup && child.isActive(this.editor) && !child.isDisabled(this.editor)) {
|
|
3228
|
+
return child;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
return null;
|
|
3232
|
+
}
|
|
3233
|
+
/** 子项词条 key;没有映射的(宿主的自定义项)退回定义里的兜底文案 */
|
|
3234
|
+
rowLabel(childId, child) {
|
|
3235
|
+
const key = TOOLTIP_KEYS[childId];
|
|
3236
|
+
return key ? this.tOr(key, zhText(key)) : child.tooltip ?? childId;
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* 菜单里带浮层的子项一律不收:子项被点中后菜单就关了,
|
|
3240
|
+
* 而面板要锚在触发器上——锚一个即将从文档里消失的行没有意义。
|
|
3241
|
+
* 这与 attachBubbleToolbar 拒绝浮层项是同一条理由。
|
|
3242
|
+
*/
|
|
3243
|
+
menuSpec(key, children, trigger) {
|
|
3244
|
+
const menuKey = TOOLTIP_KEYS[key];
|
|
3245
|
+
return {
|
|
3246
|
+
key,
|
|
3247
|
+
label: menuKey ? this.tOr(menuKey, zhText(menuKey)) : key,
|
|
3248
|
+
rows: children.reduce((rows, childId) => {
|
|
3249
|
+
const child = this.itemDefs.get(childId);
|
|
3250
|
+
if (!child) {
|
|
3251
|
+
console.warn(`[Atri Editor] Menu "${key}" declares unknown item "${childId}".`);
|
|
3252
|
+
return rows;
|
|
3253
|
+
}
|
|
3254
|
+
if (child.popup) {
|
|
3255
|
+
console.warn(
|
|
3256
|
+
`[Atri Editor] Menu item "${childId}" opens a panel, which menus cannot host.`
|
|
3257
|
+
);
|
|
3258
|
+
return rows;
|
|
3259
|
+
}
|
|
3260
|
+
rows.push({
|
|
3261
|
+
id: childId,
|
|
3262
|
+
label: this.rowLabel(childId, child),
|
|
3263
|
+
active: child.isActive(this.editor),
|
|
3264
|
+
disabled: child.isDisabled(this.editor),
|
|
3265
|
+
// 子项自己不需要触发器元素(只有浮层项会用),传过去是为了共用同一份定义
|
|
3266
|
+
run: () => child.command(this.editor, trigger)
|
|
3267
|
+
});
|
|
3268
|
+
return rows;
|
|
3269
|
+
}, [])
|
|
3270
|
+
};
|
|
3271
|
+
}
|
|
3272
|
+
/**
|
|
3273
|
+
* 面板文案在这里现取,不在建项时算好存下来:
|
|
3274
|
+
* 语言切换后重开面板要拿到新词条,而项定义只在建栏时求值一次
|
|
3275
|
+
*/
|
|
3276
|
+
styleSpec(key, parts) {
|
|
3277
|
+
return {
|
|
3278
|
+
key,
|
|
3279
|
+
label: this.tOr(`editor.${key}`, key),
|
|
3280
|
+
shape: parts.shape,
|
|
3281
|
+
options: parts.options,
|
|
3282
|
+
// 色板才有"其他颜色"这一行;字号字体用原生控件选不出任意值
|
|
3283
|
+
customColorLabel: parts.swatch ? this.tOr("formatting.customColor", "其他颜色…") : void 0,
|
|
3284
|
+
// 清除行的措辞按形状分:颜色去掉是"无",字号字体退回的是"默认"
|
|
3285
|
+
clearLabel: parts.swatch ? this.tOr("formatting.noColor", "无") : this.tOr("formatting.defaultValue", "默认"),
|
|
3286
|
+
onPick: (value) => this.applyStyle(parts.attr, value)
|
|
3287
|
+
};
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* 空串是"清除"而不是"设成空值":设成空串会留下一个属性为空串的 mark,
|
|
3291
|
+
* 而 textStyle 的 renderHTML 对空值照样会吐出一个 `<span style="">`,
|
|
3292
|
+
* 那种节点既看不见又会在往返里产噪音(见 extensions/formatting.ts 的发出侧)
|
|
3293
|
+
*/
|
|
3294
|
+
applyStyle(attr, value) {
|
|
3295
|
+
const chain = this.editor.chain().focus();
|
|
3296
|
+
if (attr === "highlight") {
|
|
3297
|
+
if (value === "") chain.unsetHighlight().run();
|
|
3298
|
+
else chain.setHighlight({ color: value }).run();
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
switch (attr) {
|
|
3302
|
+
case "color":
|
|
3303
|
+
if (value === "") chain.unsetColor().run();
|
|
3304
|
+
else chain.setColor(value).run();
|
|
3305
|
+
return;
|
|
3306
|
+
case "backgroundColor":
|
|
3307
|
+
if (value === "") chain.unsetBackgroundColor().run();
|
|
3308
|
+
else chain.setBackgroundColor(value).run();
|
|
3309
|
+
return;
|
|
3310
|
+
case "fontSize":
|
|
3311
|
+
if (value === "") chain.unsetFontSize().run();
|
|
3312
|
+
else chain.setFontSize(value).run();
|
|
3313
|
+
return;
|
|
3314
|
+
case "fontFamily":
|
|
3315
|
+
if (value === "") chain.unsetFontFamily().run();
|
|
3316
|
+
else chain.setFontFamily(value).run();
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
2259
3320
|
/**
|
|
2260
3321
|
* 创建工具栏 DOM - 直接将按钮插入 container
|
|
2261
3322
|
*/
|
|
@@ -2275,18 +3336,23 @@ class ToolbarManager {
|
|
|
2275
3336
|
}
|
|
2276
3337
|
const layout = [
|
|
2277
3338
|
["undo", "redo"],
|
|
2278
|
-
|
|
3339
|
+
// 标题收成一下拉:一到六级加正文七个取值平铺要占七格,而一次只用得上一个
|
|
3340
|
+
["heading"],
|
|
2279
3341
|
// 链接跟着行内格式走:它也是一段文字上的标记,只是要填地址所以带浮层
|
|
2280
3342
|
["bold", "italic", "underline", "strike", "code", "insertLink"],
|
|
3343
|
+
// 颜色与字号字体自成一组:都是"这段文字长什么样",且除 highlight 外都带浮层
|
|
3344
|
+
["textColor", "highlight", "highlightColor", "fontSize", "fontFamily"],
|
|
2281
3345
|
["bulletList", "orderedList", "taskList", "blockquote", "codeBlock"],
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
//
|
|
2285
|
-
["horizontalRule",
|
|
2286
|
-
//
|
|
2287
|
-
|
|
3346
|
+
// 对齐同样收成一下拉
|
|
3347
|
+
["align"],
|
|
3348
|
+
// 分割线与表格是一组:都是"往正文里插一块"的动作,没有激活态
|
|
3349
|
+
["horizontalRule", "insertTable"],
|
|
3350
|
+
// 上下标与清除格式:作用于选区已有的格式,而不是加一种新格式
|
|
3351
|
+
["subscript", "superscript", "clearFormat"],
|
|
3352
|
+
["insertImage", "insertAttachment"]
|
|
2288
3353
|
];
|
|
2289
|
-
layout.
|
|
3354
|
+
const groups = layout.filter((group) => group.some((itemId) => this.itemDefs.has(itemId)));
|
|
3355
|
+
groups.forEach((group, groupIndex) => {
|
|
2290
3356
|
if (groupIndex > 0) {
|
|
2291
3357
|
const separator = document.createElement("div");
|
|
2292
3358
|
separator.className = "atri-editor-toolbar-separator";
|
|
@@ -2318,7 +3384,22 @@ class ToolbarManager {
|
|
|
2318
3384
|
return { id, def };
|
|
2319
3385
|
}
|
|
2320
3386
|
if ((_a = item.children) == null ? void 0 : _a.length) {
|
|
2321
|
-
|
|
3387
|
+
if (def.popup !== "menu") {
|
|
3388
|
+
console.warn(
|
|
3389
|
+
`[Atri Editor] Toolbar item "${id}" declares children, but it is not a dropdown; only the built-in dropdown items (heading, align) accept children.`
|
|
3390
|
+
);
|
|
3391
|
+
} else {
|
|
3392
|
+
const nested = item.children.filter((child) => {
|
|
3393
|
+
var _a2;
|
|
3394
|
+
return (_a2 = child.children) == null ? void 0 : _a2.length;
|
|
3395
|
+
});
|
|
3396
|
+
if (nested.length) {
|
|
3397
|
+
console.warn(
|
|
3398
|
+
`[Atri Editor] Nested children on "${nested.map((child) => child.id).join(", ")}" are not supported and were ignored.`
|
|
3399
|
+
);
|
|
3400
|
+
}
|
|
3401
|
+
def.children = item.children.map((child) => child.id);
|
|
3402
|
+
}
|
|
2322
3403
|
}
|
|
2323
3404
|
return { id, def, overrides: item };
|
|
2324
3405
|
}
|
|
@@ -2338,9 +3419,14 @@ class ToolbarManager {
|
|
|
2338
3419
|
this.customTooltips.set(itemId, overrides.tooltip);
|
|
2339
3420
|
}
|
|
2340
3421
|
if (itemDef.popup) {
|
|
2341
|
-
button.setAttribute("aria-haspopup", "dialog");
|
|
3422
|
+
button.setAttribute("aria-haspopup", itemDef.popup === "menu" ? "menu" : "dialog");
|
|
2342
3423
|
button.setAttribute("aria-expanded", "false");
|
|
2343
3424
|
}
|
|
3425
|
+
if (itemDef.popup === "menu") {
|
|
3426
|
+
button.classList.add("atri-editor-toolbar-btn--menu");
|
|
3427
|
+
button.dataset.menuIcon = button.innerHTML;
|
|
3428
|
+
button.innerHTML += icons.chevronDown;
|
|
3429
|
+
}
|
|
2344
3430
|
button.addEventListener("click", (e) => {
|
|
2345
3431
|
e.preventDefault();
|
|
2346
3432
|
e.stopPropagation();
|
|
@@ -2360,12 +3446,9 @@ class ToolbarManager {
|
|
|
2360
3446
|
*/
|
|
2361
3447
|
applyTooltips() {
|
|
2362
3448
|
this.buttons.forEach((buttons, itemId) => {
|
|
2363
|
-
var _a;
|
|
2364
3449
|
const itemDef = this.itemDefs.get(itemId);
|
|
2365
3450
|
if (!itemDef) return;
|
|
2366
|
-
const
|
|
2367
|
-
const key = TOOLTIP_KEYS[itemId];
|
|
2368
|
-
const title = custom ?? (key ? ((_a = this.i18n) == null ? void 0 : _a.tOr(key, itemDef.tooltip)) ?? itemDef.tooltip : itemDef.tooltip);
|
|
3451
|
+
const title = this.itemTitle(itemId, itemDef);
|
|
2369
3452
|
buttons.forEach((button) => {
|
|
2370
3453
|
button.title = title;
|
|
2371
3454
|
});
|
|
@@ -2380,7 +3463,6 @@ class ToolbarManager {
|
|
|
2380
3463
|
items.set("insertImage", {
|
|
2381
3464
|
id: "insertImage",
|
|
2382
3465
|
icon: icons.image,
|
|
2383
|
-
tooltip: "图片",
|
|
2384
3466
|
popup: true,
|
|
2385
3467
|
command: (_editor, button) => this.openInsert("image", button),
|
|
2386
3468
|
isActive: () => {
|
|
@@ -2392,7 +3474,6 @@ class ToolbarManager {
|
|
|
2392
3474
|
items.set("insertAttachment", {
|
|
2393
3475
|
id: "insertAttachment",
|
|
2394
3476
|
icon: icons.paperclip,
|
|
2395
|
-
tooltip: "附件",
|
|
2396
3477
|
popup: true,
|
|
2397
3478
|
command: (_editor, button) => this.openInsert("attachment", button),
|
|
2398
3479
|
isActive: () => {
|
|
@@ -2404,7 +3485,6 @@ class ToolbarManager {
|
|
|
2404
3485
|
items.set("attachmentDisplay", {
|
|
2405
3486
|
id: "attachmentDisplay",
|
|
2406
3487
|
icon: icons.link,
|
|
2407
|
-
tooltip: "附件样式",
|
|
2408
3488
|
command: (editor) => editor.chain().focus().toggleAttachmentDisplay().run(),
|
|
2409
3489
|
isActive: (editor) => editor.isActive("attachment") || editor.isActive("attachmentLink"),
|
|
2410
3490
|
isDisabled: (editor) => !editor.isActive("attachment") && !editor.isActive("attachmentLink")
|
|
@@ -2414,7 +3494,6 @@ class ToolbarManager {
|
|
|
2414
3494
|
items.set("insertTable", {
|
|
2415
3495
|
id: "insertTable",
|
|
2416
3496
|
icon: icons.table,
|
|
2417
|
-
tooltip: "表格",
|
|
2418
3497
|
popup: true,
|
|
2419
3498
|
command: (_editor, button) => {
|
|
2420
3499
|
var _a;
|
|
@@ -2431,7 +3510,6 @@ class ToolbarManager {
|
|
|
2431
3510
|
items.set("insertLink", {
|
|
2432
3511
|
id: "insertLink",
|
|
2433
3512
|
icon: icons.link,
|
|
2434
|
-
tooltip: "链接",
|
|
2435
3513
|
popup: true,
|
|
2436
3514
|
command: (_editor, button) => {
|
|
2437
3515
|
var _a;
|
|
@@ -2445,10 +3523,127 @@ class ToolbarManager {
|
|
|
2445
3523
|
isDisabled: (editor) => !editor.isEditable
|
|
2446
3524
|
});
|
|
2447
3525
|
}
|
|
3526
|
+
const textStyleReady = (attr) => canUseTextStyle(this.editor, attr);
|
|
3527
|
+
if (textStyleReady("color")) {
|
|
3528
|
+
items.set("textColor", {
|
|
3529
|
+
id: "textColor",
|
|
3530
|
+
icon: icons.textColor,
|
|
3531
|
+
popup: true,
|
|
3532
|
+
command: (_editor, button) => this.styleOptionPanel.toggle(
|
|
3533
|
+
button,
|
|
3534
|
+
this.styleSpec("textColor", {
|
|
3535
|
+
shape: "grid",
|
|
3536
|
+
options: TEXT_COLORS,
|
|
3537
|
+
swatch: true,
|
|
3538
|
+
attr: "color"
|
|
3539
|
+
})
|
|
3540
|
+
),
|
|
3541
|
+
// 浮层项的按下态只跟浮层走:选区里有没有颜色是文档事实,不是"这个按钮正按着"
|
|
3542
|
+
isActive: () => {
|
|
3543
|
+
var _a;
|
|
3544
|
+
return ((_a = this.stylePanel) == null ? void 0 : _a.openKey) === "textColor";
|
|
3545
|
+
},
|
|
3546
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
if (hasMark(this.editor, "highlight")) {
|
|
3550
|
+
items.set("highlight", {
|
|
3551
|
+
id: "highlight",
|
|
3552
|
+
icon: icons.highlight,
|
|
3553
|
+
command: (editor) => editor.chain().focus().toggleHighlight().run(),
|
|
3554
|
+
isActive: (editor) => editor.isActive("highlight"),
|
|
3555
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3556
|
+
});
|
|
3557
|
+
items.set("highlightColor", {
|
|
3558
|
+
id: "highlightColor",
|
|
3559
|
+
icon: icons.highlightColor,
|
|
3560
|
+
popup: true,
|
|
3561
|
+
command: (_editor, button) => this.styleOptionPanel.toggle(
|
|
3562
|
+
button,
|
|
3563
|
+
this.styleSpec("highlightColor", {
|
|
3564
|
+
shape: "grid",
|
|
3565
|
+
options: HIGHLIGHT_COLORS,
|
|
3566
|
+
swatch: true,
|
|
3567
|
+
attr: "highlight"
|
|
3568
|
+
})
|
|
3569
|
+
),
|
|
3570
|
+
isActive: () => {
|
|
3571
|
+
var _a;
|
|
3572
|
+
return ((_a = this.stylePanel) == null ? void 0 : _a.openKey) === "highlightColor";
|
|
3573
|
+
},
|
|
3574
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
if (textStyleReady("fontSize")) {
|
|
3578
|
+
items.set("fontSize", {
|
|
3579
|
+
id: "fontSize",
|
|
3580
|
+
icon: icons.fontSize,
|
|
3581
|
+
popup: true,
|
|
3582
|
+
command: (_editor, button) => this.styleOptionPanel.toggle(
|
|
3583
|
+
button,
|
|
3584
|
+
this.styleSpec("fontSize", {
|
|
3585
|
+
shape: "list",
|
|
3586
|
+
options: FONT_SIZES,
|
|
3587
|
+
swatch: false,
|
|
3588
|
+
attr: "fontSize"
|
|
3589
|
+
})
|
|
3590
|
+
),
|
|
3591
|
+
isActive: () => {
|
|
3592
|
+
var _a;
|
|
3593
|
+
return ((_a = this.stylePanel) == null ? void 0 : _a.openKey) === "fontSize";
|
|
3594
|
+
},
|
|
3595
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
if (textStyleReady("fontFamily")) {
|
|
3599
|
+
items.set("fontFamily", {
|
|
3600
|
+
id: "fontFamily",
|
|
3601
|
+
icon: icons.fontFamily,
|
|
3602
|
+
popup: true,
|
|
3603
|
+
command: (_editor, button) => this.styleOptionPanel.toggle(
|
|
3604
|
+
button,
|
|
3605
|
+
this.styleSpec("fontFamily", {
|
|
3606
|
+
shape: "list",
|
|
3607
|
+
options: FONT_FAMILIES,
|
|
3608
|
+
swatch: false,
|
|
3609
|
+
attr: "fontFamily"
|
|
3610
|
+
})
|
|
3611
|
+
),
|
|
3612
|
+
isActive: () => {
|
|
3613
|
+
var _a;
|
|
3614
|
+
return ((_a = this.stylePanel) == null ? void 0 : _a.openKey) === "fontFamily";
|
|
3615
|
+
},
|
|
3616
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3617
|
+
});
|
|
3618
|
+
}
|
|
3619
|
+
if (hasMark(this.editor, "subscript")) {
|
|
3620
|
+
items.set("subscript", {
|
|
3621
|
+
id: "subscript",
|
|
3622
|
+
icon: icons.subscript,
|
|
3623
|
+
command: (editor) => editor.chain().focus().toggleSubscript().run(),
|
|
3624
|
+
isActive: (editor) => editor.isActive("subscript"),
|
|
3625
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3626
|
+
});
|
|
3627
|
+
}
|
|
3628
|
+
if (hasMark(this.editor, "superscript")) {
|
|
3629
|
+
items.set("superscript", {
|
|
3630
|
+
id: "superscript",
|
|
3631
|
+
icon: icons.superscript,
|
|
3632
|
+
command: (editor) => editor.chain().focus().toggleSuperscript().run(),
|
|
3633
|
+
isActive: (editor) => editor.isActive("superscript"),
|
|
3634
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
items.set("clearFormat", {
|
|
3638
|
+
id: "clearFormat",
|
|
3639
|
+
icon: icons.clearFormat,
|
|
3640
|
+
command: (editor) => editor.chain().focus().unsetAllMarks().clearNodes().run(),
|
|
3641
|
+
isActive: () => false,
|
|
3642
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3643
|
+
});
|
|
2448
3644
|
items.set("delete", {
|
|
2449
3645
|
id: "delete",
|
|
2450
3646
|
icon: icons.trash,
|
|
2451
|
-
tooltip: "删除",
|
|
2452
3647
|
command: (editor) => editor.chain().deleteSelection().run(),
|
|
2453
3648
|
isActive: () => false,
|
|
2454
3649
|
isDisabled: (editor) => !isNodeSelection(editor.state.selection)
|
|
@@ -2456,7 +3651,6 @@ class ToolbarManager {
|
|
|
2456
3651
|
items.set("undo", {
|
|
2457
3652
|
id: "undo",
|
|
2458
3653
|
icon: icons.undo,
|
|
2459
|
-
tooltip: "撤销",
|
|
2460
3654
|
command: (editor) => editor.chain().undo().run(),
|
|
2461
3655
|
isActive: () => false,
|
|
2462
3656
|
isDisabled: (editor) => !editor.can().undo()
|
|
@@ -2464,39 +3658,23 @@ class ToolbarManager {
|
|
|
2464
3658
|
items.set("redo", {
|
|
2465
3659
|
id: "redo",
|
|
2466
3660
|
icon: icons.redo,
|
|
2467
|
-
tooltip: "重做",
|
|
2468
3661
|
command: (editor) => editor.chain().redo().run(),
|
|
2469
3662
|
isActive: () => false,
|
|
2470
3663
|
isDisabled: (editor) => !editor.can().redo()
|
|
2471
3664
|
});
|
|
2472
|
-
|
|
2473
|
-
id
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
id: "heading2",
|
|
2482
|
-
icon: icons.heading2,
|
|
2483
|
-
tooltip: "标题 2",
|
|
2484
|
-
command: (editor) => editor.chain().toggleHeading({ level: 2 }).run(),
|
|
2485
|
-
isActive: (editor) => editor.isActive("heading", { level: 2 }),
|
|
2486
|
-
isDisabled: (editor) => !editor.can().toggleHeading({ level: 2 })
|
|
2487
|
-
});
|
|
2488
|
-
items.set("heading3", {
|
|
2489
|
-
id: "heading3",
|
|
2490
|
-
icon: icons.heading3,
|
|
2491
|
-
tooltip: "标题 3",
|
|
2492
|
-
command: (editor) => editor.chain().toggleHeading({ level: 3 }).run(),
|
|
2493
|
-
isActive: (editor) => editor.isActive("heading", { level: 3 }),
|
|
2494
|
-
isDisabled: (editor) => !editor.can().toggleHeading({ level: 3 })
|
|
3665
|
+
[1, 2, 3, 4, 5, 6].forEach((level) => {
|
|
3666
|
+
const id = `heading${level}`;
|
|
3667
|
+
items.set(id, {
|
|
3668
|
+
id,
|
|
3669
|
+
icon: level <= 3 ? icons[id] : icons.heading,
|
|
3670
|
+
command: (editor) => editor.chain().toggleHeading({ level }).run(),
|
|
3671
|
+
isActive: (editor) => editor.isActive("heading", { level }),
|
|
3672
|
+
isDisabled: (editor) => !editor.can().toggleHeading({ level })
|
|
3673
|
+
});
|
|
2495
3674
|
});
|
|
2496
3675
|
items.set("paragraph", {
|
|
2497
3676
|
id: "paragraph",
|
|
2498
3677
|
icon: icons.paragraph,
|
|
2499
|
-
tooltip: "正文",
|
|
2500
3678
|
command: (editor) => editor.chain().setParagraph().run(),
|
|
2501
3679
|
isActive: (editor) => editor.isActive("paragraph"),
|
|
2502
3680
|
isDisabled: (editor) => !editor.can().setParagraph()
|
|
@@ -2504,7 +3682,6 @@ class ToolbarManager {
|
|
|
2504
3682
|
items.set("bold", {
|
|
2505
3683
|
id: "bold",
|
|
2506
3684
|
icon: icons.bold,
|
|
2507
|
-
tooltip: "加粗",
|
|
2508
3685
|
command: (editor) => editor.chain().toggleBold().run(),
|
|
2509
3686
|
isActive: (editor) => editor.isActive("bold"),
|
|
2510
3687
|
isDisabled: (editor) => !editor.can().toggleBold()
|
|
@@ -2512,7 +3689,6 @@ class ToolbarManager {
|
|
|
2512
3689
|
items.set("italic", {
|
|
2513
3690
|
id: "italic",
|
|
2514
3691
|
icon: icons.italic,
|
|
2515
|
-
tooltip: "斜体",
|
|
2516
3692
|
command: (editor) => editor.chain().toggleItalic().run(),
|
|
2517
3693
|
isActive: (editor) => editor.isActive("italic"),
|
|
2518
3694
|
isDisabled: (editor) => !editor.can().toggleItalic()
|
|
@@ -2520,7 +3696,6 @@ class ToolbarManager {
|
|
|
2520
3696
|
items.set("underline", {
|
|
2521
3697
|
id: "underline",
|
|
2522
3698
|
icon: icons.underline,
|
|
2523
|
-
tooltip: "下划线",
|
|
2524
3699
|
command: (editor) => editor.chain().toggleUnderline().run(),
|
|
2525
3700
|
isActive: (editor) => editor.isActive("underline"),
|
|
2526
3701
|
isDisabled: (editor) => !editor.can().toggleUnderline()
|
|
@@ -2528,7 +3703,6 @@ class ToolbarManager {
|
|
|
2528
3703
|
items.set("strike", {
|
|
2529
3704
|
id: "strike",
|
|
2530
3705
|
icon: icons.strikethrough,
|
|
2531
|
-
tooltip: "删除线",
|
|
2532
3706
|
command: (editor) => editor.chain().toggleStrike().run(),
|
|
2533
3707
|
isActive: (editor) => editor.isActive("strike"),
|
|
2534
3708
|
isDisabled: (editor) => !editor.can().toggleStrike()
|
|
@@ -2536,7 +3710,6 @@ class ToolbarManager {
|
|
|
2536
3710
|
items.set("code", {
|
|
2537
3711
|
id: "code",
|
|
2538
3712
|
icon: icons.code,
|
|
2539
|
-
tooltip: "行内代码",
|
|
2540
3713
|
command: (editor) => editor.chain().toggleCode().run(),
|
|
2541
3714
|
isActive: (editor) => editor.isActive("code"),
|
|
2542
3715
|
isDisabled: (editor) => !editor.can().toggleCode()
|
|
@@ -2544,7 +3717,6 @@ class ToolbarManager {
|
|
|
2544
3717
|
items.set("bulletList", {
|
|
2545
3718
|
id: "bulletList",
|
|
2546
3719
|
icon: icons.list,
|
|
2547
|
-
tooltip: "无序列表",
|
|
2548
3720
|
command: (editor) => editor.chain().toggleBulletList().run(),
|
|
2549
3721
|
isActive: (editor) => editor.isActive("bulletList"),
|
|
2550
3722
|
isDisabled: (editor) => !editor.can().toggleBulletList()
|
|
@@ -2552,7 +3724,6 @@ class ToolbarManager {
|
|
|
2552
3724
|
items.set("orderedList", {
|
|
2553
3725
|
id: "orderedList",
|
|
2554
3726
|
icon: icons.listOrdered,
|
|
2555
|
-
tooltip: "有序列表",
|
|
2556
3727
|
command: (editor) => editor.chain().toggleOrderedList().run(),
|
|
2557
3728
|
isActive: (editor) => editor.isActive("orderedList"),
|
|
2558
3729
|
isDisabled: (editor) => !editor.can().toggleOrderedList()
|
|
@@ -2560,7 +3731,6 @@ class ToolbarManager {
|
|
|
2560
3731
|
items.set("taskList", {
|
|
2561
3732
|
id: "taskList",
|
|
2562
3733
|
icon: icons.listChecks,
|
|
2563
|
-
tooltip: "任务列表",
|
|
2564
3734
|
command: (editor) => editor.chain().toggleTaskList().run(),
|
|
2565
3735
|
isActive: (editor) => editor.isActive("taskList"),
|
|
2566
3736
|
isDisabled: (editor) => !editor.can().toggleTaskList()
|
|
@@ -2568,23 +3738,22 @@ class ToolbarManager {
|
|
|
2568
3738
|
items.set("blockquote", {
|
|
2569
3739
|
id: "blockquote",
|
|
2570
3740
|
icon: icons.quote,
|
|
2571
|
-
tooltip: "引用",
|
|
2572
3741
|
command: (editor) => editor.chain().toggleBlockquote().run(),
|
|
2573
3742
|
isActive: (editor) => editor.isActive("blockquote"),
|
|
2574
3743
|
isDisabled: (editor) => !editor.can().toggleBlockquote()
|
|
2575
3744
|
});
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
3745
|
+
if (this.editor.schema.nodes.codeBlock) {
|
|
3746
|
+
items.set("codeBlock", {
|
|
3747
|
+
id: "codeBlock",
|
|
3748
|
+
icon: icons.codeBlock,
|
|
3749
|
+
command: (editor) => editor.chain().toggleCodeBlock().run(),
|
|
3750
|
+
isActive: (editor) => editor.isActive("codeBlock"),
|
|
3751
|
+
isDisabled: (editor) => !editor.can().toggleCodeBlock()
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
2584
3754
|
items.set("horizontalRule", {
|
|
2585
3755
|
id: "horizontalRule",
|
|
2586
3756
|
icon: icons.horizontalRule,
|
|
2587
|
-
tooltip: "分割线",
|
|
2588
3757
|
command: (editor) => editor.chain().setHorizontalRule().run(),
|
|
2589
3758
|
isActive: () => false,
|
|
2590
3759
|
isDisabled: (editor) => !editor.can().setHorizontalRule()
|
|
@@ -2592,7 +3761,6 @@ class ToolbarManager {
|
|
|
2592
3761
|
items.set("alignLeft", {
|
|
2593
3762
|
id: "alignLeft",
|
|
2594
3763
|
icon: icons.alignLeft,
|
|
2595
|
-
tooltip: "左对齐",
|
|
2596
3764
|
command: (editor) => editor.chain().setTextAlign("left").run(),
|
|
2597
3765
|
isActive: (editor) => editor.isActive({ textAlign: "left" }),
|
|
2598
3766
|
isDisabled: (editor) => !editor.can().setTextAlign("left")
|
|
@@ -2600,7 +3768,6 @@ class ToolbarManager {
|
|
|
2600
3768
|
items.set("alignCenter", {
|
|
2601
3769
|
id: "alignCenter",
|
|
2602
3770
|
icon: icons.alignCenter,
|
|
2603
|
-
tooltip: "居中",
|
|
2604
3771
|
command: (editor) => editor.chain().setTextAlign("center").run(),
|
|
2605
3772
|
isActive: (editor) => editor.isActive({ textAlign: "center" }),
|
|
2606
3773
|
isDisabled: (editor) => !editor.can().setTextAlign("center")
|
|
@@ -2608,7 +3775,6 @@ class ToolbarManager {
|
|
|
2608
3775
|
items.set("alignRight", {
|
|
2609
3776
|
id: "alignRight",
|
|
2610
3777
|
icon: icons.alignRight,
|
|
2611
|
-
tooltip: "右对齐",
|
|
2612
3778
|
command: (editor) => editor.chain().setTextAlign("right").run(),
|
|
2613
3779
|
isActive: (editor) => editor.isActive({ textAlign: "right" }),
|
|
2614
3780
|
isDisabled: (editor) => !editor.can().setTextAlign("right")
|
|
@@ -2616,23 +3782,44 @@ class ToolbarManager {
|
|
|
2616
3782
|
items.set("alignJustify", {
|
|
2617
3783
|
id: "alignJustify",
|
|
2618
3784
|
icon: icons.alignJustify,
|
|
2619
|
-
tooltip: "两端对齐",
|
|
2620
3785
|
command: (editor) => editor.chain().setTextAlign("justify").run(),
|
|
2621
3786
|
isActive: (editor) => editor.isActive({ textAlign: "justify" }),
|
|
2622
3787
|
isDisabled: (editor) => !editor.can().setTextAlign("justify")
|
|
2623
3788
|
});
|
|
3789
|
+
items.set(
|
|
3790
|
+
"heading",
|
|
3791
|
+
this.menuItem("heading", icons.heading, [
|
|
3792
|
+
"heading1",
|
|
3793
|
+
"heading2",
|
|
3794
|
+
"heading3",
|
|
3795
|
+
"heading4",
|
|
3796
|
+
"heading5",
|
|
3797
|
+
"heading6",
|
|
3798
|
+
"paragraph"
|
|
3799
|
+
])
|
|
3800
|
+
);
|
|
3801
|
+
items.set(
|
|
3802
|
+
"align",
|
|
3803
|
+
this.menuItem("align", icons.align, [
|
|
3804
|
+
"alignLeft",
|
|
3805
|
+
"alignCenter",
|
|
3806
|
+
"alignRight",
|
|
3807
|
+
"alignJustify"
|
|
3808
|
+
])
|
|
3809
|
+
);
|
|
2624
3810
|
return items;
|
|
2625
3811
|
}
|
|
2626
3812
|
/**
|
|
2627
3813
|
* 绑定编辑器事件,更新按钮状态
|
|
2628
3814
|
*/
|
|
2629
3815
|
bindEditorEvents() {
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
this.
|
|
2634
|
-
this.
|
|
2635
|
-
|
|
3816
|
+
const refresh = () => this.updateButtonStates();
|
|
3817
|
+
this.editor.on("transaction", refresh);
|
|
3818
|
+
this.editor.on("selectionUpdate", refresh);
|
|
3819
|
+
this.unsubscribeEditor = () => {
|
|
3820
|
+
this.editor.off("transaction", refresh);
|
|
3821
|
+
this.editor.off("selectionUpdate", refresh);
|
|
3822
|
+
};
|
|
2636
3823
|
this.updateButtonStates();
|
|
2637
3824
|
}
|
|
2638
3825
|
/**
|
|
@@ -2651,20 +3838,57 @@ class ToolbarManager {
|
|
|
2651
3838
|
button.classList.toggle("active", active);
|
|
2652
3839
|
if (itemDef.popup) button.setAttribute("aria-expanded", String(active));
|
|
2653
3840
|
button.disabled = disabled;
|
|
3841
|
+
if (itemDef.popup === "menu") this.refreshMenuTrigger(itemDef, button);
|
|
2654
3842
|
});
|
|
2655
3843
|
});
|
|
2656
3844
|
}
|
|
3845
|
+
/**
|
|
3846
|
+
* 一项当前该显示的文字。下拉触发器跟着命中的子项走(现在是三级标题就写「标题 3」),
|
|
3847
|
+
* 宿主显式写的 tooltip 优先级最高。
|
|
3848
|
+
* 抽出来是因为图标与提示必须由同一份判定得出:只换其中一个就会出现
|
|
3849
|
+
* "按钮画着 H3、悬停写标题"这种一半新一半旧的界面
|
|
3850
|
+
*/
|
|
3851
|
+
itemTitle(itemId, def) {
|
|
3852
|
+
var _a;
|
|
3853
|
+
const custom = this.customTooltips.get(itemId);
|
|
3854
|
+
if (custom) return custom;
|
|
3855
|
+
let key = TOOLTIP_KEYS[itemId];
|
|
3856
|
+
if (def.popup === "menu") {
|
|
3857
|
+
const activeKey = TOOLTIP_KEYS[((_a = this.activeChild(def)) == null ? void 0 : _a.id) ?? ""];
|
|
3858
|
+
if (activeKey) key = activeKey;
|
|
3859
|
+
}
|
|
3860
|
+
const fallback = key ? zhText(key) : def.tooltip ?? "";
|
|
3861
|
+
return key ? this.tOr(key, fallback) : fallback;
|
|
3862
|
+
}
|
|
3863
|
+
/**
|
|
3864
|
+
* 下拉触发器的图标与提示跟着当前值走:现在是二级标题就显示 H2,
|
|
3865
|
+
* 一个都不匹配才退回通用图标。只有菜单项会走到这里,不是每次刷新对所有按钮都做一遍
|
|
3866
|
+
*/
|
|
3867
|
+
refreshMenuTrigger(def, button) {
|
|
3868
|
+
var _a;
|
|
3869
|
+
const base = button.dataset.menuIcon ?? def.icon;
|
|
3870
|
+
const next = `${((_a = this.activeChild(def)) == null ? void 0 : _a.icon) ?? base}${icons.chevronDown}`;
|
|
3871
|
+
if (button.innerHTML !== next) button.innerHTML = next;
|
|
3872
|
+
const title = this.itemTitle(def.id, def);
|
|
3873
|
+
if (button.title !== title) button.title = title;
|
|
3874
|
+
}
|
|
2657
3875
|
/**
|
|
2658
3876
|
* 销毁工具栏
|
|
2659
3877
|
*/
|
|
2660
3878
|
destroy() {
|
|
2661
|
-
var _a, _b, _c;
|
|
3879
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2662
3880
|
(_a = this.unsubscribeLanguage) == null ? void 0 : _a.call(this);
|
|
2663
3881
|
this.unsubscribeLanguage = void 0;
|
|
2664
|
-
(_b = this.
|
|
3882
|
+
(_b = this.unsubscribeEditor) == null ? void 0 : _b.call(this);
|
|
3883
|
+
this.unsubscribeEditor = void 0;
|
|
3884
|
+
(_c = this.panel) == null ? void 0 : _c.destroy();
|
|
2665
3885
|
this.panel = null;
|
|
2666
|
-
(
|
|
3886
|
+
(_d = this.tablePanel) == null ? void 0 : _d.destroy();
|
|
2667
3887
|
this.tablePanel = null;
|
|
3888
|
+
(_e = this.stylePanel) == null ? void 0 : _e.destroy();
|
|
3889
|
+
this.stylePanel = null;
|
|
3890
|
+
(_f = this.menuPanel) == null ? void 0 : _f.destroy();
|
|
3891
|
+
this.menuPanel = null;
|
|
2668
3892
|
this.buttons.clear();
|
|
2669
3893
|
this.customTooltips.clear();
|
|
2670
3894
|
this.createdElements.forEach((el) => el.remove());
|
|
@@ -2683,6 +3907,7 @@ class LinkPanel {
|
|
|
2683
3907
|
this.element = null;
|
|
2684
3908
|
this.floating = null;
|
|
2685
3909
|
this.detachDismiss = null;
|
|
3910
|
+
this.detachTheme = null;
|
|
2686
3911
|
this.labelled = [];
|
|
2687
3912
|
this.urlInput = null;
|
|
2688
3913
|
this.targetInput = null;
|
|
@@ -2728,19 +3953,21 @@ class LinkPanel {
|
|
|
2728
3953
|
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
2729
3954
|
}
|
|
2730
3955
|
close() {
|
|
2731
|
-
var _a, _b, _c, _d;
|
|
3956
|
+
var _a, _b, _c, _d, _e;
|
|
2732
3957
|
const wasOpen = this.element !== null;
|
|
2733
3958
|
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
2734
3959
|
this.detachDismiss = null;
|
|
2735
|
-
(_b = this.
|
|
3960
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
3961
|
+
this.detachTheme = null;
|
|
3962
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
2736
3963
|
this.floating = null;
|
|
2737
|
-
(
|
|
3964
|
+
(_d = this.element) == null ? void 0 : _d.remove();
|
|
2738
3965
|
this.element = null;
|
|
2739
3966
|
this.urlInput = null;
|
|
2740
3967
|
this.targetInput = null;
|
|
2741
3968
|
this.labelled = [];
|
|
2742
3969
|
this.editing = false;
|
|
2743
|
-
if (wasOpen) (
|
|
3970
|
+
if (wasOpen) (_e = this.onOpenChange) == null ? void 0 : _e.call(this, false);
|
|
2744
3971
|
}
|
|
2745
3972
|
destroy() {
|
|
2746
3973
|
var _a;
|
|
@@ -2812,7 +4039,7 @@ class LinkPanel {
|
|
|
2812
4039
|
panel.appendChild(actions);
|
|
2813
4040
|
document.body.appendChild(panel);
|
|
2814
4041
|
this.element = panel;
|
|
2815
|
-
|
|
4042
|
+
this.detachTheme = followThemeClasses(this.root, panel);
|
|
2816
4043
|
url.focus();
|
|
2817
4044
|
url.select();
|
|
2818
4045
|
}
|
|
@@ -2859,7 +4086,6 @@ function buildItems() {
|
|
|
2859
4086
|
id: "addRowBefore",
|
|
2860
4087
|
icon: icons.tableRowBefore,
|
|
2861
4088
|
labelKey: "table.rowBefore",
|
|
2862
|
-
label: "上方插入行",
|
|
2863
4089
|
group: 0,
|
|
2864
4090
|
command: (editor) => editor.chain().focus().addRowBefore().run()
|
|
2865
4091
|
},
|
|
@@ -2867,7 +4093,6 @@ function buildItems() {
|
|
|
2867
4093
|
id: "addRowAfter",
|
|
2868
4094
|
icon: icons.tableRowAfter,
|
|
2869
4095
|
labelKey: "table.rowAfter",
|
|
2870
|
-
label: "下方插入行",
|
|
2871
4096
|
group: 0,
|
|
2872
4097
|
command: (editor) => editor.chain().focus().addRowAfter().run()
|
|
2873
4098
|
},
|
|
@@ -2875,7 +4100,6 @@ function buildItems() {
|
|
|
2875
4100
|
id: "deleteRow",
|
|
2876
4101
|
icon: icons.tableRowDelete,
|
|
2877
4102
|
labelKey: "table.deleteRow",
|
|
2878
|
-
label: "删除行",
|
|
2879
4103
|
group: 0,
|
|
2880
4104
|
command: (editor) => editor.chain().focus().deleteRow().run()
|
|
2881
4105
|
},
|
|
@@ -2883,7 +4107,6 @@ function buildItems() {
|
|
|
2883
4107
|
id: "addColumnBefore",
|
|
2884
4108
|
icon: icons.tableColumnBefore,
|
|
2885
4109
|
labelKey: "table.columnBefore",
|
|
2886
|
-
label: "左侧插入列",
|
|
2887
4110
|
group: 1,
|
|
2888
4111
|
command: (editor) => editor.chain().focus().addColumnBefore().run()
|
|
2889
4112
|
},
|
|
@@ -2891,7 +4114,6 @@ function buildItems() {
|
|
|
2891
4114
|
id: "addColumnAfter",
|
|
2892
4115
|
icon: icons.tableColumnAfter,
|
|
2893
4116
|
labelKey: "table.columnAfter",
|
|
2894
|
-
label: "右侧插入列",
|
|
2895
4117
|
group: 1,
|
|
2896
4118
|
command: (editor) => editor.chain().focus().addColumnAfter().run()
|
|
2897
4119
|
},
|
|
@@ -2899,7 +4121,6 @@ function buildItems() {
|
|
|
2899
4121
|
id: "deleteColumn",
|
|
2900
4122
|
icon: icons.tableColumnDelete,
|
|
2901
4123
|
labelKey: "table.deleteColumn",
|
|
2902
|
-
label: "删除列",
|
|
2903
4124
|
group: 1,
|
|
2904
4125
|
command: (editor) => editor.chain().focus().deleteColumn().run()
|
|
2905
4126
|
},
|
|
@@ -2908,7 +4129,6 @@ function buildItems() {
|
|
|
2908
4129
|
id: "mergeCells",
|
|
2909
4130
|
icon: icons.tableMergeCells,
|
|
2910
4131
|
labelKey: "table.mergeCells",
|
|
2911
|
-
label: "合并单元格",
|
|
2912
4132
|
group: 2,
|
|
2913
4133
|
command: (editor) => editor.chain().focus().mergeCells().run(),
|
|
2914
4134
|
isDisabled: (editor) => !editor.can().mergeCells()
|
|
@@ -2917,7 +4137,6 @@ function buildItems() {
|
|
|
2917
4137
|
id: "splitCell",
|
|
2918
4138
|
icon: icons.tableSplitCell,
|
|
2919
4139
|
labelKey: "table.splitCell",
|
|
2920
|
-
label: "拆分单元格",
|
|
2921
4140
|
group: 2,
|
|
2922
4141
|
command: (editor) => editor.chain().focus().splitCell().run(),
|
|
2923
4142
|
isDisabled: (editor) => !editor.can().splitCell()
|
|
@@ -2926,24 +4145,22 @@ function buildItems() {
|
|
|
2926
4145
|
id: "toggleHeaderRow",
|
|
2927
4146
|
icon: icons.tableHeaderRow,
|
|
2928
4147
|
labelKey: "table.toggleHeaderRow",
|
|
2929
|
-
label: "切换表头行",
|
|
2930
4148
|
group: 2,
|
|
2931
4149
|
command: (editor) => editor.chain().focus().toggleHeaderRow().run(),
|
|
2932
4150
|
isActive: (editor) => editor.isActive("tableHeader")
|
|
2933
4151
|
}
|
|
2934
4152
|
];
|
|
2935
4153
|
const alignments = [
|
|
2936
|
-
["left", "table.alignLeft"
|
|
2937
|
-
["center", "table.alignCenter"
|
|
2938
|
-
["right", "table.alignRight"
|
|
4154
|
+
["left", "table.alignLeft"],
|
|
4155
|
+
["center", "table.alignCenter"],
|
|
4156
|
+
["right", "table.alignRight"]
|
|
2939
4157
|
];
|
|
2940
|
-
alignments.forEach(([align, labelKey
|
|
4158
|
+
alignments.forEach(([align, labelKey]) => {
|
|
2941
4159
|
const suffix = `${align[0].toUpperCase()}${align.slice(1)}`;
|
|
2942
4160
|
items.push({
|
|
2943
4161
|
id: `cellAlign${suffix}`,
|
|
2944
4162
|
icon: icons[`align${suffix}`],
|
|
2945
4163
|
labelKey,
|
|
2946
|
-
label,
|
|
2947
4164
|
group: 3,
|
|
2948
4165
|
command: (editor) => editor.chain().focus().setCellAttribute("align", align).run(),
|
|
2949
4166
|
isActive: cellAlignmentActive(align)
|
|
@@ -2953,7 +4170,6 @@ function buildItems() {
|
|
|
2953
4170
|
id: "deleteTable",
|
|
2954
4171
|
icon: icons.trash,
|
|
2955
4172
|
labelKey: "table.deleteTable",
|
|
2956
|
-
label: "删除表格",
|
|
2957
4173
|
group: 4,
|
|
2958
4174
|
command: (editor) => editor.chain().focus().deleteTable().run()
|
|
2959
4175
|
});
|
|
@@ -3013,8 +4229,8 @@ class TableMenu {
|
|
|
3013
4229
|
this.unsubscribeLanguage = void 0;
|
|
3014
4230
|
this.hide();
|
|
3015
4231
|
}
|
|
3016
|
-
t(key
|
|
3017
|
-
return tOrFallback(this.i18n, key,
|
|
4232
|
+
t(key) {
|
|
4233
|
+
return tOrFallback(this.i18n, key, zhText(key));
|
|
3018
4234
|
}
|
|
3019
4235
|
show() {
|
|
3020
4236
|
if (!this.element) this.build();
|
|
@@ -3022,9 +4238,11 @@ class TableMenu {
|
|
|
3022
4238
|
void this.position();
|
|
3023
4239
|
}
|
|
3024
4240
|
hide() {
|
|
3025
|
-
var _a;
|
|
4241
|
+
var _a, _b;
|
|
3026
4242
|
(_a = this.detachEscape) == null ? void 0 : _a.call(this);
|
|
3027
4243
|
this.detachEscape = void 0;
|
|
4244
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
4245
|
+
this.detachTheme = void 0;
|
|
3028
4246
|
if (!this.element) return;
|
|
3029
4247
|
this.element.remove();
|
|
3030
4248
|
this.element = null;
|
|
@@ -3047,7 +4265,7 @@ class TableMenu {
|
|
|
3047
4265
|
const panel = document.createElement("div");
|
|
3048
4266
|
panel.className = "atri-table-menu";
|
|
3049
4267
|
panel.setAttribute("role", "toolbar");
|
|
3050
|
-
panel.setAttribute("aria-label", this.t("table.menuLabel"
|
|
4268
|
+
panel.setAttribute("aria-label", this.t("table.menuLabel"));
|
|
3051
4269
|
panel.addEventListener("mousedown", (event) => event.preventDefault());
|
|
3052
4270
|
let lastGroup = null;
|
|
3053
4271
|
this.items.forEach((item) => {
|
|
@@ -3061,7 +4279,7 @@ class TableMenu {
|
|
|
3061
4279
|
button.type = "button";
|
|
3062
4280
|
button.className = "atri-editor-toolbar-btn";
|
|
3063
4281
|
button.setAttribute("data-table-item", item.id);
|
|
3064
|
-
button.title = this.t(item.labelKey
|
|
4282
|
+
button.title = this.t(item.labelKey);
|
|
3065
4283
|
button.innerHTML = item.icon;
|
|
3066
4284
|
button.addEventListener("click", (event) => {
|
|
3067
4285
|
event.preventDefault();
|
|
@@ -3072,16 +4290,16 @@ class TableMenu {
|
|
|
3072
4290
|
});
|
|
3073
4291
|
document.body.appendChild(panel);
|
|
3074
4292
|
this.element = panel;
|
|
3075
|
-
|
|
4293
|
+
this.detachTheme = followThemeClasses(this.root, panel);
|
|
3076
4294
|
this.detachEscape = this.listenEscape();
|
|
3077
4295
|
}
|
|
3078
4296
|
applyLabels() {
|
|
3079
4297
|
const panel = this.element;
|
|
3080
4298
|
if (!panel) return;
|
|
3081
|
-
panel.setAttribute("aria-label", this.t("table.menuLabel"
|
|
4299
|
+
panel.setAttribute("aria-label", this.t("table.menuLabel"));
|
|
3082
4300
|
this.items.forEach((item) => {
|
|
3083
4301
|
const button = panel.querySelector(`[data-table-item="${item.id}"]`);
|
|
3084
|
-
if (button) button.title = this.t(item.labelKey
|
|
4302
|
+
if (button) button.title = this.t(item.labelKey);
|
|
3085
4303
|
});
|
|
3086
4304
|
}
|
|
3087
4305
|
updateStates() {
|
|
@@ -3133,15 +4351,13 @@ class TableMenu {
|
|
|
3133
4351
|
({ x, y } = await computePosition(reference, panel, {
|
|
3134
4352
|
// 贴在单元格上沿:菜单跟着当前格走,长表格里也不会跑到视口外
|
|
3135
4353
|
placement: "top-start",
|
|
3136
|
-
middleware:
|
|
4354
|
+
middleware: panelMiddleware()
|
|
3137
4355
|
}));
|
|
3138
4356
|
} catch {
|
|
3139
4357
|
return;
|
|
3140
4358
|
}
|
|
3141
4359
|
if (this.element !== panel) return;
|
|
3142
|
-
panel
|
|
3143
|
-
panel.style.left = `${x}px`;
|
|
3144
|
-
panel.style.top = `${y}px`;
|
|
4360
|
+
placePanel(panel, x, y);
|
|
3145
4361
|
}
|
|
3146
4362
|
}
|
|
3147
4363
|
class MediaStatusStrip {
|
|
@@ -3163,7 +4379,11 @@ class MediaStatusStrip {
|
|
|
3163
4379
|
this.retry.type = "button";
|
|
3164
4380
|
this.retry.className = "atri-media-status-retry";
|
|
3165
4381
|
this.retry.hidden = true;
|
|
3166
|
-
this.retry.addEventListener("click", () =>
|
|
4382
|
+
this.retry.addEventListener("click", () => {
|
|
4383
|
+
runtime.retryFailed().catch((error) => {
|
|
4384
|
+
console.error("[Atri Editor] Retrying the failed uploads did not work:", error);
|
|
4385
|
+
});
|
|
4386
|
+
});
|
|
3167
4387
|
this.element.append(this.text, this.retry);
|
|
3168
4388
|
container.appendChild(this.element);
|
|
3169
4389
|
this.unsubscribe = runtime.subscribe((state) => this.render(state));
|
|
@@ -3195,6 +4415,15 @@ class MediaStatusStrip {
|
|
|
3195
4415
|
this.retry.textContent = this.labels.retry();
|
|
3196
4416
|
}
|
|
3197
4417
|
}
|
|
4418
|
+
function requireResponse(response, stage) {
|
|
4419
|
+
if (!response || typeof response !== "object") {
|
|
4420
|
+
throw new TypeError(`AI ${stage} must resolve to an AIResponse object.`);
|
|
4421
|
+
}
|
|
4422
|
+
if (typeof response.content !== "string") {
|
|
4423
|
+
throw new TypeError(`AI ${stage} resolved with a non-string content.`);
|
|
4424
|
+
}
|
|
4425
|
+
return response;
|
|
4426
|
+
}
|
|
3198
4427
|
class AIInterceptor {
|
|
3199
4428
|
constructor(interceptors = {}, markdownService = null, autoTranslateMarkdown = true) {
|
|
3200
4429
|
this.interceptors = interceptors;
|
|
@@ -3265,8 +4494,11 @@ class AIInterceptor {
|
|
|
3265
4494
|
if (processedContext === false) {
|
|
3266
4495
|
return;
|
|
3267
4496
|
}
|
|
3268
|
-
let response =
|
|
3269
|
-
|
|
4497
|
+
let response = requireResponse(
|
|
4498
|
+
await requestEndpoint(processedContext),
|
|
4499
|
+
`requestEndpoint("${processedContext.functionId}")`
|
|
4500
|
+
);
|
|
4501
|
+
response = requireResponse(await this.runAfterPost(response, processedContext), "afterPost");
|
|
3270
4502
|
let content = await this.runBeforeRender(response.content, processedContext);
|
|
3271
4503
|
content = await this.runAutoTranslateMarkdown(content, response);
|
|
3272
4504
|
insertCallback(content);
|
|
@@ -3353,22 +4585,42 @@ class AIService {
|
|
|
3353
4585
|
* 执行 AI 功能
|
|
3354
4586
|
*/
|
|
3355
4587
|
async execute(functionId) {
|
|
3356
|
-
var _a, _b;
|
|
3357
4588
|
const func = this.getFunction(functionId);
|
|
3358
4589
|
if (!func) {
|
|
3359
4590
|
console.error(`AI function "${functionId}" not found.`);
|
|
3360
4591
|
return;
|
|
3361
4592
|
}
|
|
3362
4593
|
const context = this.buildContext(func);
|
|
4594
|
+
const timeout = this.config.timeout ?? 0;
|
|
4595
|
+
let timedOut = false;
|
|
4596
|
+
let timer;
|
|
4597
|
+
const chain = this.interceptor.executeChain(context, this.config.requestEndpoint, (content) => {
|
|
4598
|
+
if (timedOut) return;
|
|
4599
|
+
this.insertContent(content, func.outputMode || "insert");
|
|
4600
|
+
});
|
|
3363
4601
|
try {
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
4602
|
+
if (timeout > 0) {
|
|
4603
|
+
await Promise.race([
|
|
4604
|
+
chain,
|
|
4605
|
+
new Promise((_resolve, reject) => {
|
|
4606
|
+
timer = setTimeout(() => {
|
|
4607
|
+
timedOut = true;
|
|
4608
|
+
reject(new Error(`AI function "${functionId}" timed out after ${timeout}ms.`));
|
|
4609
|
+
}, timeout);
|
|
4610
|
+
})
|
|
4611
|
+
]);
|
|
4612
|
+
} else {
|
|
4613
|
+
await chain;
|
|
4614
|
+
}
|
|
3369
4615
|
} catch (error) {
|
|
3370
4616
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
3371
|
-
|
|
4617
|
+
if (this.config.onError) {
|
|
4618
|
+
runSafely(this.config.onError, "ai.onError", [err, context]);
|
|
4619
|
+
} else {
|
|
4620
|
+
console.error(`[Atri Editor] AI function "${functionId}" failed:`, err);
|
|
4621
|
+
}
|
|
4622
|
+
} finally {
|
|
4623
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
3372
4624
|
}
|
|
3373
4625
|
}
|
|
3374
4626
|
/**
|
|
@@ -3443,14 +4695,16 @@ class AICommandMenuManager {
|
|
|
3443
4695
|
this.detachKeyListener = () => document.removeEventListener("keydown", handler, true);
|
|
3444
4696
|
}
|
|
3445
4697
|
setupKeyboardListener() {
|
|
3446
|
-
|
|
4698
|
+
const handler = ({ transaction }) => {
|
|
3447
4699
|
if (transaction.docChanged) {
|
|
3448
4700
|
const lastChar = this.getLastTypedChar();
|
|
3449
4701
|
if (lastChar === this.triggerChar && !this.isVisible) {
|
|
3450
4702
|
this.show();
|
|
3451
4703
|
}
|
|
3452
4704
|
}
|
|
3453
|
-
}
|
|
4705
|
+
};
|
|
4706
|
+
this.editor.on("transaction", handler);
|
|
4707
|
+
this.detachTransactionListener = () => this.editor.off("transaction", handler);
|
|
3454
4708
|
}
|
|
3455
4709
|
getLastTypedChar() {
|
|
3456
4710
|
const { doc, selection } = this.editor.state;
|
|
@@ -3461,7 +4715,7 @@ class AICommandMenuManager {
|
|
|
3461
4715
|
* 显示菜单
|
|
3462
4716
|
*/
|
|
3463
4717
|
show() {
|
|
3464
|
-
if (this.isVisible) return;
|
|
4718
|
+
if (this.isVisible || !this.functions.length) return;
|
|
3465
4719
|
this.isVisible = true;
|
|
3466
4720
|
this.selectedIndex = 0;
|
|
3467
4721
|
this.createMenuElement();
|
|
@@ -3525,15 +4779,13 @@ class AICommandMenuManager {
|
|
|
3525
4779
|
};
|
|
3526
4780
|
({ x, y } = await computePosition(virtualReference, menuElement, {
|
|
3527
4781
|
placement: "bottom-start",
|
|
3528
|
-
middleware:
|
|
4782
|
+
middleware: panelMiddleware()
|
|
3529
4783
|
}));
|
|
3530
4784
|
} catch {
|
|
3531
4785
|
return;
|
|
3532
4786
|
}
|
|
3533
4787
|
if (this.menuElement !== menuElement) return;
|
|
3534
|
-
menuElement
|
|
3535
|
-
menuElement.style.left = `${x}px`;
|
|
3536
|
-
menuElement.style.top = `${y}px`;
|
|
4788
|
+
placePanel(menuElement, x, y);
|
|
3537
4789
|
}
|
|
3538
4790
|
selectItem(index) {
|
|
3539
4791
|
const func = this.functions[index];
|
|
@@ -3598,9 +4850,11 @@ class AICommandMenuManager {
|
|
|
3598
4850
|
* 销毁
|
|
3599
4851
|
*/
|
|
3600
4852
|
destroy() {
|
|
3601
|
-
var _a;
|
|
4853
|
+
var _a, _b;
|
|
3602
4854
|
(_a = this.detachKeyListener) == null ? void 0 : _a.call(this);
|
|
3603
4855
|
this.detachKeyListener = void 0;
|
|
4856
|
+
(_b = this.detachTransactionListener) == null ? void 0 : _b.call(this);
|
|
4857
|
+
this.detachTransactionListener = void 0;
|
|
3604
4858
|
this.hide();
|
|
3605
4859
|
}
|
|
3606
4860
|
}
|
|
@@ -3615,6 +4869,7 @@ class AtriEditor {
|
|
|
3615
4869
|
this.mediaStatus = null;
|
|
3616
4870
|
this.pendingViewState = null;
|
|
3617
4871
|
this.bubbleElement = null;
|
|
4872
|
+
this.destroyed = false;
|
|
3618
4873
|
this.options = options;
|
|
3619
4874
|
const rootElement = resolveElement(options.element);
|
|
3620
4875
|
this.container = createContainer("atri-editor");
|
|
@@ -3647,7 +4902,7 @@ class AtriEditor {
|
|
|
3647
4902
|
return this.bubbleElement;
|
|
3648
4903
|
}
|
|
3649
4904
|
onEditorCreated() {
|
|
3650
|
-
var _a, _b, _c;
|
|
4905
|
+
var _a, _b, _c, _d;
|
|
3651
4906
|
if (this.pendingViewState) {
|
|
3652
4907
|
const { from, to } = this.pendingViewState;
|
|
3653
4908
|
this.pendingViewState = null;
|
|
@@ -3657,7 +4912,7 @@ class AtriEditor {
|
|
|
3657
4912
|
to: Math.min(to, limit)
|
|
3658
4913
|
});
|
|
3659
4914
|
}
|
|
3660
|
-
if ((_a = this.options.ai) == null ? void 0 : _a.functions) {
|
|
4915
|
+
if ((_b = (_a = this.options.ai) == null ? void 0 : _a.functions) == null ? void 0 : _b.length) {
|
|
3661
4916
|
this.aiMenuManager = new AICommandMenuManager(
|
|
3662
4917
|
this.editor,
|
|
3663
4918
|
this.options.ai.functions,
|
|
@@ -3668,7 +4923,7 @@ class AtriEditor {
|
|
|
3668
4923
|
}
|
|
3669
4924
|
);
|
|
3670
4925
|
}
|
|
3671
|
-
(
|
|
4926
|
+
(_d = (_c = this.options).onCreate) == null ? void 0 : _d.call(_c, this);
|
|
3672
4927
|
}
|
|
3673
4928
|
initAI(config) {
|
|
3674
4929
|
this.aiService = new AIService(this.editor, config, this.markdownService);
|
|
@@ -3690,6 +4945,8 @@ class AtriEditor {
|
|
|
3690
4945
|
media: this.options.media,
|
|
3691
4946
|
table: this.options.table,
|
|
3692
4947
|
link: this.options.link,
|
|
4948
|
+
formatting: this.options.formatting,
|
|
4949
|
+
codeBlock: this.options.codeBlock,
|
|
3693
4950
|
mediaRuntime: this.mediaRuntime ?? void 0,
|
|
3694
4951
|
bubbleElement: this.ensureBubbleElement(this.options.toolbar),
|
|
3695
4952
|
onCreate: () => {
|
|
@@ -3806,6 +5063,22 @@ class AtriEditor {
|
|
|
3806
5063
|
getMarkdown() {
|
|
3807
5064
|
return this.markdownService.getMarkdown();
|
|
3808
5065
|
}
|
|
5066
|
+
/**
|
|
5067
|
+
* 这一次 getMarkdown() 会丢掉哪些内容
|
|
5068
|
+
*
|
|
5069
|
+
* 序列化器对没钩子的类型是静默丢弃,所以"导出完看不出少了什么"是常态。接入方在保存前
|
|
5070
|
+
* 拿它决定要不要提示用户,配 `markdown.inlineStyles: 'drop'` 时尤其需要——那个开关
|
|
5071
|
+
* 就是把样式导出成最纯粹的 CommonMark,代价是丢内容。
|
|
5072
|
+
*/
|
|
5073
|
+
getMarkdownLosses() {
|
|
5074
|
+
return this.markdownService.getLosses();
|
|
5075
|
+
}
|
|
5076
|
+
/**
|
|
5077
|
+
* 当前内容里有没有导出 Markdown 时会丢的东西
|
|
5078
|
+
*/
|
|
5079
|
+
hasLossyContent() {
|
|
5080
|
+
return this.markdownService.hasLossyContent();
|
|
5081
|
+
}
|
|
3809
5082
|
/**
|
|
3810
5083
|
* 设置内容
|
|
3811
5084
|
*/
|
|
@@ -4023,6 +5296,7 @@ class AtriEditor {
|
|
|
4023
5296
|
* 连续注册多个组件请改用 registerNodeViews,只需重建一次
|
|
4024
5297
|
*/
|
|
4025
5298
|
registerNodeView(config) {
|
|
5299
|
+
if (this.refuseAfterDestroy("registerNodeView")) return;
|
|
4026
5300
|
this.extensionManager.registerNodeView(config);
|
|
4027
5301
|
if (this.coreEditor) {
|
|
4028
5302
|
this.recreateEditor();
|
|
@@ -4033,11 +5307,21 @@ class AtriEditor {
|
|
|
4033
5307
|
* 注意:如果编辑器已创建,会重新创建编辑器以更新 schema
|
|
4034
5308
|
*/
|
|
4035
5309
|
registerNodeViews(configs) {
|
|
5310
|
+
if (this.refuseAfterDestroy("registerNodeViews")) return;
|
|
4036
5311
|
this.extensionManager.registerNodeViews(configs);
|
|
4037
5312
|
if (this.coreEditor) {
|
|
4038
5313
|
this.recreateEditor();
|
|
4039
5314
|
}
|
|
4040
5315
|
}
|
|
5316
|
+
/**
|
|
5317
|
+
* 销毁后重建类 API 的统一守卫:判在改状态之前,
|
|
5318
|
+
* 否则配置进了 extensionManager 却没有对应的编辑器,看起来就像"注册了但没生效"
|
|
5319
|
+
*/
|
|
5320
|
+
refuseAfterDestroy(api) {
|
|
5321
|
+
if (!this.destroyed) return false;
|
|
5322
|
+
console.warn(`[Atri Editor] ${api}() was called on a destroyed editor and did nothing.`);
|
|
5323
|
+
return true;
|
|
5324
|
+
}
|
|
4041
5325
|
/**
|
|
4042
5326
|
* 获取所有已注册的 NodeView
|
|
4043
5327
|
*/
|
|
@@ -4049,6 +5333,8 @@ class AtriEditor {
|
|
|
4049
5333
|
*/
|
|
4050
5334
|
destroy() {
|
|
4051
5335
|
var _a, _b, _c, _d, _e, _f;
|
|
5336
|
+
if (this.destroyed) return;
|
|
5337
|
+
this.destroyed = true;
|
|
4052
5338
|
(_a = this.toolbarManager) == null ? void 0 : _a.destroy();
|
|
4053
5339
|
(_b = this.tableMenu) == null ? void 0 : _b.destroy();
|
|
4054
5340
|
(_c = this.linkPanel) == null ? void 0 : _c.destroy();
|
|
@@ -4077,15 +5363,41 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
4077
5363
|
super();
|
|
4078
5364
|
this.editor = null;
|
|
4079
5365
|
this._options = {};
|
|
5366
|
+
this.teardownToken = 0;
|
|
5367
|
+
this.detachedContent = null;
|
|
5368
|
+
this.destroyed = false;
|
|
4080
5369
|
}
|
|
4081
5370
|
static get observedAttributes() {
|
|
4082
5371
|
return ["theme", "editable", "lang", "placeholder"];
|
|
4083
5372
|
}
|
|
4084
5373
|
connectedCallback() {
|
|
5374
|
+
this.teardownToken += 1;
|
|
5375
|
+
if (this.destroyed) return;
|
|
4085
5376
|
this.initEditor();
|
|
4086
5377
|
}
|
|
4087
5378
|
disconnectedCallback() {
|
|
4088
|
-
this.
|
|
5379
|
+
const token = this.teardownToken;
|
|
5380
|
+
queueMicrotask(() => {
|
|
5381
|
+
if (token === this.teardownToken && !this.isConnected) this.teardownEditor();
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
/**
|
|
5385
|
+
* 显式销毁:之后重挂不再自动重建,内容快照也一并丢掉
|
|
5386
|
+
*
|
|
5387
|
+
* 框架里对应 unmount 钩子(Vue 的 unmounted、React effect 的 cleanup)。
|
|
5388
|
+
* 断连本身已经会延迟拆除,这个方法的意义在于"别接回去了"
|
|
5389
|
+
*/
|
|
5390
|
+
destroy() {
|
|
5391
|
+
this.destroyed = true;
|
|
5392
|
+
this.teardownToken += 1;
|
|
5393
|
+
this.detachedContent = null;
|
|
5394
|
+
this.teardownEditor();
|
|
5395
|
+
}
|
|
5396
|
+
teardownEditor() {
|
|
5397
|
+
if (!this.editor) return;
|
|
5398
|
+
if (!this.destroyed) this.detachedContent = this.editor.getHTML();
|
|
5399
|
+
this.editor.destroy();
|
|
5400
|
+
this.editor = null;
|
|
4089
5401
|
}
|
|
4090
5402
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
4091
5403
|
if (oldValue === newValue) return;
|
|
@@ -4117,6 +5429,7 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
4117
5429
|
*/
|
|
4118
5430
|
setOptions(options) {
|
|
4119
5431
|
this._options = { ...this._options, ...options };
|
|
5432
|
+
if ("content" in options) this.detachedContent = null;
|
|
4120
5433
|
}
|
|
4121
5434
|
initEditor() {
|
|
4122
5435
|
if (this.editor) return;
|
|
@@ -4136,6 +5449,11 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
4136
5449
|
if (content) options.content = content;
|
|
4137
5450
|
const contentFormat = this.getAttribute("data-content-format");
|
|
4138
5451
|
if (contentFormat) options.contentFormat = contentFormat;
|
|
5452
|
+
if (this.detachedContent !== null) {
|
|
5453
|
+
options.content = this.detachedContent;
|
|
5454
|
+
options.contentFormat = "html";
|
|
5455
|
+
this.detachedContent = null;
|
|
5456
|
+
}
|
|
4139
5457
|
this.editor = new AtriEditor(options);
|
|
4140
5458
|
this.dispatchEvent(
|
|
4141
5459
|
new CustomEvent("editor-created", {
|
|
@@ -4144,12 +5462,6 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
4144
5462
|
})
|
|
4145
5463
|
);
|
|
4146
5464
|
}
|
|
4147
|
-
destroyEditor() {
|
|
4148
|
-
if (this.editor) {
|
|
4149
|
-
this.editor.destroy();
|
|
4150
|
-
this.editor = null;
|
|
4151
|
-
}
|
|
4152
|
-
}
|
|
4153
5465
|
/**
|
|
4154
5466
|
* 获取编辑器实例
|
|
4155
5467
|
*/
|
|
@@ -4177,6 +5489,23 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
4177
5489
|
var _a;
|
|
4178
5490
|
return ((_a = this.editor) == null ? void 0 : _a.getMarkdown()) || "";
|
|
4179
5491
|
}
|
|
5492
|
+
/**
|
|
5493
|
+
* 这一次 getMarkdown() 会丢掉哪些内容
|
|
5494
|
+
*
|
|
5495
|
+
* 元素还没初始化时是 null 而不是空数组——"没什么可丢"和"还没建起来"是两件事,
|
|
5496
|
+
* 混在一起会让接入方把没就绪当成安全。
|
|
5497
|
+
*/
|
|
5498
|
+
getMarkdownLosses() {
|
|
5499
|
+
var _a;
|
|
5500
|
+
return ((_a = this.editor) == null ? void 0 : _a.getMarkdownLosses()) ?? null;
|
|
5501
|
+
}
|
|
5502
|
+
/**
|
|
5503
|
+
* 当前内容里有没有导出 Markdown 时会丢的东西
|
|
5504
|
+
*/
|
|
5505
|
+
hasLossyContent() {
|
|
5506
|
+
var _a;
|
|
5507
|
+
return ((_a = this.editor) == null ? void 0 : _a.hasLossyContent()) ?? false;
|
|
5508
|
+
}
|
|
4180
5509
|
/**
|
|
4181
5510
|
* 设置内容
|
|
4182
5511
|
*/
|
|
@@ -4258,6 +5587,7 @@ export {
|
|
|
4258
5587
|
ThemeManager,
|
|
4259
5588
|
ToolbarManager,
|
|
4260
5589
|
createContainer,
|
|
5590
|
+
createFormattingExtensions,
|
|
4261
5591
|
createMediaExtensions,
|
|
4262
5592
|
getCursorContext,
|
|
4263
5593
|
getDocumentText,
|