@atri-editor/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/dist/AtriEditor.d.ts +4 -1
- package/dist/AtriEditor.d.ts.map +1 -1
- package/dist/core/CoreEditor.d.ts +5 -3
- package/dist/core/CoreEditor.d.ts.map +1 -1
- package/dist/core/I18nManager.d.ts.map +1 -1
- package/dist/core/LinkPanel.d.ts +73 -0
- package/dist/core/LinkPanel.d.ts.map +1 -0
- package/dist/core/TableMenu.d.ts +67 -0
- package/dist/core/TableMenu.d.ts.map +1 -0
- package/dist/core/ToolbarManager.d.ts +8 -2
- package/dist/core/ToolbarManager.d.ts.map +1 -1
- package/dist/core/icons.d.ts.map +1 -1
- package/dist/extensions/table.d.ts +23 -0
- package/dist/extensions/table.d.ts.map +1 -0
- package/dist/index.js +685 -31
- package/dist/index.js.map +1 -1
- package/dist/style.css +125 -4
- package/dist/types/editor.d.ts +31 -2
- package/dist/types/editor.d.ts.map +1 -1
- package/dist/utils/table.d.ts +11 -4
- package/dist/utils/table.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -4,12 +4,40 @@ 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";
|
|
10
11
|
import { Node as Node$1, DOMParser } from "@tiptap/pm/model";
|
|
11
12
|
import i18next from "i18next";
|
|
12
13
|
import { autoUpdate, computePosition, offset, flip, shift } from "@floating-ui/dom";
|
|
14
|
+
function isBuiltinTableActive(editor) {
|
|
15
|
+
var _a;
|
|
16
|
+
const spec = (_a = editor.schema.nodes.table) == null ? void 0 : _a.spec;
|
|
17
|
+
return (spec == null ? void 0 : spec.tableRole) === "table";
|
|
18
|
+
}
|
|
19
|
+
function canInsertTable(editor) {
|
|
20
|
+
if (typeof editor.commands.insertTable !== "function") return false;
|
|
21
|
+
return isBuiltinTableActive(editor);
|
|
22
|
+
}
|
|
23
|
+
const DEFAULT_TABLE_CONFIG = {
|
|
24
|
+
resizable: true,
|
|
25
|
+
menu: true
|
|
26
|
+
};
|
|
27
|
+
function resolveTableConfig(table) {
|
|
28
|
+
return { ...DEFAULT_TABLE_CONFIG, ...typeof table === "object" ? table : void 0 };
|
|
29
|
+
}
|
|
30
|
+
const AtriTable = Table.extend({
|
|
31
|
+
addProseMirrorPlugins() {
|
|
32
|
+
var _a;
|
|
33
|
+
if (!isBuiltinTableActive(this.editor)) return [];
|
|
34
|
+
return ((_a = this.parent) == null ? void 0 : _a.call(this)) ?? [];
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
function createTableExtensions(table) {
|
|
38
|
+
const config = resolveTableConfig(table);
|
|
39
|
+
return [AtriTable.configure({ resizable: config.resizable }), TableRow, TableHeader, TableCell];
|
|
40
|
+
}
|
|
13
41
|
function mediaInsertTarget(selection) {
|
|
14
42
|
if (selection instanceof NodeSelection && (selection.node.type.name === "image" || selection.node.type.name === "attachment" || selection.node.type.name === "attachmentLink")) {
|
|
15
43
|
return selection.to;
|
|
@@ -1033,6 +1061,11 @@ function shouldShowBubbleMenu(props) {
|
|
|
1033
1061
|
if (!group) return false;
|
|
1034
1062
|
return !!element.querySelector(`[data-atri-bubble-group="${group}"] [data-toolbar-item]`);
|
|
1035
1063
|
}
|
|
1064
|
+
const LINK_HTML_ATTRIBUTES = {
|
|
1065
|
+
target: "_blank",
|
|
1066
|
+
rel: "noopener noreferrer nofollow",
|
|
1067
|
+
class: null
|
|
1068
|
+
};
|
|
1036
1069
|
class CoreEditor {
|
|
1037
1070
|
constructor(config) {
|
|
1038
1071
|
this.config = config;
|
|
@@ -1041,11 +1074,22 @@ class CoreEditor {
|
|
|
1041
1074
|
this.editor = this.createEditor();
|
|
1042
1075
|
}
|
|
1043
1076
|
createEditor() {
|
|
1044
|
-
var _a, _b, _c, _d, _e;
|
|
1077
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1045
1078
|
const { element, content, contentFormat, editable, extensions } = this.config;
|
|
1046
1079
|
const editorExtensions = [
|
|
1047
1080
|
StarterKit.configure({
|
|
1048
|
-
// StarterKit v3 默认包含 Underline, Link, TrailingNode
|
|
1081
|
+
// StarterKit v3 默认包含 Underline, Link, TrailingNode。
|
|
1082
|
+
// 链接:openOnClick 默认关——编辑态点击落在链接上,用户要的是改它,
|
|
1083
|
+
// 而 Link 扩展开着导航时会把这次点击吃掉(连光标都放不进去)并打开新标签页,
|
|
1084
|
+
// 未保存的内容就跟着这次点击没了。关掉后点击交给 LinkPanel 就地编辑。
|
|
1085
|
+
link: {
|
|
1086
|
+
openOnClick: ((_a = this.config.link) == null ? void 0 : _a.openOnClick) ?? false,
|
|
1087
|
+
markdownLinks: ((_b = this.config.link) == null ? void 0 : _b.markdownLinks) ?? false,
|
|
1088
|
+
HTMLAttributes: {
|
|
1089
|
+
...LINK_HTML_ATTRIBUTES,
|
|
1090
|
+
target: ((_c = this.config.link) == null ? void 0 : _c.target) ?? LINK_HTML_ATTRIBUTES.target
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1049
1093
|
})
|
|
1050
1094
|
];
|
|
1051
1095
|
editorExtensions.push(
|
|
@@ -1058,19 +1102,19 @@ class CoreEditor {
|
|
|
1058
1102
|
types: ["heading", "paragraph"]
|
|
1059
1103
|
})
|
|
1060
1104
|
);
|
|
1061
|
-
const markdownEnabled = ((
|
|
1105
|
+
const markdownEnabled = ((_d = this.config.markdown) == null ? void 0 : _d.enabled) !== false;
|
|
1062
1106
|
if (markdownEnabled) {
|
|
1063
1107
|
editorExtensions.push(
|
|
1064
1108
|
Markdown.configure({
|
|
1065
|
-
indentation: (
|
|
1066
|
-
markedOptions: (
|
|
1109
|
+
indentation: (_e = this.config.markdown) == null ? void 0 : _e.indentation,
|
|
1110
|
+
markedOptions: (_f = this.config.markdown) == null ? void 0 : _f.markedOptions
|
|
1067
1111
|
})
|
|
1068
1112
|
);
|
|
1069
1113
|
}
|
|
1070
1114
|
const contentType = contentFormat === "markdown" && !markdownEnabled ? void 0 : contentFormat;
|
|
1071
1115
|
editorExtensions.push(...createMediaExtensions(this.config.media, this.mediaRuntime));
|
|
1072
1116
|
if (this.config.table !== false) {
|
|
1073
|
-
editorExtensions.push(
|
|
1117
|
+
editorExtensions.push(...createTableExtensions(resolveTableConfig(this.config.table)));
|
|
1074
1118
|
}
|
|
1075
1119
|
if (this.config.bubbleElement) {
|
|
1076
1120
|
editorExtensions.push(
|
|
@@ -1089,7 +1133,8 @@ class CoreEditor {
|
|
|
1089
1133
|
})
|
|
1090
1134
|
);
|
|
1091
1135
|
}
|
|
1092
|
-
|
|
1136
|
+
editorExtensions.push(TaskList, TaskItem);
|
|
1137
|
+
const enableInputRules = ((_g = this.config.markdown) == null ? void 0 : _g.shortcuts) !== false;
|
|
1093
1138
|
if (extensions) {
|
|
1094
1139
|
editorExtensions.push(...extensions);
|
|
1095
1140
|
}
|
|
@@ -1126,7 +1171,7 @@ class CoreEditor {
|
|
|
1126
1171
|
(_b2 = (_a2 = this.config).onDestroy) == null ? void 0 : _b2.call(_a2);
|
|
1127
1172
|
}
|
|
1128
1173
|
});
|
|
1129
|
-
(
|
|
1174
|
+
(_h = this.mediaRuntime) == null ? void 0 : _h.bind(editor);
|
|
1130
1175
|
return editor;
|
|
1131
1176
|
}
|
|
1132
1177
|
/**
|
|
@@ -1433,6 +1478,7 @@ const zhResources = {
|
|
|
1433
1478
|
taskList: "任务列表",
|
|
1434
1479
|
codeBlock: "代码块",
|
|
1435
1480
|
blockquote: "引用",
|
|
1481
|
+
horizontalRule: "分割线",
|
|
1436
1482
|
link: "链接",
|
|
1437
1483
|
image: "图片",
|
|
1438
1484
|
attachment: "附件",
|
|
@@ -1458,6 +1504,29 @@ const zhResources = {
|
|
|
1458
1504
|
altPlaceholder: "替代文字",
|
|
1459
1505
|
insert: "插入"
|
|
1460
1506
|
},
|
|
1507
|
+
table: {
|
|
1508
|
+
menuLabel: "表格操作",
|
|
1509
|
+
rowBefore: "上方插入行",
|
|
1510
|
+
rowAfter: "下方插入行",
|
|
1511
|
+
deleteRow: "删除行",
|
|
1512
|
+
columnBefore: "左侧插入列",
|
|
1513
|
+
columnAfter: "右侧插入列",
|
|
1514
|
+
deleteColumn: "删除列",
|
|
1515
|
+
mergeCells: "合并单元格",
|
|
1516
|
+
splitCell: "拆分单元格",
|
|
1517
|
+
toggleHeaderRow: "切换表头行",
|
|
1518
|
+
alignLeft: "单元格左对齐",
|
|
1519
|
+
alignCenter: "单元格居中",
|
|
1520
|
+
alignRight: "单元格右对齐",
|
|
1521
|
+
deleteTable: "删除表格"
|
|
1522
|
+
},
|
|
1523
|
+
link: {
|
|
1524
|
+
panelLabel: "链接",
|
|
1525
|
+
urlPlaceholder: "链接地址(https://…)",
|
|
1526
|
+
newWindow: "在新窗口打开",
|
|
1527
|
+
apply: "确定",
|
|
1528
|
+
remove: "移除链接"
|
|
1529
|
+
},
|
|
1461
1530
|
ai: {
|
|
1462
1531
|
menu: "AI 菜单",
|
|
1463
1532
|
continue: "AI 续写",
|
|
@@ -1486,6 +1555,7 @@ const enResources = {
|
|
|
1486
1555
|
taskList: "Task List",
|
|
1487
1556
|
codeBlock: "Code Block",
|
|
1488
1557
|
blockquote: "Blockquote",
|
|
1558
|
+
horizontalRule: "Divider",
|
|
1489
1559
|
link: "Link",
|
|
1490
1560
|
image: "Image",
|
|
1491
1561
|
attachment: "Attachment",
|
|
@@ -1511,6 +1581,29 @@ const enResources = {
|
|
|
1511
1581
|
altPlaceholder: "Alternative text",
|
|
1512
1582
|
insert: "Insert"
|
|
1513
1583
|
},
|
|
1584
|
+
table: {
|
|
1585
|
+
menuLabel: "Table actions",
|
|
1586
|
+
rowBefore: "Insert row above",
|
|
1587
|
+
rowAfter: "Insert row below",
|
|
1588
|
+
deleteRow: "Delete row",
|
|
1589
|
+
columnBefore: "Insert column left",
|
|
1590
|
+
columnAfter: "Insert column right",
|
|
1591
|
+
deleteColumn: "Delete column",
|
|
1592
|
+
mergeCells: "Merge cells",
|
|
1593
|
+
splitCell: "Split cell",
|
|
1594
|
+
toggleHeaderRow: "Toggle header row",
|
|
1595
|
+
alignLeft: "Align cell left",
|
|
1596
|
+
alignCenter: "Center cell",
|
|
1597
|
+
alignRight: "Align cell right",
|
|
1598
|
+
deleteTable: "Delete table"
|
|
1599
|
+
},
|
|
1600
|
+
link: {
|
|
1601
|
+
panelLabel: "Link",
|
|
1602
|
+
urlPlaceholder: "Link URL (https://…)",
|
|
1603
|
+
newWindow: "Open in new window",
|
|
1604
|
+
apply: "Apply",
|
|
1605
|
+
remove: "Remove link"
|
|
1606
|
+
},
|
|
1514
1607
|
ai: {
|
|
1515
1608
|
menu: "AI Menu",
|
|
1516
1609
|
continue: "AI Continue",
|
|
@@ -2008,12 +2101,6 @@ class TableGridPanel {
|
|
|
2008
2101
|
this.highlight(row + 1, col + 1);
|
|
2009
2102
|
}
|
|
2010
2103
|
}
|
|
2011
|
-
function canInsertTable(editor) {
|
|
2012
|
-
var _a;
|
|
2013
|
-
if (typeof editor.commands.insertTable !== "function") return false;
|
|
2014
|
-
const spec = (_a = editor.schema.nodes.table) == null ? void 0 : _a.spec;
|
|
2015
|
-
return (spec == null ? void 0 : spec.tableRole) === "table";
|
|
2016
|
-
}
|
|
2017
2104
|
const icons = {
|
|
2018
2105
|
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>',
|
|
2019
2106
|
redo: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3L21 13"/></svg>',
|
|
@@ -2028,16 +2115,31 @@ const icons = {
|
|
|
2028
2115
|
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
2116
|
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
2117
|
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>',
|
|
2118
|
+
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>',
|
|
2119
|
+
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
2120
|
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
2121
|
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
2122
|
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
2123
|
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
2124
|
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>',
|
|
2125
|
+
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
2126
|
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
2127
|
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
2128
|
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
2129
|
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>'
|
|
2130
|
+
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>',
|
|
2131
|
+
// 表格操作菜单:统一用「表格边框 + 方向标记」一套句法——
|
|
2132
|
+
// 行操作画横分隔线、列操作画竖分隔线,加号落在哪一格就是插到哪一侧,
|
|
2133
|
+
// 减号落在哪一格就是删哪一格(两种删除靠分隔线方向区分)
|
|
2134
|
+
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>',
|
|
2135
|
+
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>',
|
|
2136
|
+
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>',
|
|
2137
|
+
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>',
|
|
2138
|
+
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>',
|
|
2139
|
+
tableColumnDelete: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/><path d="M15 3v18"/><path d="M10.5 12h3"/></svg>',
|
|
2140
|
+
tableMergeCells: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="m10.5 9.5-2.5 2.5 2.5 2.5"/><path d="m13.5 9.5 2.5 2.5-2.5 2.5"/></svg>',
|
|
2141
|
+
tableSplitCell: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/><path d="M9 12H6"/><path d="M15 12h3"/></svg>',
|
|
2142
|
+
tableHeaderRow: '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M7 6h4"/></svg>'
|
|
2041
2143
|
};
|
|
2042
2144
|
const TOOLTIP_KEYS = {
|
|
2043
2145
|
undo: "editor.undo",
|
|
@@ -2053,19 +2155,23 @@ const TOOLTIP_KEYS = {
|
|
|
2053
2155
|
code: "editor.code",
|
|
2054
2156
|
bulletList: "editor.bulletList",
|
|
2055
2157
|
orderedList: "editor.orderedList",
|
|
2158
|
+
taskList: "editor.taskList",
|
|
2056
2159
|
blockquote: "editor.blockquote",
|
|
2057
2160
|
codeBlock: "editor.codeBlock",
|
|
2161
|
+
horizontalRule: "editor.horizontalRule",
|
|
2058
2162
|
alignLeft: "editor.alignLeft",
|
|
2059
2163
|
alignCenter: "editor.alignCenter",
|
|
2060
2164
|
alignRight: "editor.alignRight",
|
|
2165
|
+
alignJustify: "editor.alignJustify",
|
|
2061
2166
|
insertImage: "editor.image",
|
|
2062
2167
|
insertAttachment: "editor.attachment",
|
|
2063
2168
|
attachmentDisplay: "editor.attachmentDisplay",
|
|
2064
2169
|
insertTable: "editor.table",
|
|
2170
|
+
insertLink: "editor.link",
|
|
2065
2171
|
delete: "editor.delete"
|
|
2066
2172
|
};
|
|
2067
2173
|
class ToolbarManager {
|
|
2068
|
-
constructor(editor, container, config, i18n, mediaRuntime) {
|
|
2174
|
+
constructor(editor, container, config, i18n, mediaRuntime, linkPanel) {
|
|
2069
2175
|
var _a;
|
|
2070
2176
|
this.buttons = /* @__PURE__ */ new Map();
|
|
2071
2177
|
this.customTooltips = /* @__PURE__ */ new Map();
|
|
@@ -2077,6 +2183,7 @@ class ToolbarManager {
|
|
|
2077
2183
|
this.config = config;
|
|
2078
2184
|
this.i18n = i18n;
|
|
2079
2185
|
this.mediaRuntime = mediaRuntime ?? null;
|
|
2186
|
+
this.linkPanel = linkPanel ?? null;
|
|
2080
2187
|
this.itemDefs = this.getDefaultItems();
|
|
2081
2188
|
this.createToolbarDOM();
|
|
2082
2189
|
this.bindEditorEvents();
|
|
@@ -2169,11 +2276,13 @@ class ToolbarManager {
|
|
|
2169
2276
|
const layout = [
|
|
2170
2277
|
["undo", "redo"],
|
|
2171
2278
|
["heading1", "heading2", "heading3", "paragraph"],
|
|
2172
|
-
|
|
2173
|
-
["
|
|
2174
|
-
["
|
|
2175
|
-
|
|
2176
|
-
|
|
2279
|
+
// 链接跟着行内格式走:它也是一段文字上的标记,只是要填地址所以带浮层
|
|
2280
|
+
["bold", "italic", "underline", "strike", "code", "insertLink"],
|
|
2281
|
+
["bulletList", "orderedList", "taskList", "blockquote", "codeBlock"],
|
|
2282
|
+
["alignLeft", "alignCenter", "alignRight", "alignJustify"],
|
|
2283
|
+
// 分割线与表格自成一组:都是"往正文里插一块"的动作,没有激活态。
|
|
2284
|
+
// 表格节点没注册时(table:false 或被顶掉)这项不存在,组里只剩分割线,不会吊出空组
|
|
2285
|
+
["horizontalRule", ...this.itemDefs.has("insertTable") ? ["insertTable"] : []],
|
|
2177
2286
|
// 「附件样式」不再常驻:切换形态改由选中附件时的浮动工具栏承接(bubble 默认开)
|
|
2178
2287
|
...this.itemDefs.has("insertImage") ? [["insertImage", "insertAttachment"]] : []
|
|
2179
2288
|
];
|
|
@@ -2318,6 +2427,24 @@ class ToolbarManager {
|
|
|
2318
2427
|
isDisabled: (editor) => !editor.isEditable
|
|
2319
2428
|
});
|
|
2320
2429
|
}
|
|
2430
|
+
if (this.linkPanel) {
|
|
2431
|
+
items.set("insertLink", {
|
|
2432
|
+
id: "insertLink",
|
|
2433
|
+
icon: icons.link,
|
|
2434
|
+
tooltip: "链接",
|
|
2435
|
+
popup: true,
|
|
2436
|
+
command: (_editor, button) => {
|
|
2437
|
+
var _a;
|
|
2438
|
+
return (_a = this.linkPanel) == null ? void 0 : _a.toggle(button);
|
|
2439
|
+
},
|
|
2440
|
+
// 浮层开着时按下去是关,所以按钮的"按下态"跟的是浮层,不是选区里有没有链接
|
|
2441
|
+
isActive: (editor) => {
|
|
2442
|
+
var _a;
|
|
2443
|
+
return ((_a = this.linkPanel) == null ? void 0 : _a.isOpen) === true || editor.isActive("link");
|
|
2444
|
+
},
|
|
2445
|
+
isDisabled: (editor) => !editor.isEditable
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2321
2448
|
items.set("delete", {
|
|
2322
2449
|
id: "delete",
|
|
2323
2450
|
icon: icons.trash,
|
|
@@ -2430,6 +2557,14 @@ class ToolbarManager {
|
|
|
2430
2557
|
isActive: (editor) => editor.isActive("orderedList"),
|
|
2431
2558
|
isDisabled: (editor) => !editor.can().toggleOrderedList()
|
|
2432
2559
|
});
|
|
2560
|
+
items.set("taskList", {
|
|
2561
|
+
id: "taskList",
|
|
2562
|
+
icon: icons.listChecks,
|
|
2563
|
+
tooltip: "任务列表",
|
|
2564
|
+
command: (editor) => editor.chain().toggleTaskList().run(),
|
|
2565
|
+
isActive: (editor) => editor.isActive("taskList"),
|
|
2566
|
+
isDisabled: (editor) => !editor.can().toggleTaskList()
|
|
2567
|
+
});
|
|
2433
2568
|
items.set("blockquote", {
|
|
2434
2569
|
id: "blockquote",
|
|
2435
2570
|
icon: icons.quote,
|
|
@@ -2446,6 +2581,14 @@ class ToolbarManager {
|
|
|
2446
2581
|
isActive: (editor) => editor.isActive("codeBlock"),
|
|
2447
2582
|
isDisabled: (editor) => !editor.can().toggleCodeBlock()
|
|
2448
2583
|
});
|
|
2584
|
+
items.set("horizontalRule", {
|
|
2585
|
+
id: "horizontalRule",
|
|
2586
|
+
icon: icons.horizontalRule,
|
|
2587
|
+
tooltip: "分割线",
|
|
2588
|
+
command: (editor) => editor.chain().setHorizontalRule().run(),
|
|
2589
|
+
isActive: () => false,
|
|
2590
|
+
isDisabled: (editor) => !editor.can().setHorizontalRule()
|
|
2591
|
+
});
|
|
2449
2592
|
items.set("alignLeft", {
|
|
2450
2593
|
id: "alignLeft",
|
|
2451
2594
|
icon: icons.alignLeft,
|
|
@@ -2470,6 +2613,14 @@ class ToolbarManager {
|
|
|
2470
2613
|
isActive: (editor) => editor.isActive({ textAlign: "right" }),
|
|
2471
2614
|
isDisabled: (editor) => !editor.can().setTextAlign("right")
|
|
2472
2615
|
});
|
|
2616
|
+
items.set("alignJustify", {
|
|
2617
|
+
id: "alignJustify",
|
|
2618
|
+
icon: icons.alignJustify,
|
|
2619
|
+
tooltip: "两端对齐",
|
|
2620
|
+
command: (editor) => editor.chain().setTextAlign("justify").run(),
|
|
2621
|
+
isActive: (editor) => editor.isActive({ textAlign: "justify" }),
|
|
2622
|
+
isDisabled: (editor) => !editor.can().setTextAlign("justify")
|
|
2623
|
+
});
|
|
2473
2624
|
return items;
|
|
2474
2625
|
}
|
|
2475
2626
|
/**
|
|
@@ -2486,6 +2637,9 @@ class ToolbarManager {
|
|
|
2486
2637
|
}
|
|
2487
2638
|
/**
|
|
2488
2639
|
* 更新所有按钮状态
|
|
2640
|
+
*
|
|
2641
|
+
* 公开是因为浮层开合不改文档,等不到 transaction:挂在门面上的浮层(链接)
|
|
2642
|
+
* 由门面在开合时叫一次,挂在这里的浮层则在 onOpenChange 里叫
|
|
2489
2643
|
*/
|
|
2490
2644
|
updateButtonStates() {
|
|
2491
2645
|
this.buttons.forEach((buttons, itemId) => {
|
|
@@ -2517,6 +2671,479 @@ class ToolbarManager {
|
|
|
2517
2671
|
this.createdElements = [];
|
|
2518
2672
|
}
|
|
2519
2673
|
}
|
|
2674
|
+
function normalizeHref(value) {
|
|
2675
|
+
const href = value.trim();
|
|
2676
|
+
if (!href) return "";
|
|
2677
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href) || /^[/#.]/.test(href)) return href;
|
|
2678
|
+
return `https://${href}`;
|
|
2679
|
+
}
|
|
2680
|
+
class LinkPanel {
|
|
2681
|
+
constructor(options) {
|
|
2682
|
+
var _a;
|
|
2683
|
+
this.element = null;
|
|
2684
|
+
this.floating = null;
|
|
2685
|
+
this.detachDismiss = null;
|
|
2686
|
+
this.labelled = [];
|
|
2687
|
+
this.urlInput = null;
|
|
2688
|
+
this.targetInput = null;
|
|
2689
|
+
this.editing = false;
|
|
2690
|
+
this.onEditorClick = (event) => {
|
|
2691
|
+
if (this.openOnClick || !this.editor.isEditable) return;
|
|
2692
|
+
if (!this.editor.state.selection.empty) return;
|
|
2693
|
+
const target = event.target;
|
|
2694
|
+
if (!(target instanceof Element)) return;
|
|
2695
|
+
const anchor = target.closest("a");
|
|
2696
|
+
if (!anchor || !this.root.contains(anchor)) return;
|
|
2697
|
+
event.preventDefault();
|
|
2698
|
+
this.open(anchor);
|
|
2699
|
+
};
|
|
2700
|
+
this.editor = options.editor;
|
|
2701
|
+
this.root = options.root;
|
|
2702
|
+
this.i18n = options.i18n;
|
|
2703
|
+
this.openOnClick = options.openOnClick ?? false;
|
|
2704
|
+
this.defaultTarget = options.target ?? "_blank";
|
|
2705
|
+
this.onOpenChange = options.onOpenChange;
|
|
2706
|
+
this.unsubscribeLanguage = (_a = this.i18n) == null ? void 0 : _a.onLanguageChanged(() => this.applyLabels());
|
|
2707
|
+
this.root.addEventListener("click", this.onEditorClick);
|
|
2708
|
+
}
|
|
2709
|
+
get isOpen() {
|
|
2710
|
+
return this.element !== null;
|
|
2711
|
+
}
|
|
2712
|
+
toggle(anchor) {
|
|
2713
|
+
if (this.isOpen) {
|
|
2714
|
+
this.close();
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
this.open(anchor);
|
|
2718
|
+
}
|
|
2719
|
+
open(anchor) {
|
|
2720
|
+
var _a;
|
|
2721
|
+
this.close();
|
|
2722
|
+
this.editing = this.editor.isActive("link");
|
|
2723
|
+
this.build();
|
|
2724
|
+
const panel = this.element;
|
|
2725
|
+
if (!panel) return;
|
|
2726
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
2727
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
2728
|
+
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
2729
|
+
}
|
|
2730
|
+
close() {
|
|
2731
|
+
var _a, _b, _c, _d;
|
|
2732
|
+
const wasOpen = this.element !== null;
|
|
2733
|
+
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
2734
|
+
this.detachDismiss = null;
|
|
2735
|
+
(_b = this.floating) == null ? void 0 : _b.stop();
|
|
2736
|
+
this.floating = null;
|
|
2737
|
+
(_c = this.element) == null ? void 0 : _c.remove();
|
|
2738
|
+
this.element = null;
|
|
2739
|
+
this.urlInput = null;
|
|
2740
|
+
this.targetInput = null;
|
|
2741
|
+
this.labelled = [];
|
|
2742
|
+
this.editing = false;
|
|
2743
|
+
if (wasOpen) (_d = this.onOpenChange) == null ? void 0 : _d.call(this, false);
|
|
2744
|
+
}
|
|
2745
|
+
destroy() {
|
|
2746
|
+
var _a;
|
|
2747
|
+
this.root.removeEventListener("click", this.onEditorClick);
|
|
2748
|
+
this.close();
|
|
2749
|
+
(_a = this.unsubscribeLanguage) == null ? void 0 : _a.call(this);
|
|
2750
|
+
this.unsubscribeLanguage = void 0;
|
|
2751
|
+
}
|
|
2752
|
+
t(key, fallback) {
|
|
2753
|
+
return tOrFallback(this.i18n, key, fallback);
|
|
2754
|
+
}
|
|
2755
|
+
/** 登记一处随语言重画的文案:input 画到 placeholder,其他画成文字 */
|
|
2756
|
+
label(el, key, fallback) {
|
|
2757
|
+
this.labelled.push({ el, key, fallback });
|
|
2758
|
+
this.applyLabel({ el, key, fallback });
|
|
2759
|
+
}
|
|
2760
|
+
applyLabels() {
|
|
2761
|
+
this.labelled.forEach((entry) => this.applyLabel(entry));
|
|
2762
|
+
}
|
|
2763
|
+
applyLabel({ el, key, fallback }) {
|
|
2764
|
+
const text = this.t(key, fallback);
|
|
2765
|
+
if (el instanceof HTMLInputElement) el.placeholder = text;
|
|
2766
|
+
else el.textContent = text;
|
|
2767
|
+
}
|
|
2768
|
+
build() {
|
|
2769
|
+
const panel = document.createElement("div");
|
|
2770
|
+
panel.className = "atri-link-panel";
|
|
2771
|
+
panel.setAttribute("role", "dialog");
|
|
2772
|
+
panel.setAttribute("aria-label", this.t("link.panelLabel", "链接"));
|
|
2773
|
+
const url = document.createElement("input");
|
|
2774
|
+
url.type = "url";
|
|
2775
|
+
url.className = "atri-link-panel-url";
|
|
2776
|
+
this.label(url, "link.urlPlaceholder", "链接地址(https://…)");
|
|
2777
|
+
url.value = this.editing ? String(this.editor.getAttributes("link").href ?? "") : "";
|
|
2778
|
+
url.addEventListener("keydown", (event) => {
|
|
2779
|
+
if (event.key === "Enter") {
|
|
2780
|
+
event.preventDefault();
|
|
2781
|
+
this.apply();
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
panel.appendChild(url);
|
|
2785
|
+
this.urlInput = url;
|
|
2786
|
+
const targetLabel = document.createElement("label");
|
|
2787
|
+
targetLabel.className = "atri-link-panel-target";
|
|
2788
|
+
const target = document.createElement("input");
|
|
2789
|
+
target.type = "checkbox";
|
|
2790
|
+
target.checked = this.editing ? this.editor.getAttributes("link").target !== "_self" : this.defaultTarget === "_blank";
|
|
2791
|
+
const targetText = document.createElement("span");
|
|
2792
|
+
this.label(targetText, "link.newWindow", "在新窗口打开");
|
|
2793
|
+
targetLabel.append(target, targetText);
|
|
2794
|
+
panel.appendChild(targetLabel);
|
|
2795
|
+
this.targetInput = target;
|
|
2796
|
+
const actions = document.createElement("div");
|
|
2797
|
+
actions.className = "atri-link-panel-actions";
|
|
2798
|
+
if (this.editing) {
|
|
2799
|
+
const remove = document.createElement("button");
|
|
2800
|
+
remove.type = "button";
|
|
2801
|
+
remove.className = "atri-link-panel-remove";
|
|
2802
|
+
this.label(remove, "link.remove", "移除链接");
|
|
2803
|
+
remove.addEventListener("click", () => this.remove());
|
|
2804
|
+
actions.appendChild(remove);
|
|
2805
|
+
}
|
|
2806
|
+
const apply = document.createElement("button");
|
|
2807
|
+
apply.type = "button";
|
|
2808
|
+
apply.className = "atri-link-panel-apply";
|
|
2809
|
+
this.label(apply, "link.apply", "确定");
|
|
2810
|
+
apply.addEventListener("click", () => this.apply());
|
|
2811
|
+
actions.appendChild(apply);
|
|
2812
|
+
panel.appendChild(actions);
|
|
2813
|
+
document.body.appendChild(panel);
|
|
2814
|
+
this.element = panel;
|
|
2815
|
+
copyThemeClasses(this.root, panel);
|
|
2816
|
+
url.focus();
|
|
2817
|
+
url.select();
|
|
2818
|
+
}
|
|
2819
|
+
/** 有链接就整条改(extendMarkRange 把范围摊到整段链接),没链接就看选区:套住它,或者把地址本身当链接文字插进去 */
|
|
2820
|
+
apply() {
|
|
2821
|
+
var _a, _b;
|
|
2822
|
+
const href = normalizeHref(((_a = this.urlInput) == null ? void 0 : _a.value) ?? "");
|
|
2823
|
+
if (!href) return;
|
|
2824
|
+
const target = ((_b = this.targetInput) == null ? void 0 : _b.checked) ? "_blank" : "_self";
|
|
2825
|
+
const { editor } = this;
|
|
2826
|
+
const chain = editor.chain().focus();
|
|
2827
|
+
if (this.editing) {
|
|
2828
|
+
chain.extendMarkRange("link").setLink({ href, target }).run();
|
|
2829
|
+
} else if (editor.state.selection.empty) {
|
|
2830
|
+
chain.insertContent({
|
|
2831
|
+
type: "text",
|
|
2832
|
+
text: href,
|
|
2833
|
+
marks: [{ type: "link", attrs: { href, target } }]
|
|
2834
|
+
}).run();
|
|
2835
|
+
} else {
|
|
2836
|
+
chain.setLink({ href, target }).run();
|
|
2837
|
+
}
|
|
2838
|
+
this.close();
|
|
2839
|
+
}
|
|
2840
|
+
remove() {
|
|
2841
|
+
this.editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
2842
|
+
this.close();
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
function cellPos(state) {
|
|
2846
|
+
const { $from } = state.selection;
|
|
2847
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
2848
|
+
const name = $from.node(depth).type.name;
|
|
2849
|
+
if (name === "tableCell" || name === "tableHeader") return $from.before(depth);
|
|
2850
|
+
}
|
|
2851
|
+
return null;
|
|
2852
|
+
}
|
|
2853
|
+
function cellAlignmentActive(align) {
|
|
2854
|
+
return (editor) => editor.isActive("tableCell", { align }) || editor.isActive("tableHeader", { align });
|
|
2855
|
+
}
|
|
2856
|
+
function buildItems() {
|
|
2857
|
+
const items = [
|
|
2858
|
+
{
|
|
2859
|
+
id: "addRowBefore",
|
|
2860
|
+
icon: icons.tableRowBefore,
|
|
2861
|
+
labelKey: "table.rowBefore",
|
|
2862
|
+
label: "上方插入行",
|
|
2863
|
+
group: 0,
|
|
2864
|
+
command: (editor) => editor.chain().focus().addRowBefore().run()
|
|
2865
|
+
},
|
|
2866
|
+
{
|
|
2867
|
+
id: "addRowAfter",
|
|
2868
|
+
icon: icons.tableRowAfter,
|
|
2869
|
+
labelKey: "table.rowAfter",
|
|
2870
|
+
label: "下方插入行",
|
|
2871
|
+
group: 0,
|
|
2872
|
+
command: (editor) => editor.chain().focus().addRowAfter().run()
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
id: "deleteRow",
|
|
2876
|
+
icon: icons.tableRowDelete,
|
|
2877
|
+
labelKey: "table.deleteRow",
|
|
2878
|
+
label: "删除行",
|
|
2879
|
+
group: 0,
|
|
2880
|
+
command: (editor) => editor.chain().focus().deleteRow().run()
|
|
2881
|
+
},
|
|
2882
|
+
{
|
|
2883
|
+
id: "addColumnBefore",
|
|
2884
|
+
icon: icons.tableColumnBefore,
|
|
2885
|
+
labelKey: "table.columnBefore",
|
|
2886
|
+
label: "左侧插入列",
|
|
2887
|
+
group: 1,
|
|
2888
|
+
command: (editor) => editor.chain().focus().addColumnBefore().run()
|
|
2889
|
+
},
|
|
2890
|
+
{
|
|
2891
|
+
id: "addColumnAfter",
|
|
2892
|
+
icon: icons.tableColumnAfter,
|
|
2893
|
+
labelKey: "table.columnAfter",
|
|
2894
|
+
label: "右侧插入列",
|
|
2895
|
+
group: 1,
|
|
2896
|
+
command: (editor) => editor.chain().focus().addColumnAfter().run()
|
|
2897
|
+
},
|
|
2898
|
+
{
|
|
2899
|
+
id: "deleteColumn",
|
|
2900
|
+
icon: icons.tableColumnDelete,
|
|
2901
|
+
labelKey: "table.deleteColumn",
|
|
2902
|
+
label: "删除列",
|
|
2903
|
+
group: 1,
|
|
2904
|
+
command: (editor) => editor.chain().focus().deleteColumn().run()
|
|
2905
|
+
},
|
|
2906
|
+
{
|
|
2907
|
+
// 合并要有两个以上单元格被框住,单选一个时按不动——禁用而不是隐藏,位置别跳
|
|
2908
|
+
id: "mergeCells",
|
|
2909
|
+
icon: icons.tableMergeCells,
|
|
2910
|
+
labelKey: "table.mergeCells",
|
|
2911
|
+
label: "合并单元格",
|
|
2912
|
+
group: 2,
|
|
2913
|
+
command: (editor) => editor.chain().focus().mergeCells().run(),
|
|
2914
|
+
isDisabled: (editor) => !editor.can().mergeCells()
|
|
2915
|
+
},
|
|
2916
|
+
{
|
|
2917
|
+
id: "splitCell",
|
|
2918
|
+
icon: icons.tableSplitCell,
|
|
2919
|
+
labelKey: "table.splitCell",
|
|
2920
|
+
label: "拆分单元格",
|
|
2921
|
+
group: 2,
|
|
2922
|
+
command: (editor) => editor.chain().focus().splitCell().run(),
|
|
2923
|
+
isDisabled: (editor) => !editor.can().splitCell()
|
|
2924
|
+
},
|
|
2925
|
+
{
|
|
2926
|
+
id: "toggleHeaderRow",
|
|
2927
|
+
icon: icons.tableHeaderRow,
|
|
2928
|
+
labelKey: "table.toggleHeaderRow",
|
|
2929
|
+
label: "切换表头行",
|
|
2930
|
+
group: 2,
|
|
2931
|
+
command: (editor) => editor.chain().focus().toggleHeaderRow().run(),
|
|
2932
|
+
isActive: (editor) => editor.isActive("tableHeader")
|
|
2933
|
+
}
|
|
2934
|
+
];
|
|
2935
|
+
const alignments = [
|
|
2936
|
+
["left", "table.alignLeft", "单元格左对齐"],
|
|
2937
|
+
["center", "table.alignCenter", "单元格居中"],
|
|
2938
|
+
["right", "table.alignRight", "单元格右对齐"]
|
|
2939
|
+
];
|
|
2940
|
+
alignments.forEach(([align, labelKey, label]) => {
|
|
2941
|
+
const suffix = `${align[0].toUpperCase()}${align.slice(1)}`;
|
|
2942
|
+
items.push({
|
|
2943
|
+
id: `cellAlign${suffix}`,
|
|
2944
|
+
icon: icons[`align${suffix}`],
|
|
2945
|
+
labelKey,
|
|
2946
|
+
label,
|
|
2947
|
+
group: 3,
|
|
2948
|
+
command: (editor) => editor.chain().focus().setCellAttribute("align", align).run(),
|
|
2949
|
+
isActive: cellAlignmentActive(align)
|
|
2950
|
+
});
|
|
2951
|
+
});
|
|
2952
|
+
items.push({
|
|
2953
|
+
id: "deleteTable",
|
|
2954
|
+
icon: icons.trash,
|
|
2955
|
+
labelKey: "table.deleteTable",
|
|
2956
|
+
label: "删除表格",
|
|
2957
|
+
group: 4,
|
|
2958
|
+
command: (editor) => editor.chain().focus().deleteTable().run()
|
|
2959
|
+
});
|
|
2960
|
+
return items;
|
|
2961
|
+
}
|
|
2962
|
+
class TableMenu {
|
|
2963
|
+
constructor(options) {
|
|
2964
|
+
var _a;
|
|
2965
|
+
this.element = null;
|
|
2966
|
+
this.dismissed = false;
|
|
2967
|
+
this.sync = () => {
|
|
2968
|
+
const inTable = cellPos(this.editor.state) !== null;
|
|
2969
|
+
if (!inTable) {
|
|
2970
|
+
this.dismissed = false;
|
|
2971
|
+
this.hide();
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
if (this.dismissed || !this.editor.isEditable) {
|
|
2975
|
+
this.hide();
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
this.show();
|
|
2979
|
+
};
|
|
2980
|
+
this.onBlur = () => {
|
|
2981
|
+
this.hide();
|
|
2982
|
+
};
|
|
2983
|
+
this.onScroll = () => {
|
|
2984
|
+
if (this.element) void this.position();
|
|
2985
|
+
};
|
|
2986
|
+
this.dismiss = () => {
|
|
2987
|
+
this.dismissed = true;
|
|
2988
|
+
this.hide();
|
|
2989
|
+
};
|
|
2990
|
+
this.editor = options.editor;
|
|
2991
|
+
this.root = options.root;
|
|
2992
|
+
this.i18n = options.i18n;
|
|
2993
|
+
this.items = buildItems();
|
|
2994
|
+
this.editor.on("update", this.sync);
|
|
2995
|
+
this.editor.on("selectionUpdate", this.sync);
|
|
2996
|
+
this.editor.on("focus", this.sync);
|
|
2997
|
+
this.editor.on("blur", this.onBlur);
|
|
2998
|
+
document.addEventListener("scroll", this.onScroll, true);
|
|
2999
|
+
this.unsubscribeLanguage = (_a = this.i18n) == null ? void 0 : _a.onLanguageChanged(() => this.applyLabels());
|
|
3000
|
+
this.sync();
|
|
3001
|
+
}
|
|
3002
|
+
get isOpen() {
|
|
3003
|
+
return this.element !== null;
|
|
3004
|
+
}
|
|
3005
|
+
destroy() {
|
|
3006
|
+
var _a;
|
|
3007
|
+
this.editor.off("update", this.sync);
|
|
3008
|
+
this.editor.off("selectionUpdate", this.sync);
|
|
3009
|
+
this.editor.off("focus", this.sync);
|
|
3010
|
+
this.editor.off("blur", this.onBlur);
|
|
3011
|
+
document.removeEventListener("scroll", this.onScroll, true);
|
|
3012
|
+
(_a = this.unsubscribeLanguage) == null ? void 0 : _a.call(this);
|
|
3013
|
+
this.unsubscribeLanguage = void 0;
|
|
3014
|
+
this.hide();
|
|
3015
|
+
}
|
|
3016
|
+
t(key, fallback) {
|
|
3017
|
+
return tOrFallback(this.i18n, key, fallback);
|
|
3018
|
+
}
|
|
3019
|
+
show() {
|
|
3020
|
+
if (!this.element) this.build();
|
|
3021
|
+
this.updateStates();
|
|
3022
|
+
void this.position();
|
|
3023
|
+
}
|
|
3024
|
+
hide() {
|
|
3025
|
+
var _a;
|
|
3026
|
+
(_a = this.detachEscape) == null ? void 0 : _a.call(this);
|
|
3027
|
+
this.detachEscape = void 0;
|
|
3028
|
+
if (!this.element) return;
|
|
3029
|
+
this.element.remove();
|
|
3030
|
+
this.element = null;
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Escape 收起菜单。这里不用 floating-panel 的 listenPanelDismiss:那一套还管外点关闭,
|
|
3034
|
+
* 而点外面多半仍落在同一个单元格里,关了立刻被下一次状态更新重新浮出,只会闪
|
|
3035
|
+
*/
|
|
3036
|
+
listenEscape() {
|
|
3037
|
+
const onKeyDown = (event) => {
|
|
3038
|
+
if (event.key !== "Escape") return;
|
|
3039
|
+
event.preventDefault();
|
|
3040
|
+
event.stopPropagation();
|
|
3041
|
+
this.dismiss();
|
|
3042
|
+
};
|
|
3043
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
3044
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
3045
|
+
}
|
|
3046
|
+
build() {
|
|
3047
|
+
const panel = document.createElement("div");
|
|
3048
|
+
panel.className = "atri-table-menu";
|
|
3049
|
+
panel.setAttribute("role", "toolbar");
|
|
3050
|
+
panel.setAttribute("aria-label", this.t("table.menuLabel", "表格操作"));
|
|
3051
|
+
panel.addEventListener("mousedown", (event) => event.preventDefault());
|
|
3052
|
+
let lastGroup = null;
|
|
3053
|
+
this.items.forEach((item) => {
|
|
3054
|
+
if (lastGroup !== null && item.group !== lastGroup) {
|
|
3055
|
+
const separator = document.createElement("div");
|
|
3056
|
+
separator.className = "atri-table-menu-separator";
|
|
3057
|
+
panel.appendChild(separator);
|
|
3058
|
+
}
|
|
3059
|
+
lastGroup = item.group;
|
|
3060
|
+
const button = document.createElement("button");
|
|
3061
|
+
button.type = "button";
|
|
3062
|
+
button.className = "atri-editor-toolbar-btn";
|
|
3063
|
+
button.setAttribute("data-table-item", item.id);
|
|
3064
|
+
button.title = this.t(item.labelKey, item.label);
|
|
3065
|
+
button.innerHTML = item.icon;
|
|
3066
|
+
button.addEventListener("click", (event) => {
|
|
3067
|
+
event.preventDefault();
|
|
3068
|
+
event.stopPropagation();
|
|
3069
|
+
item.command(this.editor);
|
|
3070
|
+
});
|
|
3071
|
+
panel.appendChild(button);
|
|
3072
|
+
});
|
|
3073
|
+
document.body.appendChild(panel);
|
|
3074
|
+
this.element = panel;
|
|
3075
|
+
copyThemeClasses(this.root, panel);
|
|
3076
|
+
this.detachEscape = this.listenEscape();
|
|
3077
|
+
}
|
|
3078
|
+
applyLabels() {
|
|
3079
|
+
const panel = this.element;
|
|
3080
|
+
if (!panel) return;
|
|
3081
|
+
panel.setAttribute("aria-label", this.t("table.menuLabel", "表格操作"));
|
|
3082
|
+
this.items.forEach((item) => {
|
|
3083
|
+
const button = panel.querySelector(`[data-table-item="${item.id}"]`);
|
|
3084
|
+
if (button) button.title = this.t(item.labelKey, item.label);
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
3087
|
+
updateStates() {
|
|
3088
|
+
const panel = this.element;
|
|
3089
|
+
if (!panel) return;
|
|
3090
|
+
this.items.forEach((item) => {
|
|
3091
|
+
var _a, _b;
|
|
3092
|
+
const button = panel.querySelector(`[data-table-item="${item.id}"]`);
|
|
3093
|
+
if (!button) return;
|
|
3094
|
+
button.disabled = ((_a = item.isDisabled) == null ? void 0 : _a.call(item, this.editor)) ?? false;
|
|
3095
|
+
button.classList.toggle("active", ((_b = item.isActive) == null ? void 0 : _b.call(item, this.editor)) ?? false);
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* 当前单元格的虚拟锚点;量不到返回 null,调用方保持面板可见但不改坐标
|
|
3100
|
+
*
|
|
3101
|
+
* 优先锚在单元格盒子上(菜单跟着格子走,比跟着光标稳),拿不到盒子才退回光标坐标
|
|
3102
|
+
*/
|
|
3103
|
+
anchor() {
|
|
3104
|
+
const { view, state } = this.editor;
|
|
3105
|
+
const pos = cellPos(state);
|
|
3106
|
+
if (pos !== null) {
|
|
3107
|
+
try {
|
|
3108
|
+
const dom = view.nodeDOM(pos);
|
|
3109
|
+
if (dom instanceof HTMLElement) {
|
|
3110
|
+
const rect = dom.getBoundingClientRect();
|
|
3111
|
+
if (rect.width || rect.height) return { getBoundingClientRect: () => rect };
|
|
3112
|
+
}
|
|
3113
|
+
} catch {
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
try {
|
|
3117
|
+
const coords = view.coordsAtPos(state.selection.from);
|
|
3118
|
+
return {
|
|
3119
|
+
getBoundingClientRect: () => new DOMRect(coords.left, coords.top, 0, coords.bottom - coords.top)
|
|
3120
|
+
};
|
|
3121
|
+
} catch {
|
|
3122
|
+
return null;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
async position() {
|
|
3126
|
+
const panel = this.element;
|
|
3127
|
+
if (!panel) return;
|
|
3128
|
+
const reference = this.anchor();
|
|
3129
|
+
if (!reference) return;
|
|
3130
|
+
let x;
|
|
3131
|
+
let y;
|
|
3132
|
+
try {
|
|
3133
|
+
({ x, y } = await computePosition(reference, panel, {
|
|
3134
|
+
// 贴在单元格上沿:菜单跟着当前格走,长表格里也不会跑到视口外
|
|
3135
|
+
placement: "top-start",
|
|
3136
|
+
middleware: [offset(6), flip(), shift({ padding: 8 })]
|
|
3137
|
+
}));
|
|
3138
|
+
} catch {
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
if (this.element !== panel) return;
|
|
3142
|
+
panel.style.position = "fixed";
|
|
3143
|
+
panel.style.left = `${x}px`;
|
|
3144
|
+
panel.style.top = `${y}px`;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
2520
3147
|
class MediaStatusStrip {
|
|
2521
3148
|
constructor(runtime, container, i18n) {
|
|
2522
3149
|
const t = (key, fallback, options) => (i18n == null ? void 0 : i18n.tOr(key, fallback, options)) ?? fallback;
|
|
@@ -2983,6 +3610,8 @@ class AtriEditor {
|
|
|
2983
3610
|
this.toolbarContainer = null;
|
|
2984
3611
|
this.aiService = null;
|
|
2985
3612
|
this.aiMenuManager = null;
|
|
3613
|
+
this.linkPanel = null;
|
|
3614
|
+
this.tableMenu = null;
|
|
2986
3615
|
this.mediaStatus = null;
|
|
2987
3616
|
this.pendingViewState = null;
|
|
2988
3617
|
this.bubbleElement = null;
|
|
@@ -3060,6 +3689,7 @@ class AtriEditor {
|
|
|
3060
3689
|
markdown: this.options.markdown,
|
|
3061
3690
|
media: this.options.media,
|
|
3062
3691
|
table: this.options.table,
|
|
3692
|
+
link: this.options.link,
|
|
3063
3693
|
mediaRuntime: this.mediaRuntime ?? void 0,
|
|
3064
3694
|
bubbleElement: this.ensureBubbleElement(this.options.toolbar),
|
|
3065
3695
|
onCreate: () => {
|
|
@@ -3083,19 +3713,39 @@ class AtriEditor {
|
|
|
3083
3713
|
}
|
|
3084
3714
|
});
|
|
3085
3715
|
}
|
|
3086
|
-
/** 初始化依赖编辑器实例的子系统:Markdown
|
|
3716
|
+
/** 初始化依赖编辑器实例的子系统:Markdown 服务、链接浮层、工具栏、表格菜单、AI 服务,重建后同样要重新走一遍 */
|
|
3087
3717
|
setupSubsystems() {
|
|
3718
|
+
var _a, _b;
|
|
3088
3719
|
this.markdownService = new MarkdownService(this.editor, this.options.markdown);
|
|
3720
|
+
this.linkPanel = this.editor.schema.marks.link ? new LinkPanel({
|
|
3721
|
+
editor: this.editor,
|
|
3722
|
+
root: this.container,
|
|
3723
|
+
i18n: this.i18nManager,
|
|
3724
|
+
openOnClick: ((_a = this.options.link) == null ? void 0 : _a.openOnClick) ?? false,
|
|
3725
|
+
target: ((_b = this.options.link) == null ? void 0 : _b.target) ?? "_blank",
|
|
3726
|
+
// 开合不改文档,工具栏等不到 transaction,按钮的按下态得主动推一次
|
|
3727
|
+
onOpenChange: () => {
|
|
3728
|
+
var _a2;
|
|
3729
|
+
return (_a2 = this.toolbarManager) == null ? void 0 : _a2.updateButtonStates();
|
|
3730
|
+
}
|
|
3731
|
+
}) : null;
|
|
3089
3732
|
if (this.toolbarContainer && this.options.toolbar !== false) {
|
|
3090
3733
|
this.toolbarManager = new ToolbarManager(
|
|
3091
3734
|
this.editor,
|
|
3092
3735
|
this.toolbarContainer,
|
|
3093
3736
|
this.options.toolbar,
|
|
3094
3737
|
this.i18nManager,
|
|
3095
|
-
this.mediaRuntime
|
|
3738
|
+
this.mediaRuntime,
|
|
3739
|
+
this.linkPanel
|
|
3096
3740
|
);
|
|
3097
3741
|
if (this.bubbleElement) this.toolbarManager.attachBubbleToolbar(this.bubbleElement);
|
|
3098
3742
|
}
|
|
3743
|
+
const tableConfig = resolveTableConfig(this.options.table);
|
|
3744
|
+
this.tableMenu = tableConfig.menu && canInsertTable(this.editor) ? new TableMenu({
|
|
3745
|
+
editor: this.editor,
|
|
3746
|
+
root: this.container,
|
|
3747
|
+
i18n: this.i18nManager
|
|
3748
|
+
}) : null;
|
|
3099
3749
|
if (this.options.ai) {
|
|
3100
3750
|
this.initAI(this.options.ai);
|
|
3101
3751
|
}
|
|
@@ -3105,14 +3755,16 @@ class AtriEditor {
|
|
|
3105
3755
|
* 会整体替换编辑器视图,因此内容与选区都要手动恢复
|
|
3106
3756
|
*/
|
|
3107
3757
|
recreateEditor() {
|
|
3108
|
-
var _a, _b;
|
|
3758
|
+
var _a, _b, _c, _d;
|
|
3109
3759
|
const previousEditor = this.coreEditor.getEditor();
|
|
3110
3760
|
const currentContent = this.coreEditor.getHTML();
|
|
3111
3761
|
const { from, to } = previousEditor.state.selection;
|
|
3112
3762
|
this.pendingViewState = { from, to };
|
|
3113
|
-
this.coreEditor.destroy();
|
|
3114
3763
|
(_a = this.toolbarManager) == null ? void 0 : _a.destroy();
|
|
3115
|
-
(_b = this.
|
|
3764
|
+
(_b = this.tableMenu) == null ? void 0 : _b.destroy();
|
|
3765
|
+
(_c = this.linkPanel) == null ? void 0 : _c.destroy();
|
|
3766
|
+
(_d = this.aiMenuManager) == null ? void 0 : _d.destroy();
|
|
3767
|
+
this.coreEditor.destroy();
|
|
3116
3768
|
const editorElement = this.container.querySelector(
|
|
3117
3769
|
".atri-editor-content-wrapper"
|
|
3118
3770
|
);
|
|
@@ -3396,12 +4048,14 @@ class AtriEditor {
|
|
|
3396
4048
|
* 销毁编辑器
|
|
3397
4049
|
*/
|
|
3398
4050
|
destroy() {
|
|
3399
|
-
var _a, _b, _c, _d;
|
|
4051
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3400
4052
|
(_a = this.toolbarManager) == null ? void 0 : _a.destroy();
|
|
3401
|
-
(_b = this.
|
|
4053
|
+
(_b = this.tableMenu) == null ? void 0 : _b.destroy();
|
|
4054
|
+
(_c = this.linkPanel) == null ? void 0 : _c.destroy();
|
|
4055
|
+
(_d = this.aiMenuManager) == null ? void 0 : _d.destroy();
|
|
3402
4056
|
this.coreEditor.destroy();
|
|
3403
|
-
(
|
|
3404
|
-
(
|
|
4057
|
+
(_e = this.mediaStatus) == null ? void 0 : _e.destroy();
|
|
4058
|
+
(_f = this.mediaRuntime) == null ? void 0 : _f.destroy();
|
|
3405
4059
|
this.bubbleElement = null;
|
|
3406
4060
|
this.container.remove();
|
|
3407
4061
|
}
|