@atri-editor/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +175 -0
- package/README.md +1 -1
- package/dist/AtriEditor.d.ts +11 -1
- package/dist/AtriEditor.d.ts.map +1 -1
- package/dist/AtriEditorElement.d.ts +14 -1
- 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 +14 -3
- 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 +74 -0
- package/dist/core/LinkPanel.d.ts.map +1 -0
- 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 +68 -0
- package/dist/core/TableMenu.d.ts.map +1 -0
- package/dist/core/ThemeManager.d.ts.map +1 -1
- package/dist/core/ToolbarManager.d.ts +58 -3
- 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/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/extensions/table.d.ts +23 -0
- package/dist/extensions/table.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2025 -188
- 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 +311 -4
- package/dist/types/ai.d.ts +5 -0
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/editor.d.ts +49 -3
- package/dist/types/editor.d.ts.map +1 -1
- package/dist/types/markdown.d.ts +14 -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/table.d.ts +11 -4
- package/dist/utils/table.d.ts.map +1 -1
- package/dist/utils/url.d.ts +20 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/package.json +7 -1
package/dist/index.js
CHANGED
|
@@ -4,12 +4,44 @@ import { Markdown } from "@tiptap/markdown";
|
|
|
4
4
|
import { Placeholder } from "@tiptap/extensions";
|
|
5
5
|
import TextAlign from "@tiptap/extension-text-align";
|
|
6
6
|
import BubbleMenu from "@tiptap/extension-bubble-menu";
|
|
7
|
-
import {
|
|
7
|
+
import { TaskList, TaskItem } from "@tiptap/extension-list";
|
|
8
|
+
import { TableRow, TableHeader, TableCell, Table } from "@tiptap/extension-table";
|
|
8
9
|
import Image from "@tiptap/extension-image";
|
|
9
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";
|
|
10
15
|
import { Node as Node$1, DOMParser } from "@tiptap/pm/model";
|
|
11
16
|
import i18next from "i18next";
|
|
12
|
-
import {
|
|
17
|
+
import { offset, flip, shift, autoUpdate, computePosition } from "@floating-ui/dom";
|
|
18
|
+
function isBuiltinTableActive(editor) {
|
|
19
|
+
var _a;
|
|
20
|
+
const spec = (_a = editor.schema.nodes.table) == null ? void 0 : _a.spec;
|
|
21
|
+
return (spec == null ? void 0 : spec.tableRole) === "table";
|
|
22
|
+
}
|
|
23
|
+
function canInsertTable(editor) {
|
|
24
|
+
if (typeof editor.commands.insertTable !== "function") return false;
|
|
25
|
+
return isBuiltinTableActive(editor);
|
|
26
|
+
}
|
|
27
|
+
const DEFAULT_TABLE_CONFIG = {
|
|
28
|
+
resizable: true,
|
|
29
|
+
menu: true
|
|
30
|
+
};
|
|
31
|
+
function resolveTableConfig(table) {
|
|
32
|
+
return { ...DEFAULT_TABLE_CONFIG, ...typeof table === "object" ? table : void 0 };
|
|
33
|
+
}
|
|
34
|
+
const AtriTable = Table.extend({
|
|
35
|
+
addProseMirrorPlugins() {
|
|
36
|
+
var _a;
|
|
37
|
+
if (!isBuiltinTableActive(this.editor)) return [];
|
|
38
|
+
return ((_a = this.parent) == null ? void 0 : _a.call(this)) ?? [];
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
function createTableExtensions(table) {
|
|
42
|
+
const config = resolveTableConfig(table);
|
|
43
|
+
return [AtriTable.configure({ resizable: config.resizable }), TableRow, TableHeader, TableCell];
|
|
44
|
+
}
|
|
13
45
|
function mediaInsertTarget(selection) {
|
|
14
46
|
if (selection instanceof NodeSelection && (selection.node.type.name === "image" || selection.node.type.name === "attachment" || selection.node.type.name === "attachmentLink")) {
|
|
15
47
|
return selection.to;
|
|
@@ -98,6 +130,32 @@ function validateFile(file, options) {
|
|
|
98
130
|
function isImageFile(file) {
|
|
99
131
|
return (file.type || "").toLowerCase().startsWith("image/");
|
|
100
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
|
+
}
|
|
101
159
|
function isAttachmentForm(name) {
|
|
102
160
|
return name === "attachment" || name === "attachmentLink";
|
|
103
161
|
}
|
|
@@ -122,6 +180,12 @@ function insertAttachmentContent(props, typeName, options) {
|
|
|
122
180
|
const { state, commands, tr, dispatch } = props;
|
|
123
181
|
const target = mediaInsertTarget(state.selection);
|
|
124
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
|
+
}
|
|
125
189
|
if (!commands.insertContentAt(target, {
|
|
126
190
|
type: typeName,
|
|
127
191
|
attrs: {
|
|
@@ -154,7 +218,10 @@ function attachmentAttributes() {
|
|
|
154
218
|
return {
|
|
155
219
|
src: {
|
|
156
220
|
default: null,
|
|
157
|
-
|
|
221
|
+
// 粘贴进来的 data-src 也洗一遍:不洗的话它绕开 AttachmentLink 那处 getAttrs,
|
|
222
|
+
// 直接落进节点属性,再由渲染层的 safeHref 挡掉——属性里仍留着地址,
|
|
223
|
+
// 而 setNodeMarkup / JSON 回灌都不经过渲染层
|
|
224
|
+
parseHTML: (element) => safeMediaSrc(element.getAttribute("data-src")),
|
|
158
225
|
renderHTML: (attributes) => dataAttr("data-src", attributes.src)
|
|
159
226
|
},
|
|
160
227
|
name: {
|
|
@@ -304,7 +371,19 @@ const Attachment = Node.create({
|
|
|
304
371
|
["span", { class: "atri-attachment-icon", "aria-hidden": "true" }]
|
|
305
372
|
];
|
|
306
373
|
const body = [
|
|
307
|
-
|
|
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
|
+
]
|
|
308
387
|
];
|
|
309
388
|
if (size) {
|
|
310
389
|
body.push(["span", { class: "atri-attachment-size" }, formatFileSize(size)]);
|
|
@@ -416,8 +495,9 @@ const AttachmentLink = Node.create({
|
|
|
416
495
|
{
|
|
417
496
|
tag: "a[data-atri-attachment-link]",
|
|
418
497
|
priority: 100,
|
|
419
|
-
// 手写或从别处粘来的 <a> 只带 href,没有 data-src
|
|
420
|
-
|
|
498
|
+
// 手写或从别处粘来的 <a> 只带 href,没有 data-src:认下来,别留一枚空链接。
|
|
499
|
+
// data-src 那条来路在 attachmentAttributes().src.parseHTML 里已经洗过
|
|
500
|
+
getAttrs: (element) => element.getAttribute("data-src") ? {} : { src: safeHref(element.getAttribute("href")) }
|
|
421
501
|
}
|
|
422
502
|
];
|
|
423
503
|
},
|
|
@@ -428,9 +508,11 @@ const AttachmentLink = Node.create({
|
|
|
428
508
|
mergeAttributes(HTMLAttributes, {
|
|
429
509
|
"data-atri-attachment-link": "",
|
|
430
510
|
class: "atri-attachment-link",
|
|
431
|
-
href
|
|
511
|
+
// 协议不合法就整枚 href 都不写,退化成一枚没有地址的文字,
|
|
512
|
+
// 而不是留着 href="javascript:…" 等用户点
|
|
513
|
+
...dataAttr("href", safeHref(src)),
|
|
432
514
|
// download 让同源地址点击即下载(跨域浏览器会退化成导航);
|
|
433
|
-
//
|
|
515
|
+
// 值为 null 时 PM 序列化会跳过,没名字就只剩普通链接
|
|
434
516
|
download: name,
|
|
435
517
|
rel: "noopener noreferrer"
|
|
436
518
|
}),
|
|
@@ -534,6 +616,204 @@ function createMediaExtensions(media, runtime) {
|
|
|
534
616
|
MediaFiles.configure({ runtime: runtime ?? null })
|
|
535
617
|
];
|
|
536
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
|
+
}
|
|
537
817
|
function pickUrl(body) {
|
|
538
818
|
if (!body || typeof body !== "object") return void 0;
|
|
539
819
|
const record = body;
|
|
@@ -577,10 +857,29 @@ function uploadViaEndpoint(config, file, context) {
|
|
|
577
857
|
body = xhr.responseText;
|
|
578
858
|
}
|
|
579
859
|
if (config.transformResult) {
|
|
580
|
-
|
|
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 });
|
|
581
879
|
return;
|
|
582
880
|
}
|
|
583
|
-
const
|
|
881
|
+
const candidate = typeof body === "string" ? body : pickUrl(body);
|
|
882
|
+
const url = candidate ? usableUrl(candidate) : null;
|
|
584
883
|
if (!url) {
|
|
585
884
|
reject(new Error("上传失败:响应里找不到文件地址,请用 upload.transformResult 指定"));
|
|
586
885
|
return;
|
|
@@ -589,6 +888,8 @@ function uploadViaEndpoint(config, file, context) {
|
|
|
589
888
|
};
|
|
590
889
|
xhr.onerror = () => reject(new Error("上传失败:网络错误"));
|
|
591
890
|
xhr.onabort = () => reject(new DOMException("Aborted", "AbortError"));
|
|
891
|
+
xhr.ontimeout = () => reject(new Error(`上传失败:超过 ${config.timeout}ms 未响应`));
|
|
892
|
+
xhr.timeout = config.timeout ?? 0;
|
|
592
893
|
if (context.signal.aborted) {
|
|
593
894
|
xhr.abort();
|
|
594
895
|
return;
|
|
@@ -607,6 +908,17 @@ function createUploader(upload) {
|
|
|
607
908
|
if (!upload.endpoint) return null;
|
|
608
909
|
return { upload: (file, context) => uploadViaEndpoint(upload, file, context) };
|
|
609
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
|
+
}
|
|
610
922
|
function readFileAsDataUrl(file) {
|
|
611
923
|
return new Promise((resolve, reject) => {
|
|
612
924
|
const reader = new FileReader();
|
|
@@ -947,14 +1259,7 @@ class MediaRuntime {
|
|
|
947
1259
|
findPosition(task) {
|
|
948
1260
|
const editor = this.editor;
|
|
949
1261
|
if (!editor) return null;
|
|
950
|
-
|
|
951
|
-
editor.state.doc.descendants((node, position) => {
|
|
952
|
-
if (found !== null) return;
|
|
953
|
-
if (node.attrs.uploadId === task.id || node.attrs.src === task.previewUrl) {
|
|
954
|
-
found = position;
|
|
955
|
-
}
|
|
956
|
-
});
|
|
957
|
-
return found;
|
|
1262
|
+
return findTaskPosition(editor.state.doc, task);
|
|
958
1263
|
}
|
|
959
1264
|
taskByPreviewUrl(src) {
|
|
960
1265
|
if (typeof src !== "string" || !src) return null;
|
|
@@ -974,7 +1279,7 @@ class MediaRuntime {
|
|
|
974
1279
|
}
|
|
975
1280
|
reject(file, reason, cause) {
|
|
976
1281
|
if (this.config.onError) {
|
|
977
|
-
this.config.onError
|
|
1282
|
+
runSafely(this.config.onError, "media.onError", [{ file, reason, cause }]);
|
|
978
1283
|
return;
|
|
979
1284
|
}
|
|
980
1285
|
console.warn(`[Atri Editor] File "${file.name}" was rejected (${reason}).`);
|
|
@@ -988,6 +1293,17 @@ class MediaRuntime {
|
|
|
988
1293
|
function clampPercent(percent) {
|
|
989
1294
|
return Math.round(Math.min(100, Math.max(0, percent)));
|
|
990
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
|
+
}
|
|
991
1307
|
function createPreviewUrl(file) {
|
|
992
1308
|
if (typeof URL === "undefined" || typeof URL.createObjectURL !== "function") return null;
|
|
993
1309
|
try {
|
|
@@ -1003,7 +1319,16 @@ function revokePreviewUrl(task) {
|
|
|
1003
1319
|
URL.revokeObjectURL(previewUrl);
|
|
1004
1320
|
task.previewUrl = null;
|
|
1005
1321
|
}
|
|
1006
|
-
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
|
+
];
|
|
1007
1332
|
const BUBBLE_NODE_ITEMS = ["attachmentDisplay", "delete"];
|
|
1008
1333
|
const BUBBLE_SELECTABLE_NODES = ["image", "attachment", "attachmentLink"];
|
|
1009
1334
|
function bubbleNodeNameFor(state) {
|
|
@@ -1033,19 +1358,44 @@ function shouldShowBubbleMenu(props) {
|
|
|
1033
1358
|
if (!group) return false;
|
|
1034
1359
|
return !!element.querySelector(`[data-atri-bubble-group="${group}"] [data-toolbar-item]`);
|
|
1035
1360
|
}
|
|
1361
|
+
const LINK_HTML_ATTRIBUTES = {
|
|
1362
|
+
target: "_blank",
|
|
1363
|
+
rel: "noopener noreferrer nofollow",
|
|
1364
|
+
class: null
|
|
1365
|
+
};
|
|
1036
1366
|
class CoreEditor {
|
|
1037
1367
|
constructor(config) {
|
|
1368
|
+
this.ownsMediaRuntime = false;
|
|
1038
1369
|
this.config = config;
|
|
1039
1370
|
this.placeholderText = config.placeholder ?? "";
|
|
1040
1371
|
this.mediaRuntime = config.media === false ? null : config.mediaRuntime ?? new MediaRuntime(config.media);
|
|
1372
|
+
this.ownsMediaRuntime = this.mediaRuntime !== null && !config.mediaRuntime;
|
|
1041
1373
|
this.editor = this.createEditor();
|
|
1042
1374
|
}
|
|
1043
1375
|
createEditor() {
|
|
1044
|
-
var _a, _b, _c, _d, _e;
|
|
1376
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
1045
1377
|
const { element, content, contentFormat, editable, extensions } = this.config;
|
|
1046
1378
|
const editorExtensions = [
|
|
1047
1379
|
StarterKit.configure({
|
|
1048
|
-
// StarterKit v3 默认包含 Underline, Link, TrailingNode
|
|
1380
|
+
// StarterKit v3 默认包含 Underline, Link, TrailingNode。
|
|
1381
|
+
// 链接:openOnClick 默认关——编辑态点击落在链接上,用户要的是改它,
|
|
1382
|
+
// 而 Link 扩展开着导航时会把这次点击吃掉(连光标都放不进去)并打开新标签页,
|
|
1383
|
+
// 未保存的内容就跟着这次点击没了。关掉后点击交给 LinkPanel 就地编辑。
|
|
1384
|
+
link: {
|
|
1385
|
+
openOnClick: ((_a = this.config.link) == null ? void 0 : _a.openOnClick) ?? false,
|
|
1386
|
+
markdownLinks: ((_b = this.config.link) == null ? void 0 : _b.markdownLinks) ?? false,
|
|
1387
|
+
HTMLAttributes: {
|
|
1388
|
+
...LINK_HTML_ATTRIBUTES,
|
|
1389
|
+
target: ((_c = this.config.link) == null ? void 0 : _c.target) ?? LINK_HTML_ATTRIBUTES.target
|
|
1390
|
+
}
|
|
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
|
|
1049
1399
|
})
|
|
1050
1400
|
];
|
|
1051
1401
|
editorExtensions.push(
|
|
@@ -1058,19 +1408,19 @@ class CoreEditor {
|
|
|
1058
1408
|
types: ["heading", "paragraph"]
|
|
1059
1409
|
})
|
|
1060
1410
|
);
|
|
1061
|
-
const markdownEnabled = ((
|
|
1411
|
+
const markdownEnabled = ((_d = this.config.markdown) == null ? void 0 : _d.enabled) !== false;
|
|
1062
1412
|
if (markdownEnabled) {
|
|
1063
1413
|
editorExtensions.push(
|
|
1064
1414
|
Markdown.configure({
|
|
1065
|
-
indentation: (
|
|
1066
|
-
markedOptions: (
|
|
1415
|
+
indentation: (_e = this.config.markdown) == null ? void 0 : _e.indentation,
|
|
1416
|
+
markedOptions: (_f = this.config.markdown) == null ? void 0 : _f.markedOptions
|
|
1067
1417
|
})
|
|
1068
1418
|
);
|
|
1069
1419
|
}
|
|
1070
1420
|
const contentType = contentFormat === "markdown" && !markdownEnabled ? void 0 : contentFormat;
|
|
1071
1421
|
editorExtensions.push(...createMediaExtensions(this.config.media, this.mediaRuntime));
|
|
1072
1422
|
if (this.config.table !== false) {
|
|
1073
|
-
editorExtensions.push(
|
|
1423
|
+
editorExtensions.push(...createTableExtensions(resolveTableConfig(this.config.table)));
|
|
1074
1424
|
}
|
|
1075
1425
|
if (this.config.bubbleElement) {
|
|
1076
1426
|
editorExtensions.push(
|
|
@@ -1089,7 +1439,13 @@ class CoreEditor {
|
|
|
1089
1439
|
})
|
|
1090
1440
|
);
|
|
1091
1441
|
}
|
|
1092
|
-
|
|
1442
|
+
editorExtensions.push(TaskList, TaskItem);
|
|
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;
|
|
1093
1449
|
if (extensions) {
|
|
1094
1450
|
editorExtensions.push(...extensions);
|
|
1095
1451
|
}
|
|
@@ -1106,27 +1462,22 @@ class CoreEditor {
|
|
|
1106
1462
|
}
|
|
1107
1463
|
},
|
|
1108
1464
|
onCreate: () => {
|
|
1109
|
-
|
|
1110
|
-
(_b2 = (_a2 = this.config).onCreate) == null ? void 0 : _b2.call(_a2);
|
|
1465
|
+
runSafely(this.config.onCreate, "onCreate");
|
|
1111
1466
|
},
|
|
1112
1467
|
onUpdate: () => {
|
|
1113
|
-
|
|
1114
|
-
(_b2 = (_a2 = this.config).onUpdate) == null ? void 0 : _b2.call(_a2);
|
|
1468
|
+
runSafely(this.config.onUpdate, "onUpdate");
|
|
1115
1469
|
},
|
|
1116
1470
|
onFocus: () => {
|
|
1117
|
-
|
|
1118
|
-
(_b2 = (_a2 = this.config).onFocus) == null ? void 0 : _b2.call(_a2);
|
|
1471
|
+
runSafely(this.config.onFocus, "onFocus");
|
|
1119
1472
|
},
|
|
1120
1473
|
onBlur: () => {
|
|
1121
|
-
|
|
1122
|
-
(_b2 = (_a2 = this.config).onBlur) == null ? void 0 : _b2.call(_a2);
|
|
1474
|
+
runSafely(this.config.onBlur, "onBlur");
|
|
1123
1475
|
},
|
|
1124
1476
|
onDestroy: () => {
|
|
1125
|
-
|
|
1126
|
-
(_b2 = (_a2 = this.config).onDestroy) == null ? void 0 : _b2.call(_a2);
|
|
1477
|
+
runSafely(this.config.onDestroy, "onDestroy");
|
|
1127
1478
|
}
|
|
1128
1479
|
});
|
|
1129
|
-
(
|
|
1480
|
+
(_i = this.mediaRuntime) == null ? void 0 : _i.bind(editor);
|
|
1130
1481
|
return editor;
|
|
1131
1482
|
}
|
|
1132
1483
|
/**
|
|
@@ -1226,11 +1577,17 @@ class CoreEditor {
|
|
|
1226
1577
|
* 销毁编辑器
|
|
1227
1578
|
*/
|
|
1228
1579
|
destroy() {
|
|
1580
|
+
var _a;
|
|
1229
1581
|
this.editor.destroy();
|
|
1582
|
+
if (this.ownsMediaRuntime) (_a = this.mediaRuntime) == null ? void 0 : _a.destroy();
|
|
1583
|
+
this.mediaRuntime = null;
|
|
1230
1584
|
}
|
|
1231
1585
|
}
|
|
1232
1586
|
function simpleMarkdownToHtml(markdown2) {
|
|
1233
|
-
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>");
|
|
1234
1591
|
html = html.replace(/^### (.+)$/gm, "<h3>$1</h3>");
|
|
1235
1592
|
html = html.replace(/^## (.+)$/gm, "<h2>$1</h2>");
|
|
1236
1593
|
html = html.replace(/^# (.+)$/gm, "<h1>$1</h1>");
|
|
@@ -1241,9 +1598,21 @@ function simpleMarkdownToHtml(markdown2) {
|
|
|
1241
1598
|
html = html.replace(/`(.+?)`/g, "<code>$1</code>");
|
|
1242
1599
|
html = html.replace(
|
|
1243
1600
|
/!\[([^\]]*)\]\(\s*([^\s)]+)(?:\s+"([^"]*)")?\s*\)/g,
|
|
1244
|
-
(_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
|
+
}
|
|
1245
1615
|
);
|
|
1246
|
-
html = html.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
|
|
1247
1616
|
html = html.replace(/^\* (.+)$/gm, "<li>$1</li>");
|
|
1248
1617
|
html = html.replace(/^- (.+)$/gm, "<li>$1</li>");
|
|
1249
1618
|
html = html.replace(/^(\d+)\. (.+)$/gm, "<li>$2</li>");
|
|
@@ -1263,6 +1632,9 @@ function simpleHtmlToMarkdown(html) {
|
|
|
1263
1632
|
md = md.replace(/<h1[^>]*>(.+?)<\/h1>/gi, "# $1\n\n");
|
|
1264
1633
|
md = md.replace(/<h2[^>]*>(.+?)<\/h2>/gi, "## $1\n\n");
|
|
1265
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");
|
|
1266
1638
|
md = md.replace(/<strong[^>]*>(.+?)<\/strong>/gi, "**$1**");
|
|
1267
1639
|
md = md.replace(/<b[^>]*>(.+?)<\/b>/gi, "**$1**");
|
|
1268
1640
|
md = md.replace(/<em[^>]*>(.+?)<\/em>/gi, "*$1*");
|
|
@@ -1381,6 +1753,7 @@ class MarkdownService {
|
|
|
1381
1753
|
return this.config.enabled !== false;
|
|
1382
1754
|
}
|
|
1383
1755
|
}
|
|
1756
|
+
const THEME_PREFIX$1 = "atri-theme-";
|
|
1384
1757
|
class ThemeManager {
|
|
1385
1758
|
constructor(container, theme = "light") {
|
|
1386
1759
|
this.container = container;
|
|
@@ -1391,8 +1764,8 @@ class ThemeManager {
|
|
|
1391
1764
|
* 应用当前主题:内置主题与自定义主题走同一条 class 规则,只是名字不同
|
|
1392
1765
|
*/
|
|
1393
1766
|
applyTheme() {
|
|
1394
|
-
this.container.classList.
|
|
1395
|
-
this.container.classList.add(
|
|
1767
|
+
Array.from(this.container.classList).filter((name) => name.startsWith(THEME_PREFIX$1)).forEach((name) => this.container.classList.remove(name));
|
|
1768
|
+
this.container.classList.add(`${THEME_PREFIX$1}${this.currentTheme}`);
|
|
1396
1769
|
this.container.setAttribute("data-atri-theme", this.currentTheme);
|
|
1397
1770
|
}
|
|
1398
1771
|
/**
|
|
@@ -1427,12 +1800,16 @@ const zhResources = {
|
|
|
1427
1800
|
heading1: "标题 1",
|
|
1428
1801
|
heading2: "标题 2",
|
|
1429
1802
|
heading3: "标题 3",
|
|
1803
|
+
heading4: "标题 4",
|
|
1804
|
+
heading5: "标题 5",
|
|
1805
|
+
heading6: "标题 6",
|
|
1430
1806
|
paragraph: "正文",
|
|
1431
1807
|
bulletList: "无序列表",
|
|
1432
1808
|
orderedList: "有序列表",
|
|
1433
1809
|
taskList: "任务列表",
|
|
1434
1810
|
codeBlock: "代码块",
|
|
1435
1811
|
blockquote: "引用",
|
|
1812
|
+
horizontalRule: "分割线",
|
|
1436
1813
|
link: "链接",
|
|
1437
1814
|
image: "图片",
|
|
1438
1815
|
attachment: "附件",
|
|
@@ -1442,10 +1819,27 @@ const zhResources = {
|
|
|
1442
1819
|
table: "表格",
|
|
1443
1820
|
undo: "撤销",
|
|
1444
1821
|
redo: "重做",
|
|
1822
|
+
align: "对齐",
|
|
1445
1823
|
alignLeft: "左对齐",
|
|
1446
1824
|
alignCenter: "居中",
|
|
1447
1825
|
alignRight: "右对齐",
|
|
1448
|
-
alignJustify: "两端对齐"
|
|
1826
|
+
alignJustify: "两端对齐",
|
|
1827
|
+
textColor: "文字颜色",
|
|
1828
|
+
highlight: "高亮",
|
|
1829
|
+
highlightColor: "高亮色",
|
|
1830
|
+
fontSize: "字号",
|
|
1831
|
+
fontFamily: "字体",
|
|
1832
|
+
subscript: "下标",
|
|
1833
|
+
superscript: "上标",
|
|
1834
|
+
clearFormat: "清除格式"
|
|
1835
|
+
},
|
|
1836
|
+
formatting: {
|
|
1837
|
+
customColor: "其他颜色…",
|
|
1838
|
+
noColor: "无",
|
|
1839
|
+
defaultValue: "默认",
|
|
1840
|
+
fontSans: "无衬线",
|
|
1841
|
+
fontSerif: "衬线",
|
|
1842
|
+
fontMono: "等宽"
|
|
1449
1843
|
},
|
|
1450
1844
|
media: {
|
|
1451
1845
|
uploading: "上传中 {{files}} 个 · {{percent}}%",
|
|
@@ -1458,6 +1852,29 @@ const zhResources = {
|
|
|
1458
1852
|
altPlaceholder: "替代文字",
|
|
1459
1853
|
insert: "插入"
|
|
1460
1854
|
},
|
|
1855
|
+
table: {
|
|
1856
|
+
menuLabel: "表格操作",
|
|
1857
|
+
rowBefore: "上方插入行",
|
|
1858
|
+
rowAfter: "下方插入行",
|
|
1859
|
+
deleteRow: "删除行",
|
|
1860
|
+
columnBefore: "左侧插入列",
|
|
1861
|
+
columnAfter: "右侧插入列",
|
|
1862
|
+
deleteColumn: "删除列",
|
|
1863
|
+
mergeCells: "合并单元格",
|
|
1864
|
+
splitCell: "拆分单元格",
|
|
1865
|
+
toggleHeaderRow: "切换表头行",
|
|
1866
|
+
alignLeft: "单元格左对齐",
|
|
1867
|
+
alignCenter: "单元格居中",
|
|
1868
|
+
alignRight: "单元格右对齐",
|
|
1869
|
+
deleteTable: "删除表格"
|
|
1870
|
+
},
|
|
1871
|
+
link: {
|
|
1872
|
+
panelLabel: "链接",
|
|
1873
|
+
urlPlaceholder: "链接地址(https://…)",
|
|
1874
|
+
newWindow: "在新窗口打开",
|
|
1875
|
+
apply: "确定",
|
|
1876
|
+
remove: "移除链接"
|
|
1877
|
+
},
|
|
1461
1878
|
ai: {
|
|
1462
1879
|
menu: "AI 菜单",
|
|
1463
1880
|
continue: "AI 续写",
|
|
@@ -1480,12 +1897,16 @@ const enResources = {
|
|
|
1480
1897
|
heading1: "Heading 1",
|
|
1481
1898
|
heading2: "Heading 2",
|
|
1482
1899
|
heading3: "Heading 3",
|
|
1900
|
+
heading4: "Heading 4",
|
|
1901
|
+
heading5: "Heading 5",
|
|
1902
|
+
heading6: "Heading 6",
|
|
1483
1903
|
paragraph: "Paragraph",
|
|
1484
1904
|
bulletList: "Bullet List",
|
|
1485
1905
|
orderedList: "Ordered List",
|
|
1486
1906
|
taskList: "Task List",
|
|
1487
1907
|
codeBlock: "Code Block",
|
|
1488
1908
|
blockquote: "Blockquote",
|
|
1909
|
+
horizontalRule: "Divider",
|
|
1489
1910
|
link: "Link",
|
|
1490
1911
|
image: "Image",
|
|
1491
1912
|
attachment: "Attachment",
|
|
@@ -1495,10 +1916,27 @@ const enResources = {
|
|
|
1495
1916
|
table: "Table",
|
|
1496
1917
|
undo: "Undo",
|
|
1497
1918
|
redo: "Redo",
|
|
1919
|
+
align: "Align",
|
|
1498
1920
|
alignLeft: "Align Left",
|
|
1499
1921
|
alignCenter: "Align Center",
|
|
1500
1922
|
alignRight: "Align Right",
|
|
1501
|
-
alignJustify: "Justify"
|
|
1923
|
+
alignJustify: "Justify",
|
|
1924
|
+
textColor: "Text Color",
|
|
1925
|
+
highlight: "Highlight",
|
|
1926
|
+
highlightColor: "Highlight Color",
|
|
1927
|
+
fontSize: "Font Size",
|
|
1928
|
+
fontFamily: "Font",
|
|
1929
|
+
subscript: "Subscript",
|
|
1930
|
+
superscript: "Superscript",
|
|
1931
|
+
clearFormat: "Clear Formatting"
|
|
1932
|
+
},
|
|
1933
|
+
formatting: {
|
|
1934
|
+
customColor: "Custom color…",
|
|
1935
|
+
noColor: "None",
|
|
1936
|
+
defaultValue: "Default",
|
|
1937
|
+
fontSans: "Sans",
|
|
1938
|
+
fontSerif: "Serif",
|
|
1939
|
+
fontMono: "Monospace"
|
|
1502
1940
|
},
|
|
1503
1941
|
media: {
|
|
1504
1942
|
uploading: "Uploading {{files}} · {{percent}}%",
|
|
@@ -1511,6 +1949,29 @@ const enResources = {
|
|
|
1511
1949
|
altPlaceholder: "Alternative text",
|
|
1512
1950
|
insert: "Insert"
|
|
1513
1951
|
},
|
|
1952
|
+
table: {
|
|
1953
|
+
menuLabel: "Table actions",
|
|
1954
|
+
rowBefore: "Insert row above",
|
|
1955
|
+
rowAfter: "Insert row below",
|
|
1956
|
+
deleteRow: "Delete row",
|
|
1957
|
+
columnBefore: "Insert column left",
|
|
1958
|
+
columnAfter: "Insert column right",
|
|
1959
|
+
deleteColumn: "Delete column",
|
|
1960
|
+
mergeCells: "Merge cells",
|
|
1961
|
+
splitCell: "Split cell",
|
|
1962
|
+
toggleHeaderRow: "Toggle header row",
|
|
1963
|
+
alignLeft: "Align cell left",
|
|
1964
|
+
alignCenter: "Center cell",
|
|
1965
|
+
alignRight: "Align cell right",
|
|
1966
|
+
deleteTable: "Delete table"
|
|
1967
|
+
},
|
|
1968
|
+
link: {
|
|
1969
|
+
panelLabel: "Link",
|
|
1970
|
+
urlPlaceholder: "Link URL (https://…)",
|
|
1971
|
+
newWindow: "Open in new window",
|
|
1972
|
+
apply: "Apply",
|
|
1973
|
+
remove: "Remove link"
|
|
1974
|
+
},
|
|
1514
1975
|
ai: {
|
|
1515
1976
|
menu: "AI Menu",
|
|
1516
1977
|
continue: "AI Continue",
|
|
@@ -1521,6 +1982,9 @@ const enResources = {
|
|
|
1521
1982
|
error: "AI Error"
|
|
1522
1983
|
}
|
|
1523
1984
|
};
|
|
1985
|
+
function cloneResources(value) {
|
|
1986
|
+
return JSON.parse(JSON.stringify(value));
|
|
1987
|
+
}
|
|
1524
1988
|
class I18nManager {
|
|
1525
1989
|
constructor(lang = "zh") {
|
|
1526
1990
|
this.instance = i18next.createInstance();
|
|
@@ -1529,8 +1993,8 @@ class I18nManager {
|
|
|
1529
1993
|
fallbackLng: "zh",
|
|
1530
1994
|
initImmediate: false,
|
|
1531
1995
|
resources: {
|
|
1532
|
-
zh: { translation: zhResources },
|
|
1533
|
-
en: { translation: enResources }
|
|
1996
|
+
zh: { translation: cloneResources(zhResources) },
|
|
1997
|
+
en: { translation: cloneResources(enResources) }
|
|
1534
1998
|
}
|
|
1535
1999
|
});
|
|
1536
2000
|
}
|
|
@@ -1553,6 +2017,30 @@ class I18nManager {
|
|
|
1553
2017
|
async changeLanguage(lang) {
|
|
1554
2018
|
await this.instance.changeLanguage(lang);
|
|
1555
2019
|
}
|
|
2020
|
+
/**
|
|
2021
|
+
* 注册语言包:给已存在的语言补词条,或引入一门内置清单之外的新语言。
|
|
2022
|
+
*
|
|
2023
|
+
* 内置词条是源码里的两个字面量对象,接入方改不到,于是 lang: 'ja' 只能静默回退中文
|
|
2024
|
+
* ——看起来像"支持可扩展",实际没有入口。这里补上那个入口。
|
|
2025
|
+
*
|
|
2026
|
+
* overwrite 传 false:同名 key 以先注册的一方为准,防止后加载的包把内置文案悄悄换掉。
|
|
2027
|
+
* 要自定义措辞走 toolbar.items 的 tooltip,那本来就是优先级更高的显式入口。
|
|
2028
|
+
* 注册当前语言时补发一次 languageChanged,让已挂载的界面重取文案;
|
|
2029
|
+
* 不是当前语言就不发,白重刷一遍界面没有意义。
|
|
2030
|
+
*/
|
|
2031
|
+
addResources(lang, resources) {
|
|
2032
|
+
this.instance.addResourceBundle(lang, "translation", resources, true, false);
|
|
2033
|
+
if (lang === this.instance.language) {
|
|
2034
|
+
this.instance.emit("languageChanged", lang);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* 该语言是否已有语言包。未注册的语言不会抛错,只会按 fallbackLng 回退,
|
|
2039
|
+
* 所以想知道有没有真的生效得问这里。
|
|
2040
|
+
*/
|
|
2041
|
+
hasLanguage(lang) {
|
|
2042
|
+
return this.instance.hasResourceBundle(lang, "translation");
|
|
2043
|
+
}
|
|
1556
2044
|
/**
|
|
1557
2045
|
* 监听语言变更,返回取消订阅的函数
|
|
1558
2046
|
*/
|
|
@@ -1570,6 +2058,13 @@ class I18nManager {
|
|
|
1570
2058
|
function tOrFallback(i18n, key, fallback) {
|
|
1571
2059
|
return (i18n == null ? void 0 : i18n.tOr(key, fallback)) ?? fallback;
|
|
1572
2060
|
}
|
|
2061
|
+
function zhText(key) {
|
|
2062
|
+
const found = key.split(".").reduce(
|
|
2063
|
+
(node, part) => node && typeof node === "object" ? node[part] : void 0,
|
|
2064
|
+
zhResources
|
|
2065
|
+
);
|
|
2066
|
+
return typeof found === "string" ? found : "";
|
|
2067
|
+
}
|
|
1573
2068
|
class ExtensionManager {
|
|
1574
2069
|
constructor() {
|
|
1575
2070
|
this.extensions = /* @__PURE__ */ new Map();
|
|
@@ -1649,6 +2144,16 @@ class ExtensionManager {
|
|
|
1649
2144
|
return this.extensions.has(name);
|
|
1650
2145
|
}
|
|
1651
2146
|
}
|
|
2147
|
+
const PANEL_OFFSET = 6;
|
|
2148
|
+
const PANEL_SHIFT_PADDING = 8;
|
|
2149
|
+
function panelMiddleware() {
|
|
2150
|
+
return [offset(PANEL_OFFSET), flip(), shift({ padding: PANEL_SHIFT_PADDING })];
|
|
2151
|
+
}
|
|
2152
|
+
function placePanel(panel, x, y) {
|
|
2153
|
+
panel.style.position = "fixed";
|
|
2154
|
+
panel.style.left = `${x}px`;
|
|
2155
|
+
panel.style.top = `${y}px`;
|
|
2156
|
+
}
|
|
1652
2157
|
function positionPanel(trigger, panel, isCurrent) {
|
|
1653
2158
|
let stopAutoUpdate = null;
|
|
1654
2159
|
const update = async () => {
|
|
@@ -1656,12 +2161,10 @@ function positionPanel(trigger, panel, isCurrent) {
|
|
|
1656
2161
|
try {
|
|
1657
2162
|
const { x, y } = await computePosition(trigger, panel, {
|
|
1658
2163
|
placement: "bottom-start",
|
|
1659
|
-
middleware:
|
|
2164
|
+
middleware: panelMiddleware()
|
|
1660
2165
|
});
|
|
1661
2166
|
if (!isCurrent()) return;
|
|
1662
|
-
panel
|
|
1663
|
-
panel.style.left = `${x}px`;
|
|
1664
|
-
panel.style.top = `${y}px`;
|
|
2167
|
+
placePanel(panel, x, y);
|
|
1665
2168
|
} catch {
|
|
1666
2169
|
}
|
|
1667
2170
|
};
|
|
@@ -1696,10 +2199,20 @@ function listenPanelDismiss(trigger, panel, onClose) {
|
|
|
1696
2199
|
document.removeEventListener("keydown", onKeyDown, true);
|
|
1697
2200
|
};
|
|
1698
2201
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
2202
|
+
const THEME_PREFIX = "atri-theme-";
|
|
2203
|
+
function syncThemeClasses(target, root) {
|
|
2204
|
+
Array.from(target.classList).filter((name) => name.startsWith(THEME_PREFIX)).forEach((name) => target.classList.remove(name));
|
|
2205
|
+
const themes = Array.from(root.classList).filter((name) => name.startsWith(THEME_PREFIX));
|
|
2206
|
+
target.classList.add(...themes);
|
|
2207
|
+
}
|
|
2208
|
+
function followThemeClasses(source, panel) {
|
|
2209
|
+
const root = (source == null ? void 0 : source.closest(".atri-editor")) ?? null;
|
|
2210
|
+
if (root) syncThemeClasses(panel, root);
|
|
2211
|
+
if (!root || typeof MutationObserver === "undefined") return () => {
|
|
2212
|
+
};
|
|
2213
|
+
const observer = new MutationObserver(() => syncThemeClasses(panel, root));
|
|
2214
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
2215
|
+
return () => observer.disconnect();
|
|
1703
2216
|
}
|
|
1704
2217
|
const FIELDS = {
|
|
1705
2218
|
image: { url: true, alt: true, kind: "image" },
|
|
@@ -1712,6 +2225,7 @@ class InsertPanel {
|
|
|
1712
2225
|
this.mode = null;
|
|
1713
2226
|
this.floating = null;
|
|
1714
2227
|
this.detachDismiss = null;
|
|
2228
|
+
this.detachTheme = null;
|
|
1715
2229
|
this.labelled = [];
|
|
1716
2230
|
this.editor = options.editor;
|
|
1717
2231
|
this.runtime = options.runtime;
|
|
@@ -1739,17 +2253,19 @@ class InsertPanel {
|
|
|
1739
2253
|
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, mode);
|
|
1740
2254
|
}
|
|
1741
2255
|
close() {
|
|
1742
|
-
var _a, _b, _c, _d;
|
|
2256
|
+
var _a, _b, _c, _d, _e;
|
|
1743
2257
|
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
1744
2258
|
this.detachDismiss = null;
|
|
1745
|
-
(_b = this.
|
|
2259
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
2260
|
+
this.detachTheme = null;
|
|
2261
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
1746
2262
|
this.floating = null;
|
|
1747
|
-
(
|
|
2263
|
+
(_d = this.element) == null ? void 0 : _d.remove();
|
|
1748
2264
|
this.element = null;
|
|
1749
2265
|
this.labelled = [];
|
|
1750
2266
|
if (this.mode === null) return;
|
|
1751
2267
|
this.mode = null;
|
|
1752
|
-
(
|
|
2268
|
+
(_e = this.onOpenChange) == null ? void 0 : _e.call(this, null);
|
|
1753
2269
|
}
|
|
1754
2270
|
destroy() {
|
|
1755
2271
|
var _a;
|
|
@@ -1844,7 +2360,7 @@ class InsertPanel {
|
|
|
1844
2360
|
}
|
|
1845
2361
|
document.body.appendChild(panel);
|
|
1846
2362
|
this.element = panel;
|
|
1847
|
-
|
|
2363
|
+
this.detachTheme = followThemeClasses(this.editor.view.dom, panel);
|
|
1848
2364
|
(urlInput ?? altInput ?? browse).focus();
|
|
1849
2365
|
}
|
|
1850
2366
|
/**
|
|
@@ -1875,6 +2391,232 @@ class InsertPanel {
|
|
|
1875
2391
|
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
1876
2392
|
}
|
|
1877
2393
|
}
|
|
2394
|
+
class StyleOptionPanel {
|
|
2395
|
+
constructor(options) {
|
|
2396
|
+
this.element = null;
|
|
2397
|
+
this.floating = null;
|
|
2398
|
+
this.detachDismiss = null;
|
|
2399
|
+
this.detachTheme = null;
|
|
2400
|
+
this.anchor = null;
|
|
2401
|
+
this.currentKey = null;
|
|
2402
|
+
this.rows = [];
|
|
2403
|
+
this.editor = options.editor;
|
|
2404
|
+
this.i18n = options.i18n;
|
|
2405
|
+
this.onOpenChange = options.onOpenChange;
|
|
2406
|
+
}
|
|
2407
|
+
get isOpen() {
|
|
2408
|
+
return this.element !== null;
|
|
2409
|
+
}
|
|
2410
|
+
get openKey() {
|
|
2411
|
+
return this.currentKey;
|
|
2412
|
+
}
|
|
2413
|
+
/** 面板是否正开在某个指定触发器上——工具栏据此决定按钮的按下态给谁 */
|
|
2414
|
+
isOpenFor(anchor) {
|
|
2415
|
+
return this.isOpen && this.anchor === anchor;
|
|
2416
|
+
}
|
|
2417
|
+
toggle(anchor, spec) {
|
|
2418
|
+
if (this.isOpen) {
|
|
2419
|
+
const switching = this.anchor !== anchor;
|
|
2420
|
+
this.close();
|
|
2421
|
+
if (switching) this.open(anchor, spec);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
this.open(anchor, spec);
|
|
2425
|
+
}
|
|
2426
|
+
close() {
|
|
2427
|
+
var _a, _b, _c, _d;
|
|
2428
|
+
if (!this.element) return;
|
|
2429
|
+
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
2430
|
+
this.detachDismiss = null;
|
|
2431
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
2432
|
+
this.detachTheme = null;
|
|
2433
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
2434
|
+
this.floating = null;
|
|
2435
|
+
this.element.remove();
|
|
2436
|
+
this.element = null;
|
|
2437
|
+
this.anchor = null;
|
|
2438
|
+
this.currentKey = null;
|
|
2439
|
+
this.rows = [];
|
|
2440
|
+
(_d = this.onOpenChange) == null ? void 0 : _d.call(this, false);
|
|
2441
|
+
}
|
|
2442
|
+
destroy() {
|
|
2443
|
+
this.close();
|
|
2444
|
+
}
|
|
2445
|
+
open(anchor, spec) {
|
|
2446
|
+
var _a;
|
|
2447
|
+
const panel = this.build(spec);
|
|
2448
|
+
document.body.appendChild(panel);
|
|
2449
|
+
this.detachTheme = followThemeClasses(this.editor.view.dom, panel);
|
|
2450
|
+
this.element = panel;
|
|
2451
|
+
this.anchor = anchor;
|
|
2452
|
+
this.currentKey = spec.key;
|
|
2453
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
2454
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
2455
|
+
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
2456
|
+
panel.focus();
|
|
2457
|
+
}
|
|
2458
|
+
labelOf(option) {
|
|
2459
|
+
if (!option.i18nKey) return option.label;
|
|
2460
|
+
return tOrFallback(this.i18n, option.i18nKey, option.label);
|
|
2461
|
+
}
|
|
2462
|
+
build(spec) {
|
|
2463
|
+
const panel = document.createElement("div");
|
|
2464
|
+
panel.className = `atri-style-panel atri-style-panel--${spec.shape}`;
|
|
2465
|
+
panel.setAttribute("role", "menu");
|
|
2466
|
+
panel.setAttribute("aria-label", spec.label);
|
|
2467
|
+
panel.tabIndex = -1;
|
|
2468
|
+
panel.addEventListener("keydown", (event) => this.onKeyDown(event));
|
|
2469
|
+
const body = document.createElement("div");
|
|
2470
|
+
body.className = "atri-style-panel-body";
|
|
2471
|
+
this.rows = [];
|
|
2472
|
+
spec.options.forEach((option) => {
|
|
2473
|
+
body.appendChild(this.makeRow(option, spec));
|
|
2474
|
+
});
|
|
2475
|
+
if (spec.customColorLabel) {
|
|
2476
|
+
body.appendChild(this.makeCustomColorRow(spec));
|
|
2477
|
+
}
|
|
2478
|
+
if (spec.clearLabel) {
|
|
2479
|
+
const clear = {
|
|
2480
|
+
value: "",
|
|
2481
|
+
label: spec.clearLabel,
|
|
2482
|
+
isClear: true
|
|
2483
|
+
};
|
|
2484
|
+
body.appendChild(this.makeRow(clear, spec));
|
|
2485
|
+
}
|
|
2486
|
+
panel.appendChild(body);
|
|
2487
|
+
return panel;
|
|
2488
|
+
}
|
|
2489
|
+
makeRow(option, spec) {
|
|
2490
|
+
const row = document.createElement("button");
|
|
2491
|
+
row.type = "button";
|
|
2492
|
+
row.className = "atri-style-panel-row";
|
|
2493
|
+
row.setAttribute("role", "menuitem");
|
|
2494
|
+
row.dataset.styleValue = option.value;
|
|
2495
|
+
if (option.isClear) row.classList.add("atri-style-panel-row--clear");
|
|
2496
|
+
const label = this.labelOf(option);
|
|
2497
|
+
row.title = label;
|
|
2498
|
+
row.setAttribute("aria-label", label);
|
|
2499
|
+
if (option.swatch) {
|
|
2500
|
+
const swatch = document.createElement("span");
|
|
2501
|
+
swatch.className = "atri-style-panel-swatch";
|
|
2502
|
+
swatch.style.backgroundColor = option.swatch;
|
|
2503
|
+
swatch.setAttribute("aria-hidden", "true");
|
|
2504
|
+
row.appendChild(swatch);
|
|
2505
|
+
if (spec.shape === "grid") row.classList.add("atri-style-panel-row--swatch-only");
|
|
2506
|
+
}
|
|
2507
|
+
if (spec.shape === "list" || !option.swatch) {
|
|
2508
|
+
const text = document.createElement("span");
|
|
2509
|
+
text.className = "atri-style-panel-text";
|
|
2510
|
+
text.textContent = label;
|
|
2511
|
+
if (option.preview) text.style.fontFamily = option.preview;
|
|
2512
|
+
row.appendChild(text);
|
|
2513
|
+
}
|
|
2514
|
+
row.addEventListener("click", () => {
|
|
2515
|
+
if (option.isClear) {
|
|
2516
|
+
spec.onPick("");
|
|
2517
|
+
} else {
|
|
2518
|
+
spec.onPick(option.value);
|
|
2519
|
+
}
|
|
2520
|
+
this.close();
|
|
2521
|
+
});
|
|
2522
|
+
this.rows.push(row);
|
|
2523
|
+
return row;
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* 原生取色器:不在预设色板里时用它。
|
|
2527
|
+
*
|
|
2528
|
+
* 监听 change 而不是 input——input 在拖动取色器的过程中每移一像素就触发一次,
|
|
2529
|
+
* 每一次都是一条进撤销历史的事务,用户按一次撤销只能退一格,得按上百次才回到起点。
|
|
2530
|
+
*/
|
|
2531
|
+
makeCustomColorRow(spec) {
|
|
2532
|
+
const row = document.createElement("label");
|
|
2533
|
+
row.className = "atri-style-panel-custom";
|
|
2534
|
+
const input = document.createElement("input");
|
|
2535
|
+
input.type = "color";
|
|
2536
|
+
input.className = "atri-style-panel-custom-input";
|
|
2537
|
+
input.value = "#1890ff";
|
|
2538
|
+
input.addEventListener("change", () => {
|
|
2539
|
+
spec.onPick(input.value);
|
|
2540
|
+
this.close();
|
|
2541
|
+
});
|
|
2542
|
+
const text = document.createElement("span");
|
|
2543
|
+
text.textContent = spec.customColorLabel ?? "";
|
|
2544
|
+
row.appendChild(input);
|
|
2545
|
+
row.appendChild(text);
|
|
2546
|
+
return row;
|
|
2547
|
+
}
|
|
2548
|
+
onKeyDown(event) {
|
|
2549
|
+
const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
|
|
2550
|
+
if (event.key === "Enter" || event.key === " ") return;
|
|
2551
|
+
if (step === void 0) return;
|
|
2552
|
+
event.preventDefault();
|
|
2553
|
+
if (this.rows.length === 0) return;
|
|
2554
|
+
const focused = document.activeElement;
|
|
2555
|
+
const current = this.rows.indexOf(focused);
|
|
2556
|
+
const next = current < 0 ? step > 0 ? 0 : this.rows.length - 1 : (current + step + this.rows.length) % this.rows.length;
|
|
2557
|
+
this.rows[next].focus();
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
const TEXT_COLORS = [
|
|
2561
|
+
"rgb(31, 31, 31)",
|
|
2562
|
+
"rgb(89, 89, 89)",
|
|
2563
|
+
"rgb(24, 144, 255)",
|
|
2564
|
+
"rgb(82, 196, 26)",
|
|
2565
|
+
"rgb(250, 173, 20)",
|
|
2566
|
+
"rgb(245, 34, 45)",
|
|
2567
|
+
"rgb(114, 46, 209)",
|
|
2568
|
+
"rgb(235, 47, 150)",
|
|
2569
|
+
"rgb(19, 194, 194)",
|
|
2570
|
+
"rgb(47, 84, 235)",
|
|
2571
|
+
"rgb(160, 97, 42)",
|
|
2572
|
+
"rgb(120, 120, 120)"
|
|
2573
|
+
].map((value) => ({ value, label: value, swatch: value }));
|
|
2574
|
+
const HIGHLIGHT_COLORS = [
|
|
2575
|
+
"rgb(255, 241, 184)",
|
|
2576
|
+
"rgb(200, 250, 170)",
|
|
2577
|
+
"rgb(168, 223, 255)",
|
|
2578
|
+
"rgb(255, 192, 203)",
|
|
2579
|
+
"rgb(223, 190, 255)",
|
|
2580
|
+
"rgb(255, 214, 168)",
|
|
2581
|
+
"rgb(178, 235, 242)",
|
|
2582
|
+
"rgb(230, 230, 230)"
|
|
2583
|
+
].map((value) => ({ value, label: value, swatch: value }));
|
|
2584
|
+
const FONT_SIZES = [
|
|
2585
|
+
"12px",
|
|
2586
|
+
"14px",
|
|
2587
|
+
"16px",
|
|
2588
|
+
"18px",
|
|
2589
|
+
"20px",
|
|
2590
|
+
"24px",
|
|
2591
|
+
"28px",
|
|
2592
|
+
"32px",
|
|
2593
|
+
"40px",
|
|
2594
|
+
"48px"
|
|
2595
|
+
].map((value) => ({ value, label: value }));
|
|
2596
|
+
const FONT_FAMILIES = [
|
|
2597
|
+
{
|
|
2598
|
+
value: '-apple-system, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
2599
|
+
label: "Sans",
|
|
2600
|
+
i18nKey: "formatting.fontSans",
|
|
2601
|
+
preview: "sans-serif"
|
|
2602
|
+
},
|
|
2603
|
+
{
|
|
2604
|
+
value: 'Georgia, "Times New Roman", "Songti SC", SimSun, serif',
|
|
2605
|
+
label: "Serif",
|
|
2606
|
+
i18nKey: "formatting.fontSerif",
|
|
2607
|
+
preview: "serif"
|
|
2608
|
+
},
|
|
2609
|
+
{
|
|
2610
|
+
value: '"SFMono-Regular", Consolas, Menlo, monospace',
|
|
2611
|
+
label: "Monospace",
|
|
2612
|
+
i18nKey: "formatting.fontMono",
|
|
2613
|
+
preview: "monospace"
|
|
2614
|
+
},
|
|
2615
|
+
{ value: '"PingFang SC", "Microsoft YaHei", sans-serif', label: "PingFang / YaHei" },
|
|
2616
|
+
{ value: 'SimSun, "Songti SC", serif', label: "SimSun" },
|
|
2617
|
+
{ value: 'SimHei, "Heiti SC", sans-serif', label: "SimHei" },
|
|
2618
|
+
{ value: 'KaiTi, "Kaiti SC", serif', label: "KaiTi" }
|
|
2619
|
+
];
|
|
1878
2620
|
const MAX_COLS = 8;
|
|
1879
2621
|
const MAX_ROWS = 6;
|
|
1880
2622
|
class TableGridPanel {
|
|
@@ -1882,6 +2624,7 @@ class TableGridPanel {
|
|
|
1882
2624
|
this.element = null;
|
|
1883
2625
|
this.floating = null;
|
|
1884
2626
|
this.detachDismiss = null;
|
|
2627
|
+
this.detachTheme = null;
|
|
1885
2628
|
this.cursorRow = -1;
|
|
1886
2629
|
this.cursorCol = -1;
|
|
1887
2630
|
this.cells = [];
|
|
@@ -1906,18 +2649,20 @@ class TableGridPanel {
|
|
|
1906
2649
|
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
1907
2650
|
}
|
|
1908
2651
|
close() {
|
|
1909
|
-
var _a, _b, _c;
|
|
2652
|
+
var _a, _b, _c, _d;
|
|
1910
2653
|
if (!this.element) return;
|
|
1911
2654
|
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
1912
2655
|
this.detachDismiss = null;
|
|
1913
|
-
(_b = this.
|
|
2656
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
2657
|
+
this.detachTheme = null;
|
|
2658
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
1914
2659
|
this.floating = null;
|
|
1915
2660
|
this.element.remove();
|
|
1916
2661
|
this.element = null;
|
|
1917
2662
|
this.cells = [];
|
|
1918
2663
|
this.cursorRow = -1;
|
|
1919
2664
|
this.cursorCol = -1;
|
|
1920
|
-
(
|
|
2665
|
+
(_d = this.onOpenChange) == null ? void 0 : _d.call(this, false);
|
|
1921
2666
|
}
|
|
1922
2667
|
destroy() {
|
|
1923
2668
|
this.close();
|
|
@@ -1955,7 +2700,7 @@ class TableGridPanel {
|
|
|
1955
2700
|
panel.appendChild(hint);
|
|
1956
2701
|
document.body.appendChild(panel);
|
|
1957
2702
|
this.element = panel;
|
|
1958
|
-
|
|
2703
|
+
this.detachTheme = followThemeClasses(this.editor.view.dom, panel);
|
|
1959
2704
|
panel.focus();
|
|
1960
2705
|
}
|
|
1961
2706
|
/**
|
|
@@ -2008,11 +2753,126 @@ class TableGridPanel {
|
|
|
2008
2753
|
this.highlight(row + 1, col + 1);
|
|
2009
2754
|
}
|
|
2010
2755
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2756
|
+
class ToolbarMenuPanel {
|
|
2757
|
+
constructor(options) {
|
|
2758
|
+
this.element = null;
|
|
2759
|
+
this.floating = null;
|
|
2760
|
+
this.detachDismiss = null;
|
|
2761
|
+
this.detachTheme = null;
|
|
2762
|
+
this.anchor = null;
|
|
2763
|
+
this.currentKey = null;
|
|
2764
|
+
this.rows = [];
|
|
2765
|
+
this.editor = options.editor;
|
|
2766
|
+
this.onOpenChange = options.onOpenChange;
|
|
2767
|
+
}
|
|
2768
|
+
get isOpen() {
|
|
2769
|
+
return this.element !== null;
|
|
2770
|
+
}
|
|
2771
|
+
get openKey() {
|
|
2772
|
+
return this.currentKey;
|
|
2773
|
+
}
|
|
2774
|
+
toggle(anchor, spec) {
|
|
2775
|
+
if (this.isOpen) {
|
|
2776
|
+
const switching = this.anchor !== anchor;
|
|
2777
|
+
this.close();
|
|
2778
|
+
if (switching) this.open(anchor, spec);
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
this.open(anchor, spec);
|
|
2782
|
+
}
|
|
2783
|
+
close() {
|
|
2784
|
+
var _a, _b, _c, _d;
|
|
2785
|
+
if (!this.element) return;
|
|
2786
|
+
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
2787
|
+
this.detachDismiss = null;
|
|
2788
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
2789
|
+
this.detachTheme = null;
|
|
2790
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
2791
|
+
this.floating = null;
|
|
2792
|
+
this.element.remove();
|
|
2793
|
+
this.element = null;
|
|
2794
|
+
this.anchor = null;
|
|
2795
|
+
this.currentKey = null;
|
|
2796
|
+
this.rows = [];
|
|
2797
|
+
(_d = this.onOpenChange) == null ? void 0 : _d.call(this, false);
|
|
2798
|
+
}
|
|
2799
|
+
destroy() {
|
|
2800
|
+
this.close();
|
|
2801
|
+
}
|
|
2802
|
+
open(anchor, spec) {
|
|
2803
|
+
var _a;
|
|
2804
|
+
const panel = this.build(spec);
|
|
2805
|
+
document.body.appendChild(panel);
|
|
2806
|
+
this.detachTheme = followThemeClasses(this.editorViewDom(anchor), panel);
|
|
2807
|
+
this.element = panel;
|
|
2808
|
+
this.anchor = anchor;
|
|
2809
|
+
this.currentKey = spec.key;
|
|
2810
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
2811
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
2812
|
+
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
2813
|
+
panel.focus();
|
|
2814
|
+
}
|
|
2815
|
+
/**
|
|
2816
|
+
* 主题类要从编辑器根上取,而这里只拿得到一个锚在工具栏上的按钮:
|
|
2817
|
+
* 沿 DOM 往上找 .atri-editor 比让调用方再多传一个参数省事,且工具栏就在那个根里面
|
|
2818
|
+
*/
|
|
2819
|
+
editorViewDom(anchor) {
|
|
2820
|
+
return anchor.closest(".atri-editor");
|
|
2821
|
+
}
|
|
2822
|
+
build(spec) {
|
|
2823
|
+
const panel = document.createElement("div");
|
|
2824
|
+
panel.className = "atri-toolbar-menu";
|
|
2825
|
+
panel.setAttribute("role", "menu");
|
|
2826
|
+
panel.setAttribute("aria-label", spec.label);
|
|
2827
|
+
panel.tabIndex = -1;
|
|
2828
|
+
panel.addEventListener("keydown", (event) => this.onKeyDown(event));
|
|
2829
|
+
this.rows = [];
|
|
2830
|
+
spec.rows.forEach((row) => panel.appendChild(this.makeRow(row)));
|
|
2831
|
+
return panel;
|
|
2832
|
+
}
|
|
2833
|
+
makeRow(row) {
|
|
2834
|
+
const button = document.createElement("button");
|
|
2835
|
+
button.type = "button";
|
|
2836
|
+
button.className = "atri-toolbar-menu-item";
|
|
2837
|
+
button.setAttribute("role", "menuitemradio");
|
|
2838
|
+
button.setAttribute("aria-checked", String(row.active));
|
|
2839
|
+
button.dataset.menuItem = row.id;
|
|
2840
|
+
button.textContent = row.label;
|
|
2841
|
+
button.disabled = row.disabled;
|
|
2842
|
+
if (row.active) button.classList.add("active");
|
|
2843
|
+
button.addEventListener("click", () => {
|
|
2844
|
+
row.run();
|
|
2845
|
+
this.close();
|
|
2846
|
+
this.editor.commands.focus();
|
|
2847
|
+
});
|
|
2848
|
+
this.rows.push(button);
|
|
2849
|
+
return button;
|
|
2850
|
+
}
|
|
2851
|
+
onKeyDown(event) {
|
|
2852
|
+
if (event.key === "Enter" || event.key === " ") return;
|
|
2853
|
+
const step = { ArrowDown: 1, ArrowRight: 1, ArrowUp: -1, ArrowLeft: -1 }[event.key];
|
|
2854
|
+
if (step === void 0) return;
|
|
2855
|
+
event.preventDefault();
|
|
2856
|
+
if (this.rows.length === 0) return;
|
|
2857
|
+
const current = this.rows.indexOf(document.activeElement);
|
|
2858
|
+
const next = current < 0 ? step > 0 ? 0 : this.rows.length - 1 : (current + step + this.rows.length) % this.rows.length;
|
|
2859
|
+
this.rows[next].focus();
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
const SET_COMMAND = {
|
|
2863
|
+
color: "setColor",
|
|
2864
|
+
backgroundColor: "setBackgroundColor",
|
|
2865
|
+
fontSize: "setFontSize",
|
|
2866
|
+
fontFamily: "setFontFamily"
|
|
2867
|
+
};
|
|
2868
|
+
function canUseTextStyle(editor, attr) {
|
|
2869
|
+
var _a, _b;
|
|
2870
|
+
const command = editor.commands[SET_COMMAND[attr]];
|
|
2871
|
+
if (typeof command !== "function") return false;
|
|
2872
|
+
return !!((_b = (_a = editor.schema.marks.textStyle) == null ? void 0 : _a.spec.attrs) == null ? void 0 : _b[attr]);
|
|
2873
|
+
}
|
|
2874
|
+
function hasMark(editor, name) {
|
|
2875
|
+
return !!editor.schema.marks[name];
|
|
2016
2876
|
}
|
|
2017
2877
|
const icons = {
|
|
2018
2878
|
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>',
|
|
@@ -2028,16 +2888,44 @@ const icons = {
|
|
|
2028
2888
|
code: '<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="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
2029
2889
|
list: '<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"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>',
|
|
2030
2890
|
listOrdered: '<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"><line x1="10" x2="21" y1="6" y2="6"/><line x1="10" x2="21" y1="12" y2="12"/><line x1="10" x2="21" y1="18" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>',
|
|
2891
|
+
listChecks: '<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 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg>',
|
|
2892
|
+
horizontalRule: '<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"><line x1="5" x2="19" y1="12" y2="12"/></svg>',
|
|
2031
2893
|
quote: '<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="M16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"/><path d="M5 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a2 2 0 0 1-2 2 1 1 0 0 0-1 1v2a1 1 0 0 0 1 1 6 6 0 0 0 6-6V5a2 2 0 0 0-2-2z"/></svg>',
|
|
2032
2894
|
codeBlock: '<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="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
|
|
2033
2895
|
alignLeft: '<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"><line x1="21" x2="3" y1="6" y2="6"/><line x1="15" x2="3" y1="12" y2="12"/><line x1="17" x2="3" y1="18" y2="18"/></svg>',
|
|
2034
2896
|
alignCenter: '<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"><line x1="21" x2="3" y1="6" y2="6"/><line x1="17" x2="7" y1="12" y2="12"/><line x1="19" x2="5" y1="18" y2="18"/></svg>',
|
|
2035
2897
|
alignRight: '<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"><line x1="21" x2="3" y1="6" y2="6"/><line x1="21" x2="9" y1="12" y2="12"/><line x1="21" x2="7" y1="18" y2="18"/></svg>',
|
|
2898
|
+
alignJustify: '<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"><line x1="21" x2="3" y1="6" y2="6"/><line x1="21" x2="3" y1="12" y2="12"/><line x1="21" x2="3" y1="18" y2="18"/></svg>',
|
|
2036
2899
|
image: '<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" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg>',
|
|
2037
2900
|
paperclip: '<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="m16 6-8.4 8.6a2 2 0 0 0 2.8 2.8l8.4-8.6a4 4 0 1 0-5.6-5.6l-8.4 8.5a6 6 0 1 0 8.5 8.5l8.4-8.4"/></svg>',
|
|
2038
2901
|
link: '<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 17H7A5 5 0 1 1 7 7h2"/><path d="M15 7h2a5 5 0 1 1 0 10h-2"/><line x1="8" x2="16" y1="12" y2="12"/></svg>',
|
|
2039
2902
|
table: '<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="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/></svg>',
|
|
2040
|
-
trash: '<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 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>'
|
|
2903
|
+
trash: '<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 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>',
|
|
2904
|
+
// 表格操作菜单:统一用「表格边框 + 方向标记」一套句法——
|
|
2905
|
+
// 行操作画横分隔线、列操作画竖分隔线,加号落在哪一格就是插到哪一侧,
|
|
2906
|
+
// 减号落在哪一格就是删哪一格(两种删除靠分隔线方向区分)
|
|
2907
|
+
tableRowBefore: '<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="M3 15h18"/><path d="M12 4.5v3"/><path d="M10.5 6h3"/></svg>',
|
|
2908
|
+
tableRowAfter: '<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="M3 15h18"/><path d="M12 16.5v3"/><path d="M10.5 18h3"/></svg>',
|
|
2909
|
+
tableRowDelete: '<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="M3 15h18"/><path d="M10.5 12h3"/></svg>',
|
|
2910
|
+
tableColumnBefore: '<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="M4.5 12h3"/><path d="M6 10.5v3"/></svg>',
|
|
2911
|
+
tableColumnAfter: '<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="M16.5 12h3"/><path d="M18 10.5v3"/></svg>',
|
|
2912
|
+
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>',
|
|
2913
|
+
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>',
|
|
2914
|
+
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>',
|
|
2915
|
+
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>',
|
|
2916
|
+
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>',
|
|
2917
|
+
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>',
|
|
2918
|
+
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>',
|
|
2919
|
+
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>',
|
|
2920
|
+
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>',
|
|
2921
|
+
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>',
|
|
2922
|
+
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>',
|
|
2923
|
+
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>',
|
|
2924
|
+
// 下拉触发器用的通用字形:标题菜单与对齐菜单在"当前没有选中任何一级/一种对齐"时用它,
|
|
2925
|
+
// 有当前值时触发器换成那一项自己的图标,所见即所得
|
|
2926
|
+
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>',
|
|
2927
|
+
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>',
|
|
2928
|
+
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>'
|
|
2041
2929
|
};
|
|
2042
2930
|
const TOOLTIP_KEYS = {
|
|
2043
2931
|
undo: "editor.undo",
|
|
@@ -2045,6 +2933,13 @@ const TOOLTIP_KEYS = {
|
|
|
2045
2933
|
heading1: "editor.heading1",
|
|
2046
2934
|
heading2: "editor.heading2",
|
|
2047
2935
|
heading3: "editor.heading3",
|
|
2936
|
+
heading4: "editor.heading4",
|
|
2937
|
+
heading5: "editor.heading5",
|
|
2938
|
+
heading6: "editor.heading6",
|
|
2939
|
+
// 两个下拉触发器。'editor.heading' 这条词条躺了很久、背后没有任何功能,
|
|
2940
|
+
// 这批终于有了真正读它的人
|
|
2941
|
+
heading: "editor.heading",
|
|
2942
|
+
align: "editor.align",
|
|
2048
2943
|
paragraph: "editor.paragraph",
|
|
2049
2944
|
bold: "editor.bold",
|
|
2050
2945
|
italic: "editor.italic",
|
|
@@ -2053,30 +2948,45 @@ const TOOLTIP_KEYS = {
|
|
|
2053
2948
|
code: "editor.code",
|
|
2054
2949
|
bulletList: "editor.bulletList",
|
|
2055
2950
|
orderedList: "editor.orderedList",
|
|
2951
|
+
taskList: "editor.taskList",
|
|
2056
2952
|
blockquote: "editor.blockquote",
|
|
2057
2953
|
codeBlock: "editor.codeBlock",
|
|
2954
|
+
horizontalRule: "editor.horizontalRule",
|
|
2058
2955
|
alignLeft: "editor.alignLeft",
|
|
2059
2956
|
alignCenter: "editor.alignCenter",
|
|
2060
2957
|
alignRight: "editor.alignRight",
|
|
2958
|
+
alignJustify: "editor.alignJustify",
|
|
2061
2959
|
insertImage: "editor.image",
|
|
2062
2960
|
insertAttachment: "editor.attachment",
|
|
2063
2961
|
attachmentDisplay: "editor.attachmentDisplay",
|
|
2064
2962
|
insertTable: "editor.table",
|
|
2065
|
-
|
|
2963
|
+
insertLink: "editor.link",
|
|
2964
|
+
delete: "editor.delete",
|
|
2965
|
+
textColor: "editor.textColor",
|
|
2966
|
+
highlight: "editor.highlight",
|
|
2967
|
+
highlightColor: "editor.highlightColor",
|
|
2968
|
+
fontSize: "editor.fontSize",
|
|
2969
|
+
fontFamily: "editor.fontFamily",
|
|
2970
|
+
subscript: "editor.subscript",
|
|
2971
|
+
superscript: "editor.superscript",
|
|
2972
|
+
clearFormat: "editor.clearFormat"
|
|
2066
2973
|
};
|
|
2067
2974
|
class ToolbarManager {
|
|
2068
|
-
constructor(editor, container, config, i18n, mediaRuntime) {
|
|
2975
|
+
constructor(editor, container, config, i18n, mediaRuntime, linkPanel) {
|
|
2069
2976
|
var _a;
|
|
2070
2977
|
this.buttons = /* @__PURE__ */ new Map();
|
|
2071
2978
|
this.customTooltips = /* @__PURE__ */ new Map();
|
|
2072
2979
|
this.createdElements = [];
|
|
2073
2980
|
this.panel = null;
|
|
2074
2981
|
this.tablePanel = null;
|
|
2982
|
+
this.stylePanel = null;
|
|
2983
|
+
this.menuPanel = null;
|
|
2075
2984
|
this.editor = editor;
|
|
2076
2985
|
this.container = container;
|
|
2077
2986
|
this.config = config;
|
|
2078
2987
|
this.i18n = i18n;
|
|
2079
2988
|
this.mediaRuntime = mediaRuntime ?? null;
|
|
2989
|
+
this.linkPanel = linkPanel ?? null;
|
|
2080
2990
|
this.itemDefs = this.getDefaultItems();
|
|
2081
2991
|
this.createToolbarDOM();
|
|
2082
2992
|
this.bindEditorEvents();
|
|
@@ -2149,12 +3059,156 @@ class ToolbarManager {
|
|
|
2149
3059
|
get tableExtensionRegistered() {
|
|
2150
3060
|
return canInsertTable(this.editor);
|
|
2151
3061
|
}
|
|
2152
|
-
/**
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
3062
|
+
/** 格式面板按需创建:没点开过就不该有一个实例挂在编辑器上 */
|
|
3063
|
+
get styleOptionPanel() {
|
|
3064
|
+
this.stylePanel ?? (this.stylePanel = new StyleOptionPanel({
|
|
3065
|
+
editor: this.editor,
|
|
3066
|
+
i18n: this.i18n,
|
|
3067
|
+
// 开合不改文档,等不到 transaction,得主动刷一次按钮状态
|
|
3068
|
+
onOpenChange: () => this.updateButtonStates()
|
|
3069
|
+
}));
|
|
3070
|
+
return this.stylePanel;
|
|
3071
|
+
}
|
|
3072
|
+
tOr(key, fallback) {
|
|
3073
|
+
var _a;
|
|
3074
|
+
return ((_a = this.i18n) == null ? void 0 : _a.tOr(key, fallback)) ?? fallback;
|
|
3075
|
+
}
|
|
3076
|
+
/** 下拉菜单面板按需创建:没点开过就不该有一个实例挂在编辑器上 */
|
|
3077
|
+
get menu() {
|
|
3078
|
+
this.menuPanel ?? (this.menuPanel = new ToolbarMenuPanel({
|
|
3079
|
+
editor: this.editor,
|
|
3080
|
+
// 开合不改文档,等不到 transaction,得主动刷一次按钮状态
|
|
3081
|
+
onOpenChange: () => this.updateButtonStates()
|
|
3082
|
+
}));
|
|
3083
|
+
return this.menuPanel;
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* 把一个项做成下拉触发器。
|
|
3087
|
+
* 子项走的仍是同一张 itemDefs 表,所以它们和顶栏项一样受能力门控——
|
|
3088
|
+
* 表格被顶掉时 insertTable 就不会出现在菜单里,而不是摆一行按了没反应的
|
|
3089
|
+
*/
|
|
3090
|
+
menuItem(key, icon, children) {
|
|
3091
|
+
const def = {
|
|
3092
|
+
id: key,
|
|
3093
|
+
icon,
|
|
3094
|
+
popup: "menu",
|
|
3095
|
+
children,
|
|
3096
|
+
command: (_editor, button) => {
|
|
3097
|
+
var _a;
|
|
3098
|
+
const live = ((_a = this.itemDefs.get(key)) == null ? void 0 : _a.children) ?? children;
|
|
3099
|
+
this.menu.toggle(button, this.menuSpec(key, live, button));
|
|
3100
|
+
},
|
|
3101
|
+
isActive: () => {
|
|
3102
|
+
var _a;
|
|
3103
|
+
return ((_a = this.menuPanel) == null ? void 0 : _a.openKey) === key;
|
|
3104
|
+
},
|
|
3105
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3106
|
+
};
|
|
3107
|
+
return def;
|
|
3108
|
+
}
|
|
3109
|
+
/** 当前命中的子项:触发器的图标与提示都跟着它走,用户不必点开就知道现在的级别 */
|
|
3110
|
+
activeChild(def) {
|
|
3111
|
+
for (const childId of def.children ?? []) {
|
|
3112
|
+
const child = this.itemDefs.get(childId);
|
|
3113
|
+
if (child && !child.popup && child.isActive(this.editor) && !child.isDisabled(this.editor)) {
|
|
3114
|
+
return child;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
return null;
|
|
3118
|
+
}
|
|
3119
|
+
/** 子项词条 key;没有映射的(宿主的自定义项)退回定义里的兜底文案 */
|
|
3120
|
+
rowLabel(childId, child) {
|
|
3121
|
+
const key = TOOLTIP_KEYS[childId];
|
|
3122
|
+
return key ? this.tOr(key, zhText(key)) : child.tooltip ?? childId;
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* 菜单里带浮层的子项一律不收:子项被点中后菜单就关了,
|
|
3126
|
+
* 而面板要锚在触发器上——锚一个即将从文档里消失的行没有意义。
|
|
3127
|
+
* 这与 attachBubbleToolbar 拒绝浮层项是同一条理由。
|
|
3128
|
+
*/
|
|
3129
|
+
menuSpec(key, children, trigger) {
|
|
3130
|
+
const menuKey = TOOLTIP_KEYS[key];
|
|
3131
|
+
return {
|
|
3132
|
+
key,
|
|
3133
|
+
label: menuKey ? this.tOr(menuKey, zhText(menuKey)) : key,
|
|
3134
|
+
rows: children.reduce((rows, childId) => {
|
|
3135
|
+
const child = this.itemDefs.get(childId);
|
|
3136
|
+
if (!child) {
|
|
3137
|
+
console.warn(`[Atri Editor] Menu "${key}" declares unknown item "${childId}".`);
|
|
3138
|
+
return rows;
|
|
3139
|
+
}
|
|
3140
|
+
if (child.popup) {
|
|
3141
|
+
console.warn(
|
|
3142
|
+
`[Atri Editor] Menu item "${childId}" opens a panel, which menus cannot host.`
|
|
3143
|
+
);
|
|
3144
|
+
return rows;
|
|
3145
|
+
}
|
|
3146
|
+
rows.push({
|
|
3147
|
+
id: childId,
|
|
3148
|
+
label: this.rowLabel(childId, child),
|
|
3149
|
+
active: child.isActive(this.editor),
|
|
3150
|
+
disabled: child.isDisabled(this.editor),
|
|
3151
|
+
// 子项自己不需要触发器元素(只有浮层项会用),传过去是为了共用同一份定义
|
|
3152
|
+
run: () => child.command(this.editor, trigger)
|
|
3153
|
+
});
|
|
3154
|
+
return rows;
|
|
3155
|
+
}, [])
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
/**
|
|
3159
|
+
* 面板文案在这里现取,不在建项时算好存下来:
|
|
3160
|
+
* 语言切换后重开面板要拿到新词条,而项定义只在建栏时求值一次
|
|
3161
|
+
*/
|
|
3162
|
+
styleSpec(key, parts) {
|
|
3163
|
+
return {
|
|
3164
|
+
key,
|
|
3165
|
+
label: this.tOr(`editor.${key}`, key),
|
|
3166
|
+
shape: parts.shape,
|
|
3167
|
+
options: parts.options,
|
|
3168
|
+
// 色板才有"其他颜色"这一行;字号字体用原生控件选不出任意值
|
|
3169
|
+
customColorLabel: parts.swatch ? this.tOr("formatting.customColor", "其他颜色…") : void 0,
|
|
3170
|
+
// 清除行的措辞按形状分:颜色去掉是"无",字号字体退回的是"默认"
|
|
3171
|
+
clearLabel: parts.swatch ? this.tOr("formatting.noColor", "无") : this.tOr("formatting.defaultValue", "默认"),
|
|
3172
|
+
onPick: (value) => this.applyStyle(parts.attr, value)
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* 空串是"清除"而不是"设成空值":设成空串会留下一个属性为空串的 mark,
|
|
3177
|
+
* 而 textStyle 的 renderHTML 对空值照样会吐出一个 `<span style="">`,
|
|
3178
|
+
* 那种节点既看不见又会在往返里产噪音(见 extensions/formatting.ts 的发出侧)
|
|
3179
|
+
*/
|
|
3180
|
+
applyStyle(attr, value) {
|
|
3181
|
+
const chain = this.editor.chain().focus();
|
|
3182
|
+
if (attr === "highlight") {
|
|
3183
|
+
if (value === "") chain.unsetHighlight().run();
|
|
3184
|
+
else chain.setHighlight({ color: value }).run();
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
switch (attr) {
|
|
3188
|
+
case "color":
|
|
3189
|
+
if (value === "") chain.unsetColor().run();
|
|
3190
|
+
else chain.setColor(value).run();
|
|
3191
|
+
return;
|
|
3192
|
+
case "backgroundColor":
|
|
3193
|
+
if (value === "") chain.unsetBackgroundColor().run();
|
|
3194
|
+
else chain.setBackgroundColor(value).run();
|
|
3195
|
+
return;
|
|
3196
|
+
case "fontSize":
|
|
3197
|
+
if (value === "") chain.unsetFontSize().run();
|
|
3198
|
+
else chain.setFontSize(value).run();
|
|
3199
|
+
return;
|
|
3200
|
+
case "fontFamily":
|
|
3201
|
+
if (value === "") chain.unsetFontFamily().run();
|
|
3202
|
+
else chain.setFontFamily(value).run();
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
/**
|
|
3207
|
+
* 创建工具栏 DOM - 直接将按钮插入 container
|
|
3208
|
+
*/
|
|
3209
|
+
createToolbarDOM() {
|
|
3210
|
+
var _a;
|
|
3211
|
+
const items = (_a = this.config) == null ? void 0 : _a.items;
|
|
2158
3212
|
if (items) {
|
|
2159
3213
|
items.forEach((item) => {
|
|
2160
3214
|
const resolved = this.resolveItem(item);
|
|
@@ -2168,16 +3222,23 @@ class ToolbarManager {
|
|
|
2168
3222
|
}
|
|
2169
3223
|
const layout = [
|
|
2170
3224
|
["undo", "redo"],
|
|
2171
|
-
|
|
2172
|
-
["
|
|
2173
|
-
|
|
2174
|
-
["
|
|
2175
|
-
//
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
3225
|
+
// 标题收成一下拉:一到六级加正文七个取值平铺要占七格,而一次只用得上一个
|
|
3226
|
+
["heading"],
|
|
3227
|
+
// 链接跟着行内格式走:它也是一段文字上的标记,只是要填地址所以带浮层
|
|
3228
|
+
["bold", "italic", "underline", "strike", "code", "insertLink"],
|
|
3229
|
+
// 颜色与字号字体自成一组:都是"这段文字长什么样",且除 highlight 外都带浮层
|
|
3230
|
+
["textColor", "highlight", "highlightColor", "fontSize", "fontFamily"],
|
|
3231
|
+
["bulletList", "orderedList", "taskList", "blockquote", "codeBlock"],
|
|
3232
|
+
// 对齐同样收成一下拉
|
|
3233
|
+
["align"],
|
|
3234
|
+
// 分割线与表格是一组:都是"往正文里插一块"的动作,没有激活态
|
|
3235
|
+
["horizontalRule", "insertTable"],
|
|
3236
|
+
// 上下标与清除格式:作用于选区已有的格式,而不是加一种新格式
|
|
3237
|
+
["subscript", "superscript", "clearFormat"],
|
|
3238
|
+
["insertImage", "insertAttachment"]
|
|
2179
3239
|
];
|
|
2180
|
-
layout.
|
|
3240
|
+
const groups = layout.filter((group) => group.some((itemId) => this.itemDefs.has(itemId)));
|
|
3241
|
+
groups.forEach((group, groupIndex) => {
|
|
2181
3242
|
if (groupIndex > 0) {
|
|
2182
3243
|
const separator = document.createElement("div");
|
|
2183
3244
|
separator.className = "atri-editor-toolbar-separator";
|
|
@@ -2209,7 +3270,22 @@ class ToolbarManager {
|
|
|
2209
3270
|
return { id, def };
|
|
2210
3271
|
}
|
|
2211
3272
|
if ((_a = item.children) == null ? void 0 : _a.length) {
|
|
2212
|
-
|
|
3273
|
+
if (def.popup !== "menu") {
|
|
3274
|
+
console.warn(
|
|
3275
|
+
`[Atri Editor] Toolbar item "${id}" declares children, but it is not a dropdown; only the built-in dropdown items (heading, align) accept children.`
|
|
3276
|
+
);
|
|
3277
|
+
} else {
|
|
3278
|
+
const nested = item.children.filter((child) => {
|
|
3279
|
+
var _a2;
|
|
3280
|
+
return (_a2 = child.children) == null ? void 0 : _a2.length;
|
|
3281
|
+
});
|
|
3282
|
+
if (nested.length) {
|
|
3283
|
+
console.warn(
|
|
3284
|
+
`[Atri Editor] Nested children on "${nested.map((child) => child.id).join(", ")}" are not supported and were ignored.`
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
def.children = item.children.map((child) => child.id);
|
|
3288
|
+
}
|
|
2213
3289
|
}
|
|
2214
3290
|
return { id, def, overrides: item };
|
|
2215
3291
|
}
|
|
@@ -2229,9 +3305,14 @@ class ToolbarManager {
|
|
|
2229
3305
|
this.customTooltips.set(itemId, overrides.tooltip);
|
|
2230
3306
|
}
|
|
2231
3307
|
if (itemDef.popup) {
|
|
2232
|
-
button.setAttribute("aria-haspopup", "dialog");
|
|
3308
|
+
button.setAttribute("aria-haspopup", itemDef.popup === "menu" ? "menu" : "dialog");
|
|
2233
3309
|
button.setAttribute("aria-expanded", "false");
|
|
2234
3310
|
}
|
|
3311
|
+
if (itemDef.popup === "menu") {
|
|
3312
|
+
button.classList.add("atri-editor-toolbar-btn--menu");
|
|
3313
|
+
button.dataset.menuIcon = button.innerHTML;
|
|
3314
|
+
button.innerHTML += icons.chevronDown;
|
|
3315
|
+
}
|
|
2235
3316
|
button.addEventListener("click", (e) => {
|
|
2236
3317
|
e.preventDefault();
|
|
2237
3318
|
e.stopPropagation();
|
|
@@ -2251,12 +3332,9 @@ class ToolbarManager {
|
|
|
2251
3332
|
*/
|
|
2252
3333
|
applyTooltips() {
|
|
2253
3334
|
this.buttons.forEach((buttons, itemId) => {
|
|
2254
|
-
var _a;
|
|
2255
3335
|
const itemDef = this.itemDefs.get(itemId);
|
|
2256
3336
|
if (!itemDef) return;
|
|
2257
|
-
const
|
|
2258
|
-
const key = TOOLTIP_KEYS[itemId];
|
|
2259
|
-
const title = custom ?? (key ? ((_a = this.i18n) == null ? void 0 : _a.tOr(key, itemDef.tooltip)) ?? itemDef.tooltip : itemDef.tooltip);
|
|
3337
|
+
const title = this.itemTitle(itemId, itemDef);
|
|
2260
3338
|
buttons.forEach((button) => {
|
|
2261
3339
|
button.title = title;
|
|
2262
3340
|
});
|
|
@@ -2271,7 +3349,6 @@ class ToolbarManager {
|
|
|
2271
3349
|
items.set("insertImage", {
|
|
2272
3350
|
id: "insertImage",
|
|
2273
3351
|
icon: icons.image,
|
|
2274
|
-
tooltip: "图片",
|
|
2275
3352
|
popup: true,
|
|
2276
3353
|
command: (_editor, button) => this.openInsert("image", button),
|
|
2277
3354
|
isActive: () => {
|
|
@@ -2283,7 +3360,6 @@ class ToolbarManager {
|
|
|
2283
3360
|
items.set("insertAttachment", {
|
|
2284
3361
|
id: "insertAttachment",
|
|
2285
3362
|
icon: icons.paperclip,
|
|
2286
|
-
tooltip: "附件",
|
|
2287
3363
|
popup: true,
|
|
2288
3364
|
command: (_editor, button) => this.openInsert("attachment", button),
|
|
2289
3365
|
isActive: () => {
|
|
@@ -2295,7 +3371,6 @@ class ToolbarManager {
|
|
|
2295
3371
|
items.set("attachmentDisplay", {
|
|
2296
3372
|
id: "attachmentDisplay",
|
|
2297
3373
|
icon: icons.link,
|
|
2298
|
-
tooltip: "附件样式",
|
|
2299
3374
|
command: (editor) => editor.chain().focus().toggleAttachmentDisplay().run(),
|
|
2300
3375
|
isActive: (editor) => editor.isActive("attachment") || editor.isActive("attachmentLink"),
|
|
2301
3376
|
isDisabled: (editor) => !editor.isActive("attachment") && !editor.isActive("attachmentLink")
|
|
@@ -2305,7 +3380,6 @@ class ToolbarManager {
|
|
|
2305
3380
|
items.set("insertTable", {
|
|
2306
3381
|
id: "insertTable",
|
|
2307
3382
|
icon: icons.table,
|
|
2308
|
-
tooltip: "表格",
|
|
2309
3383
|
popup: true,
|
|
2310
3384
|
command: (_editor, button) => {
|
|
2311
3385
|
var _a;
|
|
@@ -2318,10 +3392,144 @@ class ToolbarManager {
|
|
|
2318
3392
|
isDisabled: (editor) => !editor.isEditable
|
|
2319
3393
|
});
|
|
2320
3394
|
}
|
|
3395
|
+
if (this.linkPanel) {
|
|
3396
|
+
items.set("insertLink", {
|
|
3397
|
+
id: "insertLink",
|
|
3398
|
+
icon: icons.link,
|
|
3399
|
+
popup: true,
|
|
3400
|
+
command: (_editor, button) => {
|
|
3401
|
+
var _a;
|
|
3402
|
+
return (_a = this.linkPanel) == null ? void 0 : _a.toggle(button);
|
|
3403
|
+
},
|
|
3404
|
+
// 浮层开着时按下去是关,所以按钮的"按下态"跟的是浮层,不是选区里有没有链接
|
|
3405
|
+
isActive: (editor) => {
|
|
3406
|
+
var _a;
|
|
3407
|
+
return ((_a = this.linkPanel) == null ? void 0 : _a.isOpen) === true || editor.isActive("link");
|
|
3408
|
+
},
|
|
3409
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
const textStyleReady = (attr) => canUseTextStyle(this.editor, attr);
|
|
3413
|
+
if (textStyleReady("color")) {
|
|
3414
|
+
items.set("textColor", {
|
|
3415
|
+
id: "textColor",
|
|
3416
|
+
icon: icons.textColor,
|
|
3417
|
+
popup: true,
|
|
3418
|
+
command: (_editor, button) => this.styleOptionPanel.toggle(
|
|
3419
|
+
button,
|
|
3420
|
+
this.styleSpec("textColor", {
|
|
3421
|
+
shape: "grid",
|
|
3422
|
+
options: TEXT_COLORS,
|
|
3423
|
+
swatch: true,
|
|
3424
|
+
attr: "color"
|
|
3425
|
+
})
|
|
3426
|
+
),
|
|
3427
|
+
// 浮层项的按下态只跟浮层走:选区里有没有颜色是文档事实,不是"这个按钮正按着"
|
|
3428
|
+
isActive: () => {
|
|
3429
|
+
var _a;
|
|
3430
|
+
return ((_a = this.stylePanel) == null ? void 0 : _a.openKey) === "textColor";
|
|
3431
|
+
},
|
|
3432
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3435
|
+
if (hasMark(this.editor, "highlight")) {
|
|
3436
|
+
items.set("highlight", {
|
|
3437
|
+
id: "highlight",
|
|
3438
|
+
icon: icons.highlight,
|
|
3439
|
+
command: (editor) => editor.chain().focus().toggleHighlight().run(),
|
|
3440
|
+
isActive: (editor) => editor.isActive("highlight"),
|
|
3441
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3442
|
+
});
|
|
3443
|
+
items.set("highlightColor", {
|
|
3444
|
+
id: "highlightColor",
|
|
3445
|
+
icon: icons.highlightColor,
|
|
3446
|
+
popup: true,
|
|
3447
|
+
command: (_editor, button) => this.styleOptionPanel.toggle(
|
|
3448
|
+
button,
|
|
3449
|
+
this.styleSpec("highlightColor", {
|
|
3450
|
+
shape: "grid",
|
|
3451
|
+
options: HIGHLIGHT_COLORS,
|
|
3452
|
+
swatch: true,
|
|
3453
|
+
attr: "highlight"
|
|
3454
|
+
})
|
|
3455
|
+
),
|
|
3456
|
+
isActive: () => {
|
|
3457
|
+
var _a;
|
|
3458
|
+
return ((_a = this.stylePanel) == null ? void 0 : _a.openKey) === "highlightColor";
|
|
3459
|
+
},
|
|
3460
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3463
|
+
if (textStyleReady("fontSize")) {
|
|
3464
|
+
items.set("fontSize", {
|
|
3465
|
+
id: "fontSize",
|
|
3466
|
+
icon: icons.fontSize,
|
|
3467
|
+
popup: true,
|
|
3468
|
+
command: (_editor, button) => this.styleOptionPanel.toggle(
|
|
3469
|
+
button,
|
|
3470
|
+
this.styleSpec("fontSize", {
|
|
3471
|
+
shape: "list",
|
|
3472
|
+
options: FONT_SIZES,
|
|
3473
|
+
swatch: false,
|
|
3474
|
+
attr: "fontSize"
|
|
3475
|
+
})
|
|
3476
|
+
),
|
|
3477
|
+
isActive: () => {
|
|
3478
|
+
var _a;
|
|
3479
|
+
return ((_a = this.stylePanel) == null ? void 0 : _a.openKey) === "fontSize";
|
|
3480
|
+
},
|
|
3481
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
if (textStyleReady("fontFamily")) {
|
|
3485
|
+
items.set("fontFamily", {
|
|
3486
|
+
id: "fontFamily",
|
|
3487
|
+
icon: icons.fontFamily,
|
|
3488
|
+
popup: true,
|
|
3489
|
+
command: (_editor, button) => this.styleOptionPanel.toggle(
|
|
3490
|
+
button,
|
|
3491
|
+
this.styleSpec("fontFamily", {
|
|
3492
|
+
shape: "list",
|
|
3493
|
+
options: FONT_FAMILIES,
|
|
3494
|
+
swatch: false,
|
|
3495
|
+
attr: "fontFamily"
|
|
3496
|
+
})
|
|
3497
|
+
),
|
|
3498
|
+
isActive: () => {
|
|
3499
|
+
var _a;
|
|
3500
|
+
return ((_a = this.stylePanel) == null ? void 0 : _a.openKey) === "fontFamily";
|
|
3501
|
+
},
|
|
3502
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
if (hasMark(this.editor, "subscript")) {
|
|
3506
|
+
items.set("subscript", {
|
|
3507
|
+
id: "subscript",
|
|
3508
|
+
icon: icons.subscript,
|
|
3509
|
+
command: (editor) => editor.chain().focus().toggleSubscript().run(),
|
|
3510
|
+
isActive: (editor) => editor.isActive("subscript"),
|
|
3511
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
if (hasMark(this.editor, "superscript")) {
|
|
3515
|
+
items.set("superscript", {
|
|
3516
|
+
id: "superscript",
|
|
3517
|
+
icon: icons.superscript,
|
|
3518
|
+
command: (editor) => editor.chain().focus().toggleSuperscript().run(),
|
|
3519
|
+
isActive: (editor) => editor.isActive("superscript"),
|
|
3520
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
items.set("clearFormat", {
|
|
3524
|
+
id: "clearFormat",
|
|
3525
|
+
icon: icons.clearFormat,
|
|
3526
|
+
command: (editor) => editor.chain().focus().unsetAllMarks().clearNodes().run(),
|
|
3527
|
+
isActive: () => false,
|
|
3528
|
+
isDisabled: (editor) => !editor.isEditable
|
|
3529
|
+
});
|
|
2321
3530
|
items.set("delete", {
|
|
2322
3531
|
id: "delete",
|
|
2323
3532
|
icon: icons.trash,
|
|
2324
|
-
tooltip: "删除",
|
|
2325
3533
|
command: (editor) => editor.chain().deleteSelection().run(),
|
|
2326
3534
|
isActive: () => false,
|
|
2327
3535
|
isDisabled: (editor) => !isNodeSelection(editor.state.selection)
|
|
@@ -2329,7 +3537,6 @@ class ToolbarManager {
|
|
|
2329
3537
|
items.set("undo", {
|
|
2330
3538
|
id: "undo",
|
|
2331
3539
|
icon: icons.undo,
|
|
2332
|
-
tooltip: "撤销",
|
|
2333
3540
|
command: (editor) => editor.chain().undo().run(),
|
|
2334
3541
|
isActive: () => false,
|
|
2335
3542
|
isDisabled: (editor) => !editor.can().undo()
|
|
@@ -2337,39 +3544,23 @@ class ToolbarManager {
|
|
|
2337
3544
|
items.set("redo", {
|
|
2338
3545
|
id: "redo",
|
|
2339
3546
|
icon: icons.redo,
|
|
2340
|
-
tooltip: "重做",
|
|
2341
3547
|
command: (editor) => editor.chain().redo().run(),
|
|
2342
3548
|
isActive: () => false,
|
|
2343
3549
|
isDisabled: (editor) => !editor.can().redo()
|
|
2344
3550
|
});
|
|
2345
|
-
|
|
2346
|
-
id
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
id: "heading2",
|
|
2355
|
-
icon: icons.heading2,
|
|
2356
|
-
tooltip: "标题 2",
|
|
2357
|
-
command: (editor) => editor.chain().toggleHeading({ level: 2 }).run(),
|
|
2358
|
-
isActive: (editor) => editor.isActive("heading", { level: 2 }),
|
|
2359
|
-
isDisabled: (editor) => !editor.can().toggleHeading({ level: 2 })
|
|
2360
|
-
});
|
|
2361
|
-
items.set("heading3", {
|
|
2362
|
-
id: "heading3",
|
|
2363
|
-
icon: icons.heading3,
|
|
2364
|
-
tooltip: "标题 3",
|
|
2365
|
-
command: (editor) => editor.chain().toggleHeading({ level: 3 }).run(),
|
|
2366
|
-
isActive: (editor) => editor.isActive("heading", { level: 3 }),
|
|
2367
|
-
isDisabled: (editor) => !editor.can().toggleHeading({ level: 3 })
|
|
3551
|
+
[1, 2, 3, 4, 5, 6].forEach((level) => {
|
|
3552
|
+
const id = `heading${level}`;
|
|
3553
|
+
items.set(id, {
|
|
3554
|
+
id,
|
|
3555
|
+
icon: level <= 3 ? icons[id] : icons.heading,
|
|
3556
|
+
command: (editor) => editor.chain().toggleHeading({ level }).run(),
|
|
3557
|
+
isActive: (editor) => editor.isActive("heading", { level }),
|
|
3558
|
+
isDisabled: (editor) => !editor.can().toggleHeading({ level })
|
|
3559
|
+
});
|
|
2368
3560
|
});
|
|
2369
3561
|
items.set("paragraph", {
|
|
2370
3562
|
id: "paragraph",
|
|
2371
3563
|
icon: icons.paragraph,
|
|
2372
|
-
tooltip: "正文",
|
|
2373
3564
|
command: (editor) => editor.chain().setParagraph().run(),
|
|
2374
3565
|
isActive: (editor) => editor.isActive("paragraph"),
|
|
2375
3566
|
isDisabled: (editor) => !editor.can().setParagraph()
|
|
@@ -2377,7 +3568,6 @@ class ToolbarManager {
|
|
|
2377
3568
|
items.set("bold", {
|
|
2378
3569
|
id: "bold",
|
|
2379
3570
|
icon: icons.bold,
|
|
2380
|
-
tooltip: "加粗",
|
|
2381
3571
|
command: (editor) => editor.chain().toggleBold().run(),
|
|
2382
3572
|
isActive: (editor) => editor.isActive("bold"),
|
|
2383
3573
|
isDisabled: (editor) => !editor.can().toggleBold()
|
|
@@ -2385,7 +3575,6 @@ class ToolbarManager {
|
|
|
2385
3575
|
items.set("italic", {
|
|
2386
3576
|
id: "italic",
|
|
2387
3577
|
icon: icons.italic,
|
|
2388
|
-
tooltip: "斜体",
|
|
2389
3578
|
command: (editor) => editor.chain().toggleItalic().run(),
|
|
2390
3579
|
isActive: (editor) => editor.isActive("italic"),
|
|
2391
3580
|
isDisabled: (editor) => !editor.can().toggleItalic()
|
|
@@ -2393,7 +3582,6 @@ class ToolbarManager {
|
|
|
2393
3582
|
items.set("underline", {
|
|
2394
3583
|
id: "underline",
|
|
2395
3584
|
icon: icons.underline,
|
|
2396
|
-
tooltip: "下划线",
|
|
2397
3585
|
command: (editor) => editor.chain().toggleUnderline().run(),
|
|
2398
3586
|
isActive: (editor) => editor.isActive("underline"),
|
|
2399
3587
|
isDisabled: (editor) => !editor.can().toggleUnderline()
|
|
@@ -2401,7 +3589,6 @@ class ToolbarManager {
|
|
|
2401
3589
|
items.set("strike", {
|
|
2402
3590
|
id: "strike",
|
|
2403
3591
|
icon: icons.strikethrough,
|
|
2404
|
-
tooltip: "删除线",
|
|
2405
3592
|
command: (editor) => editor.chain().toggleStrike().run(),
|
|
2406
3593
|
isActive: (editor) => editor.isActive("strike"),
|
|
2407
3594
|
isDisabled: (editor) => !editor.can().toggleStrike()
|
|
@@ -2409,7 +3596,6 @@ class ToolbarManager {
|
|
|
2409
3596
|
items.set("code", {
|
|
2410
3597
|
id: "code",
|
|
2411
3598
|
icon: icons.code,
|
|
2412
|
-
tooltip: "行内代码",
|
|
2413
3599
|
command: (editor) => editor.chain().toggleCode().run(),
|
|
2414
3600
|
isActive: (editor) => editor.isActive("code"),
|
|
2415
3601
|
isDisabled: (editor) => !editor.can().toggleCode()
|
|
@@ -2417,7 +3603,6 @@ class ToolbarManager {
|
|
|
2417
3603
|
items.set("bulletList", {
|
|
2418
3604
|
id: "bulletList",
|
|
2419
3605
|
icon: icons.list,
|
|
2420
|
-
tooltip: "无序列表",
|
|
2421
3606
|
command: (editor) => editor.chain().toggleBulletList().run(),
|
|
2422
3607
|
isActive: (editor) => editor.isActive("bulletList"),
|
|
2423
3608
|
isDisabled: (editor) => !editor.can().toggleBulletList()
|
|
@@ -2425,31 +3610,43 @@ class ToolbarManager {
|
|
|
2425
3610
|
items.set("orderedList", {
|
|
2426
3611
|
id: "orderedList",
|
|
2427
3612
|
icon: icons.listOrdered,
|
|
2428
|
-
tooltip: "有序列表",
|
|
2429
3613
|
command: (editor) => editor.chain().toggleOrderedList().run(),
|
|
2430
3614
|
isActive: (editor) => editor.isActive("orderedList"),
|
|
2431
3615
|
isDisabled: (editor) => !editor.can().toggleOrderedList()
|
|
2432
3616
|
});
|
|
3617
|
+
items.set("taskList", {
|
|
3618
|
+
id: "taskList",
|
|
3619
|
+
icon: icons.listChecks,
|
|
3620
|
+
command: (editor) => editor.chain().toggleTaskList().run(),
|
|
3621
|
+
isActive: (editor) => editor.isActive("taskList"),
|
|
3622
|
+
isDisabled: (editor) => !editor.can().toggleTaskList()
|
|
3623
|
+
});
|
|
2433
3624
|
items.set("blockquote", {
|
|
2434
3625
|
id: "blockquote",
|
|
2435
3626
|
icon: icons.quote,
|
|
2436
|
-
tooltip: "引用",
|
|
2437
3627
|
command: (editor) => editor.chain().toggleBlockquote().run(),
|
|
2438
3628
|
isActive: (editor) => editor.isActive("blockquote"),
|
|
2439
3629
|
isDisabled: (editor) => !editor.can().toggleBlockquote()
|
|
2440
3630
|
});
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
3631
|
+
if (this.editor.schema.nodes.codeBlock) {
|
|
3632
|
+
items.set("codeBlock", {
|
|
3633
|
+
id: "codeBlock",
|
|
3634
|
+
icon: icons.codeBlock,
|
|
3635
|
+
command: (editor) => editor.chain().toggleCodeBlock().run(),
|
|
3636
|
+
isActive: (editor) => editor.isActive("codeBlock"),
|
|
3637
|
+
isDisabled: (editor) => !editor.can().toggleCodeBlock()
|
|
3638
|
+
});
|
|
3639
|
+
}
|
|
3640
|
+
items.set("horizontalRule", {
|
|
3641
|
+
id: "horizontalRule",
|
|
3642
|
+
icon: icons.horizontalRule,
|
|
3643
|
+
command: (editor) => editor.chain().setHorizontalRule().run(),
|
|
3644
|
+
isActive: () => false,
|
|
3645
|
+
isDisabled: (editor) => !editor.can().setHorizontalRule()
|
|
2448
3646
|
});
|
|
2449
3647
|
items.set("alignLeft", {
|
|
2450
3648
|
id: "alignLeft",
|
|
2451
3649
|
icon: icons.alignLeft,
|
|
2452
|
-
tooltip: "左对齐",
|
|
2453
3650
|
command: (editor) => editor.chain().setTextAlign("left").run(),
|
|
2454
3651
|
isActive: (editor) => editor.isActive({ textAlign: "left" }),
|
|
2455
3652
|
isDisabled: (editor) => !editor.can().setTextAlign("left")
|
|
@@ -2457,7 +3654,6 @@ class ToolbarManager {
|
|
|
2457
3654
|
items.set("alignCenter", {
|
|
2458
3655
|
id: "alignCenter",
|
|
2459
3656
|
icon: icons.alignCenter,
|
|
2460
|
-
tooltip: "居中",
|
|
2461
3657
|
command: (editor) => editor.chain().setTextAlign("center").run(),
|
|
2462
3658
|
isActive: (editor) => editor.isActive({ textAlign: "center" }),
|
|
2463
3659
|
isDisabled: (editor) => !editor.can().setTextAlign("center")
|
|
@@ -2465,27 +3661,58 @@ class ToolbarManager {
|
|
|
2465
3661
|
items.set("alignRight", {
|
|
2466
3662
|
id: "alignRight",
|
|
2467
3663
|
icon: icons.alignRight,
|
|
2468
|
-
tooltip: "右对齐",
|
|
2469
3664
|
command: (editor) => editor.chain().setTextAlign("right").run(),
|
|
2470
3665
|
isActive: (editor) => editor.isActive({ textAlign: "right" }),
|
|
2471
3666
|
isDisabled: (editor) => !editor.can().setTextAlign("right")
|
|
2472
3667
|
});
|
|
3668
|
+
items.set("alignJustify", {
|
|
3669
|
+
id: "alignJustify",
|
|
3670
|
+
icon: icons.alignJustify,
|
|
3671
|
+
command: (editor) => editor.chain().setTextAlign("justify").run(),
|
|
3672
|
+
isActive: (editor) => editor.isActive({ textAlign: "justify" }),
|
|
3673
|
+
isDisabled: (editor) => !editor.can().setTextAlign("justify")
|
|
3674
|
+
});
|
|
3675
|
+
items.set(
|
|
3676
|
+
"heading",
|
|
3677
|
+
this.menuItem("heading", icons.heading, [
|
|
3678
|
+
"heading1",
|
|
3679
|
+
"heading2",
|
|
3680
|
+
"heading3",
|
|
3681
|
+
"heading4",
|
|
3682
|
+
"heading5",
|
|
3683
|
+
"heading6",
|
|
3684
|
+
"paragraph"
|
|
3685
|
+
])
|
|
3686
|
+
);
|
|
3687
|
+
items.set(
|
|
3688
|
+
"align",
|
|
3689
|
+
this.menuItem("align", icons.align, [
|
|
3690
|
+
"alignLeft",
|
|
3691
|
+
"alignCenter",
|
|
3692
|
+
"alignRight",
|
|
3693
|
+
"alignJustify"
|
|
3694
|
+
])
|
|
3695
|
+
);
|
|
2473
3696
|
return items;
|
|
2474
3697
|
}
|
|
2475
3698
|
/**
|
|
2476
3699
|
* 绑定编辑器事件,更新按钮状态
|
|
2477
3700
|
*/
|
|
2478
3701
|
bindEditorEvents() {
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
this.
|
|
2483
|
-
this.
|
|
2484
|
-
|
|
3702
|
+
const refresh = () => this.updateButtonStates();
|
|
3703
|
+
this.editor.on("transaction", refresh);
|
|
3704
|
+
this.editor.on("selectionUpdate", refresh);
|
|
3705
|
+
this.unsubscribeEditor = () => {
|
|
3706
|
+
this.editor.off("transaction", refresh);
|
|
3707
|
+
this.editor.off("selectionUpdate", refresh);
|
|
3708
|
+
};
|
|
2485
3709
|
this.updateButtonStates();
|
|
2486
3710
|
}
|
|
2487
3711
|
/**
|
|
2488
3712
|
* 更新所有按钮状态
|
|
3713
|
+
*
|
|
3714
|
+
* 公开是因为浮层开合不改文档,等不到 transaction:挂在门面上的浮层(链接)
|
|
3715
|
+
* 由门面在开合时叫一次,挂在这里的浮层则在 onOpenChange 里叫
|
|
2489
3716
|
*/
|
|
2490
3717
|
updateButtonStates() {
|
|
2491
3718
|
this.buttons.forEach((buttons, itemId) => {
|
|
@@ -2497,26 +3724,528 @@ class ToolbarManager {
|
|
|
2497
3724
|
button.classList.toggle("active", active);
|
|
2498
3725
|
if (itemDef.popup) button.setAttribute("aria-expanded", String(active));
|
|
2499
3726
|
button.disabled = disabled;
|
|
3727
|
+
if (itemDef.popup === "menu") this.refreshMenuTrigger(itemDef, button);
|
|
2500
3728
|
});
|
|
2501
3729
|
});
|
|
2502
3730
|
}
|
|
3731
|
+
/**
|
|
3732
|
+
* 一项当前该显示的文字。下拉触发器跟着命中的子项走(现在是三级标题就写「标题 3」),
|
|
3733
|
+
* 宿主显式写的 tooltip 优先级最高。
|
|
3734
|
+
* 抽出来是因为图标与提示必须由同一份判定得出:只换其中一个就会出现
|
|
3735
|
+
* "按钮画着 H3、悬停写标题"这种一半新一半旧的界面
|
|
3736
|
+
*/
|
|
3737
|
+
itemTitle(itemId, def) {
|
|
3738
|
+
var _a;
|
|
3739
|
+
const custom = this.customTooltips.get(itemId);
|
|
3740
|
+
if (custom) return custom;
|
|
3741
|
+
let key = TOOLTIP_KEYS[itemId];
|
|
3742
|
+
if (def.popup === "menu") {
|
|
3743
|
+
const activeKey = TOOLTIP_KEYS[((_a = this.activeChild(def)) == null ? void 0 : _a.id) ?? ""];
|
|
3744
|
+
if (activeKey) key = activeKey;
|
|
3745
|
+
}
|
|
3746
|
+
const fallback = key ? zhText(key) : def.tooltip ?? "";
|
|
3747
|
+
return key ? this.tOr(key, fallback) : fallback;
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3750
|
+
* 下拉触发器的图标与提示跟着当前值走:现在是二级标题就显示 H2,
|
|
3751
|
+
* 一个都不匹配才退回通用图标。只有菜单项会走到这里,不是每次刷新对所有按钮都做一遍
|
|
3752
|
+
*/
|
|
3753
|
+
refreshMenuTrigger(def, button) {
|
|
3754
|
+
var _a;
|
|
3755
|
+
const base = button.dataset.menuIcon ?? def.icon;
|
|
3756
|
+
const next = `${((_a = this.activeChild(def)) == null ? void 0 : _a.icon) ?? base}${icons.chevronDown}`;
|
|
3757
|
+
if (button.innerHTML !== next) button.innerHTML = next;
|
|
3758
|
+
const title = this.itemTitle(def.id, def);
|
|
3759
|
+
if (button.title !== title) button.title = title;
|
|
3760
|
+
}
|
|
2503
3761
|
/**
|
|
2504
3762
|
* 销毁工具栏
|
|
2505
3763
|
*/
|
|
2506
3764
|
destroy() {
|
|
2507
|
-
var _a, _b, _c;
|
|
3765
|
+
var _a, _b, _c, _d, _e, _f;
|
|
2508
3766
|
(_a = this.unsubscribeLanguage) == null ? void 0 : _a.call(this);
|
|
2509
3767
|
this.unsubscribeLanguage = void 0;
|
|
2510
|
-
(_b = this.
|
|
3768
|
+
(_b = this.unsubscribeEditor) == null ? void 0 : _b.call(this);
|
|
3769
|
+
this.unsubscribeEditor = void 0;
|
|
3770
|
+
(_c = this.panel) == null ? void 0 : _c.destroy();
|
|
2511
3771
|
this.panel = null;
|
|
2512
|
-
(
|
|
3772
|
+
(_d = this.tablePanel) == null ? void 0 : _d.destroy();
|
|
2513
3773
|
this.tablePanel = null;
|
|
3774
|
+
(_e = this.stylePanel) == null ? void 0 : _e.destroy();
|
|
3775
|
+
this.stylePanel = null;
|
|
3776
|
+
(_f = this.menuPanel) == null ? void 0 : _f.destroy();
|
|
3777
|
+
this.menuPanel = null;
|
|
2514
3778
|
this.buttons.clear();
|
|
2515
3779
|
this.customTooltips.clear();
|
|
2516
3780
|
this.createdElements.forEach((el) => el.remove());
|
|
2517
3781
|
this.createdElements = [];
|
|
2518
3782
|
}
|
|
2519
3783
|
}
|
|
3784
|
+
function normalizeHref(value) {
|
|
3785
|
+
const href = value.trim();
|
|
3786
|
+
if (!href) return "";
|
|
3787
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || /^[/#.]/.test(href)) return href;
|
|
3788
|
+
return `https://${href}`;
|
|
3789
|
+
}
|
|
3790
|
+
class LinkPanel {
|
|
3791
|
+
constructor(options) {
|
|
3792
|
+
var _a;
|
|
3793
|
+
this.element = null;
|
|
3794
|
+
this.floating = null;
|
|
3795
|
+
this.detachDismiss = null;
|
|
3796
|
+
this.detachTheme = null;
|
|
3797
|
+
this.labelled = [];
|
|
3798
|
+
this.urlInput = null;
|
|
3799
|
+
this.targetInput = null;
|
|
3800
|
+
this.editing = false;
|
|
3801
|
+
this.onEditorClick = (event) => {
|
|
3802
|
+
if (this.openOnClick || !this.editor.isEditable) return;
|
|
3803
|
+
if (!this.editor.state.selection.empty) return;
|
|
3804
|
+
const target = event.target;
|
|
3805
|
+
if (!(target instanceof Element)) return;
|
|
3806
|
+
const anchor = target.closest("a");
|
|
3807
|
+
if (!anchor || !this.root.contains(anchor)) return;
|
|
3808
|
+
event.preventDefault();
|
|
3809
|
+
this.open(anchor);
|
|
3810
|
+
};
|
|
3811
|
+
this.editor = options.editor;
|
|
3812
|
+
this.root = options.root;
|
|
3813
|
+
this.i18n = options.i18n;
|
|
3814
|
+
this.openOnClick = options.openOnClick ?? false;
|
|
3815
|
+
this.defaultTarget = options.target ?? "_blank";
|
|
3816
|
+
this.onOpenChange = options.onOpenChange;
|
|
3817
|
+
this.unsubscribeLanguage = (_a = this.i18n) == null ? void 0 : _a.onLanguageChanged(() => this.applyLabels());
|
|
3818
|
+
this.root.addEventListener("click", this.onEditorClick);
|
|
3819
|
+
}
|
|
3820
|
+
get isOpen() {
|
|
3821
|
+
return this.element !== null;
|
|
3822
|
+
}
|
|
3823
|
+
toggle(anchor) {
|
|
3824
|
+
if (this.isOpen) {
|
|
3825
|
+
this.close();
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
this.open(anchor);
|
|
3829
|
+
}
|
|
3830
|
+
open(anchor) {
|
|
3831
|
+
var _a;
|
|
3832
|
+
this.close();
|
|
3833
|
+
this.editing = this.editor.isActive("link");
|
|
3834
|
+
this.build();
|
|
3835
|
+
const panel = this.element;
|
|
3836
|
+
if (!panel) return;
|
|
3837
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
3838
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
3839
|
+
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
3840
|
+
}
|
|
3841
|
+
close() {
|
|
3842
|
+
var _a, _b, _c, _d, _e;
|
|
3843
|
+
const wasOpen = this.element !== null;
|
|
3844
|
+
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
3845
|
+
this.detachDismiss = null;
|
|
3846
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
3847
|
+
this.detachTheme = null;
|
|
3848
|
+
(_c = this.floating) == null ? void 0 : _c.stop();
|
|
3849
|
+
this.floating = null;
|
|
3850
|
+
(_d = this.element) == null ? void 0 : _d.remove();
|
|
3851
|
+
this.element = null;
|
|
3852
|
+
this.urlInput = null;
|
|
3853
|
+
this.targetInput = null;
|
|
3854
|
+
this.labelled = [];
|
|
3855
|
+
this.editing = false;
|
|
3856
|
+
if (wasOpen) (_e = this.onOpenChange) == null ? void 0 : _e.call(this, false);
|
|
3857
|
+
}
|
|
3858
|
+
destroy() {
|
|
3859
|
+
var _a;
|
|
3860
|
+
this.root.removeEventListener("click", this.onEditorClick);
|
|
3861
|
+
this.close();
|
|
3862
|
+
(_a = this.unsubscribeLanguage) == null ? void 0 : _a.call(this);
|
|
3863
|
+
this.unsubscribeLanguage = void 0;
|
|
3864
|
+
}
|
|
3865
|
+
t(key, fallback) {
|
|
3866
|
+
return tOrFallback(this.i18n, key, fallback);
|
|
3867
|
+
}
|
|
3868
|
+
/** 登记一处随语言重画的文案:input 画到 placeholder,其他画成文字 */
|
|
3869
|
+
label(el, key, fallback) {
|
|
3870
|
+
this.labelled.push({ el, key, fallback });
|
|
3871
|
+
this.applyLabel({ el, key, fallback });
|
|
3872
|
+
}
|
|
3873
|
+
applyLabels() {
|
|
3874
|
+
this.labelled.forEach((entry) => this.applyLabel(entry));
|
|
3875
|
+
}
|
|
3876
|
+
applyLabel({ el, key, fallback }) {
|
|
3877
|
+
const text = this.t(key, fallback);
|
|
3878
|
+
if (el instanceof HTMLInputElement) el.placeholder = text;
|
|
3879
|
+
else el.textContent = text;
|
|
3880
|
+
}
|
|
3881
|
+
build() {
|
|
3882
|
+
const panel = document.createElement("div");
|
|
3883
|
+
panel.className = "atri-link-panel";
|
|
3884
|
+
panel.setAttribute("role", "dialog");
|
|
3885
|
+
panel.setAttribute("aria-label", this.t("link.panelLabel", "链接"));
|
|
3886
|
+
const url = document.createElement("input");
|
|
3887
|
+
url.type = "url";
|
|
3888
|
+
url.className = "atri-link-panel-url";
|
|
3889
|
+
this.label(url, "link.urlPlaceholder", "链接地址(https://…)");
|
|
3890
|
+
url.value = this.editing ? String(this.editor.getAttributes("link").href ?? "") : "";
|
|
3891
|
+
url.addEventListener("keydown", (event) => {
|
|
3892
|
+
if (event.key === "Enter") {
|
|
3893
|
+
event.preventDefault();
|
|
3894
|
+
this.apply();
|
|
3895
|
+
}
|
|
3896
|
+
});
|
|
3897
|
+
panel.appendChild(url);
|
|
3898
|
+
this.urlInput = url;
|
|
3899
|
+
const targetLabel = document.createElement("label");
|
|
3900
|
+
targetLabel.className = "atri-link-panel-target";
|
|
3901
|
+
const target = document.createElement("input");
|
|
3902
|
+
target.type = "checkbox";
|
|
3903
|
+
target.checked = this.editing ? this.editor.getAttributes("link").target !== "_self" : this.defaultTarget === "_blank";
|
|
3904
|
+
const targetText = document.createElement("span");
|
|
3905
|
+
this.label(targetText, "link.newWindow", "在新窗口打开");
|
|
3906
|
+
targetLabel.append(target, targetText);
|
|
3907
|
+
panel.appendChild(targetLabel);
|
|
3908
|
+
this.targetInput = target;
|
|
3909
|
+
const actions = document.createElement("div");
|
|
3910
|
+
actions.className = "atri-link-panel-actions";
|
|
3911
|
+
if (this.editing) {
|
|
3912
|
+
const remove = document.createElement("button");
|
|
3913
|
+
remove.type = "button";
|
|
3914
|
+
remove.className = "atri-link-panel-remove";
|
|
3915
|
+
this.label(remove, "link.remove", "移除链接");
|
|
3916
|
+
remove.addEventListener("click", () => this.remove());
|
|
3917
|
+
actions.appendChild(remove);
|
|
3918
|
+
}
|
|
3919
|
+
const apply = document.createElement("button");
|
|
3920
|
+
apply.type = "button";
|
|
3921
|
+
apply.className = "atri-link-panel-apply";
|
|
3922
|
+
this.label(apply, "link.apply", "确定");
|
|
3923
|
+
apply.addEventListener("click", () => this.apply());
|
|
3924
|
+
actions.appendChild(apply);
|
|
3925
|
+
panel.appendChild(actions);
|
|
3926
|
+
document.body.appendChild(panel);
|
|
3927
|
+
this.element = panel;
|
|
3928
|
+
this.detachTheme = followThemeClasses(this.root, panel);
|
|
3929
|
+
url.focus();
|
|
3930
|
+
url.select();
|
|
3931
|
+
}
|
|
3932
|
+
/** 有链接就整条改(extendMarkRange 把范围摊到整段链接),没链接就看选区:套住它,或者把地址本身当链接文字插进去 */
|
|
3933
|
+
apply() {
|
|
3934
|
+
var _a, _b;
|
|
3935
|
+
const href = normalizeHref(((_a = this.urlInput) == null ? void 0 : _a.value) ?? "");
|
|
3936
|
+
if (!href) return;
|
|
3937
|
+
const target = ((_b = this.targetInput) == null ? void 0 : _b.checked) ? "_blank" : "_self";
|
|
3938
|
+
const { editor } = this;
|
|
3939
|
+
const chain = editor.chain().focus();
|
|
3940
|
+
if (this.editing) {
|
|
3941
|
+
chain.extendMarkRange("link").setLink({ href, target }).run();
|
|
3942
|
+
} else if (editor.state.selection.empty) {
|
|
3943
|
+
chain.insertContent({
|
|
3944
|
+
type: "text",
|
|
3945
|
+
text: href,
|
|
3946
|
+
marks: [{ type: "link", attrs: { href, target } }]
|
|
3947
|
+
}).run();
|
|
3948
|
+
} else {
|
|
3949
|
+
chain.setLink({ href, target }).run();
|
|
3950
|
+
}
|
|
3951
|
+
this.close();
|
|
3952
|
+
}
|
|
3953
|
+
remove() {
|
|
3954
|
+
this.editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
3955
|
+
this.close();
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
function cellPos(state) {
|
|
3959
|
+
const { $from } = state.selection;
|
|
3960
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
3961
|
+
const name = $from.node(depth).type.name;
|
|
3962
|
+
if (name === "tableCell" || name === "tableHeader") return $from.before(depth);
|
|
3963
|
+
}
|
|
3964
|
+
return null;
|
|
3965
|
+
}
|
|
3966
|
+
function cellAlignmentActive(align) {
|
|
3967
|
+
return (editor) => editor.isActive("tableCell", { align }) || editor.isActive("tableHeader", { align });
|
|
3968
|
+
}
|
|
3969
|
+
function buildItems() {
|
|
3970
|
+
const items = [
|
|
3971
|
+
{
|
|
3972
|
+
id: "addRowBefore",
|
|
3973
|
+
icon: icons.tableRowBefore,
|
|
3974
|
+
labelKey: "table.rowBefore",
|
|
3975
|
+
group: 0,
|
|
3976
|
+
command: (editor) => editor.chain().focus().addRowBefore().run()
|
|
3977
|
+
},
|
|
3978
|
+
{
|
|
3979
|
+
id: "addRowAfter",
|
|
3980
|
+
icon: icons.tableRowAfter,
|
|
3981
|
+
labelKey: "table.rowAfter",
|
|
3982
|
+
group: 0,
|
|
3983
|
+
command: (editor) => editor.chain().focus().addRowAfter().run()
|
|
3984
|
+
},
|
|
3985
|
+
{
|
|
3986
|
+
id: "deleteRow",
|
|
3987
|
+
icon: icons.tableRowDelete,
|
|
3988
|
+
labelKey: "table.deleteRow",
|
|
3989
|
+
group: 0,
|
|
3990
|
+
command: (editor) => editor.chain().focus().deleteRow().run()
|
|
3991
|
+
},
|
|
3992
|
+
{
|
|
3993
|
+
id: "addColumnBefore",
|
|
3994
|
+
icon: icons.tableColumnBefore,
|
|
3995
|
+
labelKey: "table.columnBefore",
|
|
3996
|
+
group: 1,
|
|
3997
|
+
command: (editor) => editor.chain().focus().addColumnBefore().run()
|
|
3998
|
+
},
|
|
3999
|
+
{
|
|
4000
|
+
id: "addColumnAfter",
|
|
4001
|
+
icon: icons.tableColumnAfter,
|
|
4002
|
+
labelKey: "table.columnAfter",
|
|
4003
|
+
group: 1,
|
|
4004
|
+
command: (editor) => editor.chain().focus().addColumnAfter().run()
|
|
4005
|
+
},
|
|
4006
|
+
{
|
|
4007
|
+
id: "deleteColumn",
|
|
4008
|
+
icon: icons.tableColumnDelete,
|
|
4009
|
+
labelKey: "table.deleteColumn",
|
|
4010
|
+
group: 1,
|
|
4011
|
+
command: (editor) => editor.chain().focus().deleteColumn().run()
|
|
4012
|
+
},
|
|
4013
|
+
{
|
|
4014
|
+
// 合并要有两个以上单元格被框住,单选一个时按不动——禁用而不是隐藏,位置别跳
|
|
4015
|
+
id: "mergeCells",
|
|
4016
|
+
icon: icons.tableMergeCells,
|
|
4017
|
+
labelKey: "table.mergeCells",
|
|
4018
|
+
group: 2,
|
|
4019
|
+
command: (editor) => editor.chain().focus().mergeCells().run(),
|
|
4020
|
+
isDisabled: (editor) => !editor.can().mergeCells()
|
|
4021
|
+
},
|
|
4022
|
+
{
|
|
4023
|
+
id: "splitCell",
|
|
4024
|
+
icon: icons.tableSplitCell,
|
|
4025
|
+
labelKey: "table.splitCell",
|
|
4026
|
+
group: 2,
|
|
4027
|
+
command: (editor) => editor.chain().focus().splitCell().run(),
|
|
4028
|
+
isDisabled: (editor) => !editor.can().splitCell()
|
|
4029
|
+
},
|
|
4030
|
+
{
|
|
4031
|
+
id: "toggleHeaderRow",
|
|
4032
|
+
icon: icons.tableHeaderRow,
|
|
4033
|
+
labelKey: "table.toggleHeaderRow",
|
|
4034
|
+
group: 2,
|
|
4035
|
+
command: (editor) => editor.chain().focus().toggleHeaderRow().run(),
|
|
4036
|
+
isActive: (editor) => editor.isActive("tableHeader")
|
|
4037
|
+
}
|
|
4038
|
+
];
|
|
4039
|
+
const alignments = [
|
|
4040
|
+
["left", "table.alignLeft"],
|
|
4041
|
+
["center", "table.alignCenter"],
|
|
4042
|
+
["right", "table.alignRight"]
|
|
4043
|
+
];
|
|
4044
|
+
alignments.forEach(([align, labelKey]) => {
|
|
4045
|
+
const suffix = `${align[0].toUpperCase()}${align.slice(1)}`;
|
|
4046
|
+
items.push({
|
|
4047
|
+
id: `cellAlign${suffix}`,
|
|
4048
|
+
icon: icons[`align${suffix}`],
|
|
4049
|
+
labelKey,
|
|
4050
|
+
group: 3,
|
|
4051
|
+
command: (editor) => editor.chain().focus().setCellAttribute("align", align).run(),
|
|
4052
|
+
isActive: cellAlignmentActive(align)
|
|
4053
|
+
});
|
|
4054
|
+
});
|
|
4055
|
+
items.push({
|
|
4056
|
+
id: "deleteTable",
|
|
4057
|
+
icon: icons.trash,
|
|
4058
|
+
labelKey: "table.deleteTable",
|
|
4059
|
+
group: 4,
|
|
4060
|
+
command: (editor) => editor.chain().focus().deleteTable().run()
|
|
4061
|
+
});
|
|
4062
|
+
return items;
|
|
4063
|
+
}
|
|
4064
|
+
class TableMenu {
|
|
4065
|
+
constructor(options) {
|
|
4066
|
+
var _a;
|
|
4067
|
+
this.element = null;
|
|
4068
|
+
this.dismissed = false;
|
|
4069
|
+
this.sync = () => {
|
|
4070
|
+
const inTable = cellPos(this.editor.state) !== null;
|
|
4071
|
+
if (!inTable) {
|
|
4072
|
+
this.dismissed = false;
|
|
4073
|
+
this.hide();
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
if (this.dismissed || !this.editor.isEditable) {
|
|
4077
|
+
this.hide();
|
|
4078
|
+
return;
|
|
4079
|
+
}
|
|
4080
|
+
this.show();
|
|
4081
|
+
};
|
|
4082
|
+
this.onBlur = () => {
|
|
4083
|
+
this.hide();
|
|
4084
|
+
};
|
|
4085
|
+
this.onScroll = () => {
|
|
4086
|
+
if (this.element) void this.position();
|
|
4087
|
+
};
|
|
4088
|
+
this.dismiss = () => {
|
|
4089
|
+
this.dismissed = true;
|
|
4090
|
+
this.hide();
|
|
4091
|
+
};
|
|
4092
|
+
this.editor = options.editor;
|
|
4093
|
+
this.root = options.root;
|
|
4094
|
+
this.i18n = options.i18n;
|
|
4095
|
+
this.items = buildItems();
|
|
4096
|
+
this.editor.on("update", this.sync);
|
|
4097
|
+
this.editor.on("selectionUpdate", this.sync);
|
|
4098
|
+
this.editor.on("focus", this.sync);
|
|
4099
|
+
this.editor.on("blur", this.onBlur);
|
|
4100
|
+
document.addEventListener("scroll", this.onScroll, true);
|
|
4101
|
+
this.unsubscribeLanguage = (_a = this.i18n) == null ? void 0 : _a.onLanguageChanged(() => this.applyLabels());
|
|
4102
|
+
this.sync();
|
|
4103
|
+
}
|
|
4104
|
+
get isOpen() {
|
|
4105
|
+
return this.element !== null;
|
|
4106
|
+
}
|
|
4107
|
+
destroy() {
|
|
4108
|
+
var _a;
|
|
4109
|
+
this.editor.off("update", this.sync);
|
|
4110
|
+
this.editor.off("selectionUpdate", this.sync);
|
|
4111
|
+
this.editor.off("focus", this.sync);
|
|
4112
|
+
this.editor.off("blur", this.onBlur);
|
|
4113
|
+
document.removeEventListener("scroll", this.onScroll, true);
|
|
4114
|
+
(_a = this.unsubscribeLanguage) == null ? void 0 : _a.call(this);
|
|
4115
|
+
this.unsubscribeLanguage = void 0;
|
|
4116
|
+
this.hide();
|
|
4117
|
+
}
|
|
4118
|
+
t(key) {
|
|
4119
|
+
return tOrFallback(this.i18n, key, zhText(key));
|
|
4120
|
+
}
|
|
4121
|
+
show() {
|
|
4122
|
+
if (!this.element) this.build();
|
|
4123
|
+
this.updateStates();
|
|
4124
|
+
void this.position();
|
|
4125
|
+
}
|
|
4126
|
+
hide() {
|
|
4127
|
+
var _a, _b;
|
|
4128
|
+
(_a = this.detachEscape) == null ? void 0 : _a.call(this);
|
|
4129
|
+
this.detachEscape = void 0;
|
|
4130
|
+
(_b = this.detachTheme) == null ? void 0 : _b.call(this);
|
|
4131
|
+
this.detachTheme = void 0;
|
|
4132
|
+
if (!this.element) return;
|
|
4133
|
+
this.element.remove();
|
|
4134
|
+
this.element = null;
|
|
4135
|
+
}
|
|
4136
|
+
/**
|
|
4137
|
+
* Escape 收起菜单。这里不用 floating-panel 的 listenPanelDismiss:那一套还管外点关闭,
|
|
4138
|
+
* 而点外面多半仍落在同一个单元格里,关了立刻被下一次状态更新重新浮出,只会闪
|
|
4139
|
+
*/
|
|
4140
|
+
listenEscape() {
|
|
4141
|
+
const onKeyDown = (event) => {
|
|
4142
|
+
if (event.key !== "Escape") return;
|
|
4143
|
+
event.preventDefault();
|
|
4144
|
+
event.stopPropagation();
|
|
4145
|
+
this.dismiss();
|
|
4146
|
+
};
|
|
4147
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
4148
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
4149
|
+
}
|
|
4150
|
+
build() {
|
|
4151
|
+
const panel = document.createElement("div");
|
|
4152
|
+
panel.className = "atri-table-menu";
|
|
4153
|
+
panel.setAttribute("role", "toolbar");
|
|
4154
|
+
panel.setAttribute("aria-label", this.t("table.menuLabel"));
|
|
4155
|
+
panel.addEventListener("mousedown", (event) => event.preventDefault());
|
|
4156
|
+
let lastGroup = null;
|
|
4157
|
+
this.items.forEach((item) => {
|
|
4158
|
+
if (lastGroup !== null && item.group !== lastGroup) {
|
|
4159
|
+
const separator = document.createElement("div");
|
|
4160
|
+
separator.className = "atri-table-menu-separator";
|
|
4161
|
+
panel.appendChild(separator);
|
|
4162
|
+
}
|
|
4163
|
+
lastGroup = item.group;
|
|
4164
|
+
const button = document.createElement("button");
|
|
4165
|
+
button.type = "button";
|
|
4166
|
+
button.className = "atri-editor-toolbar-btn";
|
|
4167
|
+
button.setAttribute("data-table-item", item.id);
|
|
4168
|
+
button.title = this.t(item.labelKey);
|
|
4169
|
+
button.innerHTML = item.icon;
|
|
4170
|
+
button.addEventListener("click", (event) => {
|
|
4171
|
+
event.preventDefault();
|
|
4172
|
+
event.stopPropagation();
|
|
4173
|
+
item.command(this.editor);
|
|
4174
|
+
});
|
|
4175
|
+
panel.appendChild(button);
|
|
4176
|
+
});
|
|
4177
|
+
document.body.appendChild(panel);
|
|
4178
|
+
this.element = panel;
|
|
4179
|
+
this.detachTheme = followThemeClasses(this.root, panel);
|
|
4180
|
+
this.detachEscape = this.listenEscape();
|
|
4181
|
+
}
|
|
4182
|
+
applyLabels() {
|
|
4183
|
+
const panel = this.element;
|
|
4184
|
+
if (!panel) return;
|
|
4185
|
+
panel.setAttribute("aria-label", this.t("table.menuLabel"));
|
|
4186
|
+
this.items.forEach((item) => {
|
|
4187
|
+
const button = panel.querySelector(`[data-table-item="${item.id}"]`);
|
|
4188
|
+
if (button) button.title = this.t(item.labelKey);
|
|
4189
|
+
});
|
|
4190
|
+
}
|
|
4191
|
+
updateStates() {
|
|
4192
|
+
const panel = this.element;
|
|
4193
|
+
if (!panel) return;
|
|
4194
|
+
this.items.forEach((item) => {
|
|
4195
|
+
var _a, _b;
|
|
4196
|
+
const button = panel.querySelector(`[data-table-item="${item.id}"]`);
|
|
4197
|
+
if (!button) return;
|
|
4198
|
+
button.disabled = ((_a = item.isDisabled) == null ? void 0 : _a.call(item, this.editor)) ?? false;
|
|
4199
|
+
button.classList.toggle("active", ((_b = item.isActive) == null ? void 0 : _b.call(item, this.editor)) ?? false);
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
/**
|
|
4203
|
+
* 当前单元格的虚拟锚点;量不到返回 null,调用方保持面板可见但不改坐标
|
|
4204
|
+
*
|
|
4205
|
+
* 优先锚在单元格盒子上(菜单跟着格子走,比跟着光标稳),拿不到盒子才退回光标坐标
|
|
4206
|
+
*/
|
|
4207
|
+
anchor() {
|
|
4208
|
+
const { view, state } = this.editor;
|
|
4209
|
+
const pos = cellPos(state);
|
|
4210
|
+
if (pos !== null) {
|
|
4211
|
+
try {
|
|
4212
|
+
const dom = view.nodeDOM(pos);
|
|
4213
|
+
if (dom instanceof HTMLElement) {
|
|
4214
|
+
const rect = dom.getBoundingClientRect();
|
|
4215
|
+
if (rect.width || rect.height) return { getBoundingClientRect: () => rect };
|
|
4216
|
+
}
|
|
4217
|
+
} catch {
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
try {
|
|
4221
|
+
const coords = view.coordsAtPos(state.selection.from);
|
|
4222
|
+
return {
|
|
4223
|
+
getBoundingClientRect: () => new DOMRect(coords.left, coords.top, 0, coords.bottom - coords.top)
|
|
4224
|
+
};
|
|
4225
|
+
} catch {
|
|
4226
|
+
return null;
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
async position() {
|
|
4230
|
+
const panel = this.element;
|
|
4231
|
+
if (!panel) return;
|
|
4232
|
+
const reference = this.anchor();
|
|
4233
|
+
if (!reference) return;
|
|
4234
|
+
let x;
|
|
4235
|
+
let y;
|
|
4236
|
+
try {
|
|
4237
|
+
({ x, y } = await computePosition(reference, panel, {
|
|
4238
|
+
// 贴在单元格上沿:菜单跟着当前格走,长表格里也不会跑到视口外
|
|
4239
|
+
placement: "top-start",
|
|
4240
|
+
middleware: panelMiddleware()
|
|
4241
|
+
}));
|
|
4242
|
+
} catch {
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
if (this.element !== panel) return;
|
|
4246
|
+
placePanel(panel, x, y);
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
2520
4249
|
class MediaStatusStrip {
|
|
2521
4250
|
constructor(runtime, container, i18n) {
|
|
2522
4251
|
const t = (key, fallback, options) => (i18n == null ? void 0 : i18n.tOr(key, fallback, options)) ?? fallback;
|
|
@@ -2536,7 +4265,11 @@ class MediaStatusStrip {
|
|
|
2536
4265
|
this.retry.type = "button";
|
|
2537
4266
|
this.retry.className = "atri-media-status-retry";
|
|
2538
4267
|
this.retry.hidden = true;
|
|
2539
|
-
this.retry.addEventListener("click", () =>
|
|
4268
|
+
this.retry.addEventListener("click", () => {
|
|
4269
|
+
runtime.retryFailed().catch((error) => {
|
|
4270
|
+
console.error("[Atri Editor] Retrying the failed uploads did not work:", error);
|
|
4271
|
+
});
|
|
4272
|
+
});
|
|
2540
4273
|
this.element.append(this.text, this.retry);
|
|
2541
4274
|
container.appendChild(this.element);
|
|
2542
4275
|
this.unsubscribe = runtime.subscribe((state) => this.render(state));
|
|
@@ -2568,6 +4301,15 @@ class MediaStatusStrip {
|
|
|
2568
4301
|
this.retry.textContent = this.labels.retry();
|
|
2569
4302
|
}
|
|
2570
4303
|
}
|
|
4304
|
+
function requireResponse(response, stage) {
|
|
4305
|
+
if (!response || typeof response !== "object") {
|
|
4306
|
+
throw new TypeError(`AI ${stage} must resolve to an AIResponse object.`);
|
|
4307
|
+
}
|
|
4308
|
+
if (typeof response.content !== "string") {
|
|
4309
|
+
throw new TypeError(`AI ${stage} resolved with a non-string content.`);
|
|
4310
|
+
}
|
|
4311
|
+
return response;
|
|
4312
|
+
}
|
|
2571
4313
|
class AIInterceptor {
|
|
2572
4314
|
constructor(interceptors = {}, markdownService = null, autoTranslateMarkdown = true) {
|
|
2573
4315
|
this.interceptors = interceptors;
|
|
@@ -2638,8 +4380,11 @@ class AIInterceptor {
|
|
|
2638
4380
|
if (processedContext === false) {
|
|
2639
4381
|
return;
|
|
2640
4382
|
}
|
|
2641
|
-
let response =
|
|
2642
|
-
|
|
4383
|
+
let response = requireResponse(
|
|
4384
|
+
await requestEndpoint(processedContext),
|
|
4385
|
+
`requestEndpoint("${processedContext.functionId}")`
|
|
4386
|
+
);
|
|
4387
|
+
response = requireResponse(await this.runAfterPost(response, processedContext), "afterPost");
|
|
2643
4388
|
let content = await this.runBeforeRender(response.content, processedContext);
|
|
2644
4389
|
content = await this.runAutoTranslateMarkdown(content, response);
|
|
2645
4390
|
insertCallback(content);
|
|
@@ -2726,22 +4471,42 @@ class AIService {
|
|
|
2726
4471
|
* 执行 AI 功能
|
|
2727
4472
|
*/
|
|
2728
4473
|
async execute(functionId) {
|
|
2729
|
-
var _a, _b;
|
|
2730
4474
|
const func = this.getFunction(functionId);
|
|
2731
4475
|
if (!func) {
|
|
2732
4476
|
console.error(`AI function "${functionId}" not found.`);
|
|
2733
4477
|
return;
|
|
2734
4478
|
}
|
|
2735
4479
|
const context = this.buildContext(func);
|
|
4480
|
+
const timeout = this.config.timeout ?? 0;
|
|
4481
|
+
let timedOut = false;
|
|
4482
|
+
let timer;
|
|
4483
|
+
const chain = this.interceptor.executeChain(context, this.config.requestEndpoint, (content) => {
|
|
4484
|
+
if (timedOut) return;
|
|
4485
|
+
this.insertContent(content, func.outputMode || "insert");
|
|
4486
|
+
});
|
|
2736
4487
|
try {
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
4488
|
+
if (timeout > 0) {
|
|
4489
|
+
await Promise.race([
|
|
4490
|
+
chain,
|
|
4491
|
+
new Promise((_resolve, reject) => {
|
|
4492
|
+
timer = setTimeout(() => {
|
|
4493
|
+
timedOut = true;
|
|
4494
|
+
reject(new Error(`AI function "${functionId}" timed out after ${timeout}ms.`));
|
|
4495
|
+
}, timeout);
|
|
4496
|
+
})
|
|
4497
|
+
]);
|
|
4498
|
+
} else {
|
|
4499
|
+
await chain;
|
|
4500
|
+
}
|
|
2742
4501
|
} catch (error) {
|
|
2743
4502
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
2744
|
-
|
|
4503
|
+
if (this.config.onError) {
|
|
4504
|
+
runSafely(this.config.onError, "ai.onError", [err, context]);
|
|
4505
|
+
} else {
|
|
4506
|
+
console.error(`[Atri Editor] AI function "${functionId}" failed:`, err);
|
|
4507
|
+
}
|
|
4508
|
+
} finally {
|
|
4509
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
2745
4510
|
}
|
|
2746
4511
|
}
|
|
2747
4512
|
/**
|
|
@@ -2816,14 +4581,16 @@ class AICommandMenuManager {
|
|
|
2816
4581
|
this.detachKeyListener = () => document.removeEventListener("keydown", handler, true);
|
|
2817
4582
|
}
|
|
2818
4583
|
setupKeyboardListener() {
|
|
2819
|
-
|
|
4584
|
+
const handler = ({ transaction }) => {
|
|
2820
4585
|
if (transaction.docChanged) {
|
|
2821
4586
|
const lastChar = this.getLastTypedChar();
|
|
2822
4587
|
if (lastChar === this.triggerChar && !this.isVisible) {
|
|
2823
4588
|
this.show();
|
|
2824
4589
|
}
|
|
2825
4590
|
}
|
|
2826
|
-
}
|
|
4591
|
+
};
|
|
4592
|
+
this.editor.on("transaction", handler);
|
|
4593
|
+
this.detachTransactionListener = () => this.editor.off("transaction", handler);
|
|
2827
4594
|
}
|
|
2828
4595
|
getLastTypedChar() {
|
|
2829
4596
|
const { doc, selection } = this.editor.state;
|
|
@@ -2834,7 +4601,7 @@ class AICommandMenuManager {
|
|
|
2834
4601
|
* 显示菜单
|
|
2835
4602
|
*/
|
|
2836
4603
|
show() {
|
|
2837
|
-
if (this.isVisible) return;
|
|
4604
|
+
if (this.isVisible || !this.functions.length) return;
|
|
2838
4605
|
this.isVisible = true;
|
|
2839
4606
|
this.selectedIndex = 0;
|
|
2840
4607
|
this.createMenuElement();
|
|
@@ -2898,15 +4665,13 @@ class AICommandMenuManager {
|
|
|
2898
4665
|
};
|
|
2899
4666
|
({ x, y } = await computePosition(virtualReference, menuElement, {
|
|
2900
4667
|
placement: "bottom-start",
|
|
2901
|
-
middleware:
|
|
4668
|
+
middleware: panelMiddleware()
|
|
2902
4669
|
}));
|
|
2903
4670
|
} catch {
|
|
2904
4671
|
return;
|
|
2905
4672
|
}
|
|
2906
4673
|
if (this.menuElement !== menuElement) return;
|
|
2907
|
-
menuElement
|
|
2908
|
-
menuElement.style.left = `${x}px`;
|
|
2909
|
-
menuElement.style.top = `${y}px`;
|
|
4674
|
+
placePanel(menuElement, x, y);
|
|
2910
4675
|
}
|
|
2911
4676
|
selectItem(index) {
|
|
2912
4677
|
const func = this.functions[index];
|
|
@@ -2971,9 +4736,11 @@ class AICommandMenuManager {
|
|
|
2971
4736
|
* 销毁
|
|
2972
4737
|
*/
|
|
2973
4738
|
destroy() {
|
|
2974
|
-
var _a;
|
|
4739
|
+
var _a, _b;
|
|
2975
4740
|
(_a = this.detachKeyListener) == null ? void 0 : _a.call(this);
|
|
2976
4741
|
this.detachKeyListener = void 0;
|
|
4742
|
+
(_b = this.detachTransactionListener) == null ? void 0 : _b.call(this);
|
|
4743
|
+
this.detachTransactionListener = void 0;
|
|
2977
4744
|
this.hide();
|
|
2978
4745
|
}
|
|
2979
4746
|
}
|
|
@@ -2983,9 +4750,12 @@ class AtriEditor {
|
|
|
2983
4750
|
this.toolbarContainer = null;
|
|
2984
4751
|
this.aiService = null;
|
|
2985
4752
|
this.aiMenuManager = null;
|
|
4753
|
+
this.linkPanel = null;
|
|
4754
|
+
this.tableMenu = null;
|
|
2986
4755
|
this.mediaStatus = null;
|
|
2987
4756
|
this.pendingViewState = null;
|
|
2988
4757
|
this.bubbleElement = null;
|
|
4758
|
+
this.destroyed = false;
|
|
2989
4759
|
this.options = options;
|
|
2990
4760
|
const rootElement = resolveElement(options.element);
|
|
2991
4761
|
this.container = createContainer("atri-editor");
|
|
@@ -3018,7 +4788,7 @@ class AtriEditor {
|
|
|
3018
4788
|
return this.bubbleElement;
|
|
3019
4789
|
}
|
|
3020
4790
|
onEditorCreated() {
|
|
3021
|
-
var _a, _b, _c;
|
|
4791
|
+
var _a, _b, _c, _d;
|
|
3022
4792
|
if (this.pendingViewState) {
|
|
3023
4793
|
const { from, to } = this.pendingViewState;
|
|
3024
4794
|
this.pendingViewState = null;
|
|
@@ -3028,7 +4798,7 @@ class AtriEditor {
|
|
|
3028
4798
|
to: Math.min(to, limit)
|
|
3029
4799
|
});
|
|
3030
4800
|
}
|
|
3031
|
-
if ((_a = this.options.ai) == null ? void 0 : _a.functions) {
|
|
4801
|
+
if ((_b = (_a = this.options.ai) == null ? void 0 : _a.functions) == null ? void 0 : _b.length) {
|
|
3032
4802
|
this.aiMenuManager = new AICommandMenuManager(
|
|
3033
4803
|
this.editor,
|
|
3034
4804
|
this.options.ai.functions,
|
|
@@ -3039,7 +4809,7 @@ class AtriEditor {
|
|
|
3039
4809
|
}
|
|
3040
4810
|
);
|
|
3041
4811
|
}
|
|
3042
|
-
(
|
|
4812
|
+
(_d = (_c = this.options).onCreate) == null ? void 0 : _d.call(_c, this);
|
|
3043
4813
|
}
|
|
3044
4814
|
initAI(config) {
|
|
3045
4815
|
this.aiService = new AIService(this.editor, config, this.markdownService);
|
|
@@ -3060,6 +4830,9 @@ class AtriEditor {
|
|
|
3060
4830
|
markdown: this.options.markdown,
|
|
3061
4831
|
media: this.options.media,
|
|
3062
4832
|
table: this.options.table,
|
|
4833
|
+
link: this.options.link,
|
|
4834
|
+
formatting: this.options.formatting,
|
|
4835
|
+
codeBlock: this.options.codeBlock,
|
|
3063
4836
|
mediaRuntime: this.mediaRuntime ?? void 0,
|
|
3064
4837
|
bubbleElement: this.ensureBubbleElement(this.options.toolbar),
|
|
3065
4838
|
onCreate: () => {
|
|
@@ -3083,19 +4856,39 @@ class AtriEditor {
|
|
|
3083
4856
|
}
|
|
3084
4857
|
});
|
|
3085
4858
|
}
|
|
3086
|
-
/** 初始化依赖编辑器实例的子系统:Markdown
|
|
4859
|
+
/** 初始化依赖编辑器实例的子系统:Markdown 服务、链接浮层、工具栏、表格菜单、AI 服务,重建后同样要重新走一遍 */
|
|
3087
4860
|
setupSubsystems() {
|
|
4861
|
+
var _a, _b;
|
|
3088
4862
|
this.markdownService = new MarkdownService(this.editor, this.options.markdown);
|
|
4863
|
+
this.linkPanel = this.editor.schema.marks.link ? new LinkPanel({
|
|
4864
|
+
editor: this.editor,
|
|
4865
|
+
root: this.container,
|
|
4866
|
+
i18n: this.i18nManager,
|
|
4867
|
+
openOnClick: ((_a = this.options.link) == null ? void 0 : _a.openOnClick) ?? false,
|
|
4868
|
+
target: ((_b = this.options.link) == null ? void 0 : _b.target) ?? "_blank",
|
|
4869
|
+
// 开合不改文档,工具栏等不到 transaction,按钮的按下态得主动推一次
|
|
4870
|
+
onOpenChange: () => {
|
|
4871
|
+
var _a2;
|
|
4872
|
+
return (_a2 = this.toolbarManager) == null ? void 0 : _a2.updateButtonStates();
|
|
4873
|
+
}
|
|
4874
|
+
}) : null;
|
|
3089
4875
|
if (this.toolbarContainer && this.options.toolbar !== false) {
|
|
3090
4876
|
this.toolbarManager = new ToolbarManager(
|
|
3091
4877
|
this.editor,
|
|
3092
4878
|
this.toolbarContainer,
|
|
3093
4879
|
this.options.toolbar,
|
|
3094
4880
|
this.i18nManager,
|
|
3095
|
-
this.mediaRuntime
|
|
4881
|
+
this.mediaRuntime,
|
|
4882
|
+
this.linkPanel
|
|
3096
4883
|
);
|
|
3097
4884
|
if (this.bubbleElement) this.toolbarManager.attachBubbleToolbar(this.bubbleElement);
|
|
3098
4885
|
}
|
|
4886
|
+
const tableConfig = resolveTableConfig(this.options.table);
|
|
4887
|
+
this.tableMenu = tableConfig.menu && canInsertTable(this.editor) ? new TableMenu({
|
|
4888
|
+
editor: this.editor,
|
|
4889
|
+
root: this.container,
|
|
4890
|
+
i18n: this.i18nManager
|
|
4891
|
+
}) : null;
|
|
3099
4892
|
if (this.options.ai) {
|
|
3100
4893
|
this.initAI(this.options.ai);
|
|
3101
4894
|
}
|
|
@@ -3105,14 +4898,16 @@ class AtriEditor {
|
|
|
3105
4898
|
* 会整体替换编辑器视图,因此内容与选区都要手动恢复
|
|
3106
4899
|
*/
|
|
3107
4900
|
recreateEditor() {
|
|
3108
|
-
var _a, _b;
|
|
4901
|
+
var _a, _b, _c, _d;
|
|
3109
4902
|
const previousEditor = this.coreEditor.getEditor();
|
|
3110
4903
|
const currentContent = this.coreEditor.getHTML();
|
|
3111
4904
|
const { from, to } = previousEditor.state.selection;
|
|
3112
4905
|
this.pendingViewState = { from, to };
|
|
3113
|
-
this.coreEditor.destroy();
|
|
3114
4906
|
(_a = this.toolbarManager) == null ? void 0 : _a.destroy();
|
|
3115
|
-
(_b = this.
|
|
4907
|
+
(_b = this.tableMenu) == null ? void 0 : _b.destroy();
|
|
4908
|
+
(_c = this.linkPanel) == null ? void 0 : _c.destroy();
|
|
4909
|
+
(_d = this.aiMenuManager) == null ? void 0 : _d.destroy();
|
|
4910
|
+
this.coreEditor.destroy();
|
|
3116
4911
|
const editorElement = this.container.querySelector(
|
|
3117
4912
|
".atri-editor-content-wrapper"
|
|
3118
4913
|
);
|
|
@@ -3371,6 +5166,7 @@ class AtriEditor {
|
|
|
3371
5166
|
* 连续注册多个组件请改用 registerNodeViews,只需重建一次
|
|
3372
5167
|
*/
|
|
3373
5168
|
registerNodeView(config) {
|
|
5169
|
+
if (this.refuseAfterDestroy("registerNodeView")) return;
|
|
3374
5170
|
this.extensionManager.registerNodeView(config);
|
|
3375
5171
|
if (this.coreEditor) {
|
|
3376
5172
|
this.recreateEditor();
|
|
@@ -3381,11 +5177,21 @@ class AtriEditor {
|
|
|
3381
5177
|
* 注意:如果编辑器已创建,会重新创建编辑器以更新 schema
|
|
3382
5178
|
*/
|
|
3383
5179
|
registerNodeViews(configs) {
|
|
5180
|
+
if (this.refuseAfterDestroy("registerNodeViews")) return;
|
|
3384
5181
|
this.extensionManager.registerNodeViews(configs);
|
|
3385
5182
|
if (this.coreEditor) {
|
|
3386
5183
|
this.recreateEditor();
|
|
3387
5184
|
}
|
|
3388
5185
|
}
|
|
5186
|
+
/**
|
|
5187
|
+
* 销毁后重建类 API 的统一守卫:判在改状态之前,
|
|
5188
|
+
* 否则配置进了 extensionManager 却没有对应的编辑器,看起来就像"注册了但没生效"
|
|
5189
|
+
*/
|
|
5190
|
+
refuseAfterDestroy(api) {
|
|
5191
|
+
if (!this.destroyed) return false;
|
|
5192
|
+
console.warn(`[Atri Editor] ${api}() was called on a destroyed editor and did nothing.`);
|
|
5193
|
+
return true;
|
|
5194
|
+
}
|
|
3389
5195
|
/**
|
|
3390
5196
|
* 获取所有已注册的 NodeView
|
|
3391
5197
|
*/
|
|
@@ -3396,12 +5202,16 @@ class AtriEditor {
|
|
|
3396
5202
|
* 销毁编辑器
|
|
3397
5203
|
*/
|
|
3398
5204
|
destroy() {
|
|
3399
|
-
var _a, _b, _c, _d;
|
|
5205
|
+
var _a, _b, _c, _d, _e, _f;
|
|
5206
|
+
if (this.destroyed) return;
|
|
5207
|
+
this.destroyed = true;
|
|
3400
5208
|
(_a = this.toolbarManager) == null ? void 0 : _a.destroy();
|
|
3401
|
-
(_b = this.
|
|
5209
|
+
(_b = this.tableMenu) == null ? void 0 : _b.destroy();
|
|
5210
|
+
(_c = this.linkPanel) == null ? void 0 : _c.destroy();
|
|
5211
|
+
(_d = this.aiMenuManager) == null ? void 0 : _d.destroy();
|
|
3402
5212
|
this.coreEditor.destroy();
|
|
3403
|
-
(
|
|
3404
|
-
(
|
|
5213
|
+
(_e = this.mediaStatus) == null ? void 0 : _e.destroy();
|
|
5214
|
+
(_f = this.mediaRuntime) == null ? void 0 : _f.destroy();
|
|
3405
5215
|
this.bubbleElement = null;
|
|
3406
5216
|
this.container.remove();
|
|
3407
5217
|
}
|
|
@@ -3423,15 +5233,41 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
3423
5233
|
super();
|
|
3424
5234
|
this.editor = null;
|
|
3425
5235
|
this._options = {};
|
|
5236
|
+
this.teardownToken = 0;
|
|
5237
|
+
this.detachedContent = null;
|
|
5238
|
+
this.destroyed = false;
|
|
3426
5239
|
}
|
|
3427
5240
|
static get observedAttributes() {
|
|
3428
5241
|
return ["theme", "editable", "lang", "placeholder"];
|
|
3429
5242
|
}
|
|
3430
5243
|
connectedCallback() {
|
|
5244
|
+
this.teardownToken += 1;
|
|
5245
|
+
if (this.destroyed) return;
|
|
3431
5246
|
this.initEditor();
|
|
3432
5247
|
}
|
|
3433
5248
|
disconnectedCallback() {
|
|
3434
|
-
this.
|
|
5249
|
+
const token = this.teardownToken;
|
|
5250
|
+
queueMicrotask(() => {
|
|
5251
|
+
if (token === this.teardownToken && !this.isConnected) this.teardownEditor();
|
|
5252
|
+
});
|
|
5253
|
+
}
|
|
5254
|
+
/**
|
|
5255
|
+
* 显式销毁:之后重挂不再自动重建,内容快照也一并丢掉
|
|
5256
|
+
*
|
|
5257
|
+
* 框架里对应 unmount 钩子(Vue 的 unmounted、React effect 的 cleanup)。
|
|
5258
|
+
* 断连本身已经会延迟拆除,这个方法的意义在于"别接回去了"
|
|
5259
|
+
*/
|
|
5260
|
+
destroy() {
|
|
5261
|
+
this.destroyed = true;
|
|
5262
|
+
this.teardownToken += 1;
|
|
5263
|
+
this.detachedContent = null;
|
|
5264
|
+
this.teardownEditor();
|
|
5265
|
+
}
|
|
5266
|
+
teardownEditor() {
|
|
5267
|
+
if (!this.editor) return;
|
|
5268
|
+
if (!this.destroyed) this.detachedContent = this.editor.getHTML();
|
|
5269
|
+
this.editor.destroy();
|
|
5270
|
+
this.editor = null;
|
|
3435
5271
|
}
|
|
3436
5272
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
3437
5273
|
if (oldValue === newValue) return;
|
|
@@ -3463,6 +5299,7 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
3463
5299
|
*/
|
|
3464
5300
|
setOptions(options) {
|
|
3465
5301
|
this._options = { ...this._options, ...options };
|
|
5302
|
+
if ("content" in options) this.detachedContent = null;
|
|
3466
5303
|
}
|
|
3467
5304
|
initEditor() {
|
|
3468
5305
|
if (this.editor) return;
|
|
@@ -3482,6 +5319,11 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
3482
5319
|
if (content) options.content = content;
|
|
3483
5320
|
const contentFormat = this.getAttribute("data-content-format");
|
|
3484
5321
|
if (contentFormat) options.contentFormat = contentFormat;
|
|
5322
|
+
if (this.detachedContent !== null) {
|
|
5323
|
+
options.content = this.detachedContent;
|
|
5324
|
+
options.contentFormat = "html";
|
|
5325
|
+
this.detachedContent = null;
|
|
5326
|
+
}
|
|
3485
5327
|
this.editor = new AtriEditor(options);
|
|
3486
5328
|
this.dispatchEvent(
|
|
3487
5329
|
new CustomEvent("editor-created", {
|
|
@@ -3490,12 +5332,6 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
3490
5332
|
})
|
|
3491
5333
|
);
|
|
3492
5334
|
}
|
|
3493
|
-
destroyEditor() {
|
|
3494
|
-
if (this.editor) {
|
|
3495
|
-
this.editor.destroy();
|
|
3496
|
-
this.editor = null;
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
5335
|
/**
|
|
3500
5336
|
* 获取编辑器实例
|
|
3501
5337
|
*/
|
|
@@ -3604,6 +5440,7 @@ export {
|
|
|
3604
5440
|
ThemeManager,
|
|
3605
5441
|
ToolbarManager,
|
|
3606
5442
|
createContainer,
|
|
5443
|
+
createFormattingExtensions,
|
|
3607
5444
|
createMediaExtensions,
|
|
3608
5445
|
getCursorContext,
|
|
3609
5446
|
getDocumentText,
|