@atri-editor/core 0.1.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 +36 -0
- package/README.md +5 -5
- package/dist/AtriEditor.d.ts +23 -5
- package/dist/AtriEditor.d.ts.map +1 -1
- package/dist/AtriEditorElement.d.ts +5 -1
- package/dist/AtriEditorElement.d.ts.map +1 -1
- package/dist/ai/AICommandMenu.d.ts.map +1 -1
- package/dist/core/CoreEditor.d.ts +5 -1
- package/dist/core/CoreEditor.d.ts.map +1 -1
- package/dist/core/ExtensionManager.d.ts +2 -14
- package/dist/core/ExtensionManager.d.ts.map +1 -1
- package/dist/core/I18nManager.d.ts +9 -0
- 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/TableGridPanel.d.ts +41 -0
- package/dist/core/TableGridPanel.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/ThemeManager.d.ts +3 -10
- package/dist/core/ThemeManager.d.ts.map +1 -1
- package/dist/core/ToolbarManager.d.ts +15 -2
- package/dist/core/ToolbarManager.d.ts.map +1 -1
- package/dist/core/floating-panel.d.ts +20 -0
- package/dist/core/floating-panel.d.ts.map +1 -0
- package/dist/core/icons.d.ts.map +1 -1
- package/dist/extensions/Attachment.d.ts.map +1 -1
- package/dist/extensions/AttachmentLink.d.ts.map +1 -1
- package/dist/extensions/MediaFiles.d.ts.map +1 -1
- package/dist/extensions/attachment-markdown.d.ts +22 -0
- package/dist/extensions/attachment-markdown.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 +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1131 -350
- package/dist/index.js.map +1 -1
- package/dist/media/InsertPanel.d.ts +5 -6
- package/dist/media/InsertPanel.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 +183 -0
- package/dist/types/ai.d.ts +2 -0
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/editor.d.ts +52 -7
- package/dist/types/editor.d.ts.map +1 -1
- package/dist/types/extension.d.ts +16 -34
- package/dist/types/extension.d.ts.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/dom.d.ts +3 -2
- package/dist/utils/dom.d.ts.map +1 -1
- package/dist/utils/table.d.ts +24 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/package.json +24 -5
- package/dist/types/events.d.ts +0 -26
- package/dist/types/events.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -4,11 +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 { TaskList, TaskItem } from "@tiptap/extension-list";
|
|
8
|
+
import { TableRow, TableHeader, TableCell, Table } from "@tiptap/extension-table";
|
|
7
9
|
import Image from "@tiptap/extension-image";
|
|
8
10
|
import { NodeSelection, Plugin, PluginKey } from "@tiptap/pm/state";
|
|
9
11
|
import { Node as Node$1, DOMParser } from "@tiptap/pm/model";
|
|
10
12
|
import i18next from "i18next";
|
|
11
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
|
+
}
|
|
12
41
|
function mediaInsertTarget(selection) {
|
|
13
42
|
if (selection instanceof NodeSelection && (selection.node.type.name === "image" || selection.node.type.name === "attachment" || selection.node.type.name === "attachmentLink")) {
|
|
14
43
|
return selection.to;
|
|
@@ -196,7 +225,50 @@ function attachmentAttributes() {
|
|
|
196
225
|
}
|
|
197
226
|
};
|
|
198
227
|
}
|
|
199
|
-
|
|
228
|
+
function makeAttachmentMarkdown(options) {
|
|
229
|
+
const { name, level, nodeName } = options;
|
|
230
|
+
const directive = `!${name}`;
|
|
231
|
+
const trailing = level === "block" ? "(?:\\n|$)" : "";
|
|
232
|
+
const pattern = new RegExp(
|
|
233
|
+
`^${directive.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\[([^\\]]*)\\]\\(\\s*([^\\s)]+)(?:\\s+"([^"]*)")?\\s*\\)${trailing}`
|
|
234
|
+
);
|
|
235
|
+
return {
|
|
236
|
+
markdownTokenName: name,
|
|
237
|
+
markdownTokenizer: {
|
|
238
|
+
name,
|
|
239
|
+
level,
|
|
240
|
+
start: (src) => src.indexOf(`${directive}[`),
|
|
241
|
+
tokenize: (src) => {
|
|
242
|
+
const match = pattern.exec(src);
|
|
243
|
+
if (!match) return void 0;
|
|
244
|
+
return {
|
|
245
|
+
type: name,
|
|
246
|
+
raw: match[0],
|
|
247
|
+
text: match[1] ?? "",
|
|
248
|
+
href: match[2] ?? "",
|
|
249
|
+
title: match[3]
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
parseMarkdown: (token, helpers) => helpers.createNode(
|
|
254
|
+
nodeName,
|
|
255
|
+
{
|
|
256
|
+
src: token.href,
|
|
257
|
+
name: token.text || null,
|
|
258
|
+
size: parseFileSize(token.title) ?? null,
|
|
259
|
+
mime: null
|
|
260
|
+
},
|
|
261
|
+
[]
|
|
262
|
+
),
|
|
263
|
+
renderMarkdown: (node) => {
|
|
264
|
+
var _a, _b, _c;
|
|
265
|
+
const src = ((_a = node.attrs) == null ? void 0 : _a.src) ?? "";
|
|
266
|
+
const label = ((_b = node.attrs) == null ? void 0 : _b.name) ?? "";
|
|
267
|
+
const size = (_c = node.attrs) == null ? void 0 : _c.size;
|
|
268
|
+
return size ? `${directive}[${label}](${src} "${formatFileSize(size)}")` : `${directive}[${label}](${src})`;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
200
272
|
const FORM_TYPES = {
|
|
201
273
|
card: "attachment",
|
|
202
274
|
link: "attachmentLink"
|
|
@@ -351,42 +423,12 @@ const Attachment = Node.create({
|
|
|
351
423
|
})
|
|
352
424
|
];
|
|
353
425
|
},
|
|
354
|
-
|
|
355
|
-
markdownTokenizer: {
|
|
426
|
+
...makeAttachmentMarkdown({
|
|
356
427
|
name: "file",
|
|
357
428
|
level: "block",
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const match = markdownPattern$1.exec(src);
|
|
361
|
-
if (!match) return void 0;
|
|
362
|
-
return {
|
|
363
|
-
type: "file",
|
|
364
|
-
raw: match[0],
|
|
365
|
-
text: match[1] ?? "",
|
|
366
|
-
href: match[2] ?? "",
|
|
367
|
-
title: match[3]
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
},
|
|
371
|
-
parseMarkdown: (token, helpers) => helpers.createNode(
|
|
372
|
-
"attachment",
|
|
373
|
-
{
|
|
374
|
-
src: token.href,
|
|
375
|
-
name: token.text || null,
|
|
376
|
-
size: parseFileSize(token.title) ?? null,
|
|
377
|
-
mime: null
|
|
378
|
-
},
|
|
379
|
-
[]
|
|
380
|
-
),
|
|
381
|
-
renderMarkdown: (node) => {
|
|
382
|
-
var _a, _b, _c;
|
|
383
|
-
const src = ((_a = node.attrs) == null ? void 0 : _a.src) ?? "";
|
|
384
|
-
const name = ((_b = node.attrs) == null ? void 0 : _b.name) ?? "";
|
|
385
|
-
const size = (_c = node.attrs) == null ? void 0 : _c.size;
|
|
386
|
-
return size ? `!file[${name}](${src} "${formatFileSize(size)}")` : `!file[${name}](${src})`;
|
|
387
|
-
}
|
|
429
|
+
nodeName: "attachment"
|
|
430
|
+
})
|
|
388
431
|
});
|
|
389
|
-
const markdownPattern = /^!filelink\[([^\]]*)\]\(\s*([^\s)]+)(?:\s+"([^"]*)")?\s*\)/;
|
|
390
432
|
const AttachmentLink = Node.create({
|
|
391
433
|
name: "attachmentLink",
|
|
392
434
|
inline: true,
|
|
@@ -429,42 +471,30 @@ const AttachmentLink = Node.create({
|
|
|
429
471
|
setAttachmentLink: (options) => (props) => insertAttachmentContent(props, this.name, options)
|
|
430
472
|
};
|
|
431
473
|
},
|
|
432
|
-
|
|
433
|
-
markdownTokenizer: {
|
|
474
|
+
...makeAttachmentMarkdown({
|
|
434
475
|
name: "filelink",
|
|
435
476
|
level: "inline",
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
href: match[2] ?? "",
|
|
445
|
-
title: match[3]
|
|
446
|
-
};
|
|
477
|
+
nodeName: "attachmentLink"
|
|
478
|
+
})
|
|
479
|
+
});
|
|
480
|
+
function resolveElement(element) {
|
|
481
|
+
if (typeof element === "string") {
|
|
482
|
+
const el = document.querySelector(element);
|
|
483
|
+
if (!el) {
|
|
484
|
+
throw new Error(`Element not found: ${element}`);
|
|
447
485
|
}
|
|
448
|
-
|
|
449
|
-
parseMarkdown: (token, helpers) => helpers.createNode(
|
|
450
|
-
"attachmentLink",
|
|
451
|
-
{
|
|
452
|
-
src: token.href,
|
|
453
|
-
name: token.text || null,
|
|
454
|
-
size: parseFileSize(token.title) ?? null,
|
|
455
|
-
mime: null
|
|
456
|
-
},
|
|
457
|
-
[]
|
|
458
|
-
),
|
|
459
|
-
renderMarkdown: (node) => {
|
|
460
|
-
var _a, _b, _c;
|
|
461
|
-
const src = ((_a = node.attrs) == null ? void 0 : _a.src) ?? "";
|
|
462
|
-
const name = ((_b = node.attrs) == null ? void 0 : _b.name) ?? "";
|
|
463
|
-
const size = (_c = node.attrs) == null ? void 0 : _c.size;
|
|
464
|
-
return size ? `!filelink[${name}](${src} "${formatFileSize(size)}")` : `!filelink[${name}](${src})`;
|
|
486
|
+
return el;
|
|
465
487
|
}
|
|
466
|
-
|
|
467
|
-
|
|
488
|
+
return element;
|
|
489
|
+
}
|
|
490
|
+
function createContainer(className) {
|
|
491
|
+
const div = document.createElement("div");
|
|
492
|
+
if (className) {
|
|
493
|
+
div.className = className;
|
|
494
|
+
}
|
|
495
|
+
return div;
|
|
496
|
+
}
|
|
497
|
+
function filesOf(transfer) {
|
|
468
498
|
if (!transfer) return [];
|
|
469
499
|
return Array.from(transfer.files);
|
|
470
500
|
}
|
|
@@ -486,7 +516,7 @@ const MediaFiles = Extension.create({
|
|
|
486
516
|
props: {
|
|
487
517
|
/** 先于 PM 的剪贴板解析:带文件的粘贴(截图)交给上传管线,纯文本照常走默认链路 */
|
|
488
518
|
handlePaste: (view, event) => {
|
|
489
|
-
const files = filesOf
|
|
519
|
+
const files = filesOf(event.clipboardData);
|
|
490
520
|
if (!files.length) return false;
|
|
491
521
|
event.preventDefault();
|
|
492
522
|
if (view.editable) void runtime.handleFiles(files);
|
|
@@ -494,7 +524,7 @@ const MediaFiles = Extension.create({
|
|
|
494
524
|
},
|
|
495
525
|
handleDrop: (view, event) => {
|
|
496
526
|
const dragEvent = event;
|
|
497
|
-
const files = filesOf
|
|
527
|
+
const files = filesOf(dragEvent.dataTransfer);
|
|
498
528
|
if (!files.length) return false;
|
|
499
529
|
dragEvent.preventDefault();
|
|
500
530
|
if (!view.editable) return true;
|
|
@@ -533,11 +563,17 @@ function createMediaExtensions(media, runtime) {
|
|
|
533
563
|
];
|
|
534
564
|
}
|
|
535
565
|
function pickUrl(body) {
|
|
536
|
-
var _a;
|
|
537
566
|
if (!body || typeof body !== "object") return void 0;
|
|
538
567
|
const record = body;
|
|
539
|
-
const
|
|
540
|
-
const
|
|
568
|
+
const nested = record.data;
|
|
569
|
+
const candidates = [
|
|
570
|
+
record.url,
|
|
571
|
+
record.location,
|
|
572
|
+
nested && typeof nested === "object" ? nested.url : void 0
|
|
573
|
+
];
|
|
574
|
+
const url = candidates.find(
|
|
575
|
+
(value) => typeof value === "string" && value !== ""
|
|
576
|
+
);
|
|
541
577
|
return url;
|
|
542
578
|
}
|
|
543
579
|
function uploadViaEndpoint(config, file, context) {
|
|
@@ -1025,6 +1061,11 @@ function shouldShowBubbleMenu(props) {
|
|
|
1025
1061
|
if (!group) return false;
|
|
1026
1062
|
return !!element.querySelector(`[data-atri-bubble-group="${group}"] [data-toolbar-item]`);
|
|
1027
1063
|
}
|
|
1064
|
+
const LINK_HTML_ATTRIBUTES = {
|
|
1065
|
+
target: "_blank",
|
|
1066
|
+
rel: "noopener noreferrer nofollow",
|
|
1067
|
+
class: null
|
|
1068
|
+
};
|
|
1028
1069
|
class CoreEditor {
|
|
1029
1070
|
constructor(config) {
|
|
1030
1071
|
this.config = config;
|
|
@@ -1033,11 +1074,22 @@ class CoreEditor {
|
|
|
1033
1074
|
this.editor = this.createEditor();
|
|
1034
1075
|
}
|
|
1035
1076
|
createEditor() {
|
|
1036
|
-
var _a, _b, _c, _d, _e;
|
|
1077
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1037
1078
|
const { element, content, contentFormat, editable, extensions } = this.config;
|
|
1038
1079
|
const editorExtensions = [
|
|
1039
1080
|
StarterKit.configure({
|
|
1040
|
-
// 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
|
+
}
|
|
1041
1093
|
})
|
|
1042
1094
|
];
|
|
1043
1095
|
editorExtensions.push(
|
|
@@ -1050,17 +1102,20 @@ class CoreEditor {
|
|
|
1050
1102
|
types: ["heading", "paragraph"]
|
|
1051
1103
|
})
|
|
1052
1104
|
);
|
|
1053
|
-
const markdownEnabled = ((
|
|
1105
|
+
const markdownEnabled = ((_d = this.config.markdown) == null ? void 0 : _d.enabled) !== false;
|
|
1054
1106
|
if (markdownEnabled) {
|
|
1055
1107
|
editorExtensions.push(
|
|
1056
1108
|
Markdown.configure({
|
|
1057
|
-
indentation: (
|
|
1058
|
-
markedOptions: (
|
|
1109
|
+
indentation: (_e = this.config.markdown) == null ? void 0 : _e.indentation,
|
|
1110
|
+
markedOptions: (_f = this.config.markdown) == null ? void 0 : _f.markedOptions
|
|
1059
1111
|
})
|
|
1060
1112
|
);
|
|
1061
1113
|
}
|
|
1062
1114
|
const contentType = contentFormat === "markdown" && !markdownEnabled ? void 0 : contentFormat;
|
|
1063
1115
|
editorExtensions.push(...createMediaExtensions(this.config.media, this.mediaRuntime));
|
|
1116
|
+
if (this.config.table !== false) {
|
|
1117
|
+
editorExtensions.push(...createTableExtensions(resolveTableConfig(this.config.table)));
|
|
1118
|
+
}
|
|
1064
1119
|
if (this.config.bubbleElement) {
|
|
1065
1120
|
editorExtensions.push(
|
|
1066
1121
|
BubbleMenu.configure({
|
|
@@ -1078,7 +1133,8 @@ class CoreEditor {
|
|
|
1078
1133
|
})
|
|
1079
1134
|
);
|
|
1080
1135
|
}
|
|
1081
|
-
|
|
1136
|
+
editorExtensions.push(TaskList, TaskItem);
|
|
1137
|
+
const enableInputRules = ((_g = this.config.markdown) == null ? void 0 : _g.shortcuts) !== false;
|
|
1082
1138
|
if (extensions) {
|
|
1083
1139
|
editorExtensions.push(...extensions);
|
|
1084
1140
|
}
|
|
@@ -1115,7 +1171,7 @@ class CoreEditor {
|
|
|
1115
1171
|
(_b2 = (_a2 = this.config).onDestroy) == null ? void 0 : _b2.call(_a2);
|
|
1116
1172
|
}
|
|
1117
1173
|
});
|
|
1118
|
-
(
|
|
1174
|
+
(_h = this.mediaRuntime) == null ? void 0 : _h.bind(editor);
|
|
1119
1175
|
return editor;
|
|
1120
1176
|
}
|
|
1121
1177
|
/**
|
|
@@ -1137,9 +1193,14 @@ class CoreEditor {
|
|
|
1137
1193
|
try {
|
|
1138
1194
|
return this.editor.getHTML();
|
|
1139
1195
|
} catch (err) {
|
|
1140
|
-
console.error(
|
|
1141
|
-
|
|
1142
|
-
|
|
1196
|
+
console.error(
|
|
1197
|
+
"[CoreEditor] getHTML() failed:",
|
|
1198
|
+
err,
|
|
1199
|
+
"\nSchema nodes:",
|
|
1200
|
+
Object.keys(this.editor.schema.nodes),
|
|
1201
|
+
"\nSchema marks:",
|
|
1202
|
+
Object.keys(this.editor.schema.marks)
|
|
1203
|
+
);
|
|
1143
1204
|
throw err;
|
|
1144
1205
|
}
|
|
1145
1206
|
}
|
|
@@ -1372,23 +1433,13 @@ class ThemeManager {
|
|
|
1372
1433
|
this.applyTheme();
|
|
1373
1434
|
}
|
|
1374
1435
|
/**
|
|
1375
|
-
*
|
|
1436
|
+
* 应用当前主题:内置主题与自定义主题走同一条 class 规则,只是名字不同
|
|
1376
1437
|
*/
|
|
1377
1438
|
applyTheme() {
|
|
1378
1439
|
this.container.classList.remove("atri-theme-light", "atri-theme-dark");
|
|
1379
|
-
|
|
1380
|
-
this.container.classList.add(`atri-theme-${this.currentTheme}`);
|
|
1381
|
-
} else {
|
|
1382
|
-
this.container.classList.add(`atri-theme-${this.currentTheme}`);
|
|
1383
|
-
}
|
|
1440
|
+
this.container.classList.add(`atri-theme-${this.currentTheme}`);
|
|
1384
1441
|
this.container.setAttribute("data-atri-theme", this.currentTheme);
|
|
1385
1442
|
}
|
|
1386
|
-
/**
|
|
1387
|
-
* 获取当前主题
|
|
1388
|
-
*/
|
|
1389
|
-
getTheme() {
|
|
1390
|
-
return this.currentTheme;
|
|
1391
|
-
}
|
|
1392
1443
|
/**
|
|
1393
1444
|
* 设置主题
|
|
1394
1445
|
*/
|
|
@@ -1408,12 +1459,6 @@ class ThemeManager {
|
|
|
1408
1459
|
this.setTheme("light");
|
|
1409
1460
|
}
|
|
1410
1461
|
}
|
|
1411
|
-
/**
|
|
1412
|
-
* 是否为暗色主题
|
|
1413
|
-
*/
|
|
1414
|
-
isDark() {
|
|
1415
|
-
return this.currentTheme === "dark";
|
|
1416
|
-
}
|
|
1417
1462
|
}
|
|
1418
1463
|
const zhResources = {
|
|
1419
1464
|
editor: {
|
|
@@ -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",
|
|
@@ -1540,6 +1633,13 @@ class I18nManager {
|
|
|
1540
1633
|
t(key, options) {
|
|
1541
1634
|
return this.instance.t(key, options);
|
|
1542
1635
|
}
|
|
1636
|
+
/**
|
|
1637
|
+
* 翻译,词条缺失时回退到 fallback:i18next 缺词条时原样返回 key
|
|
1638
|
+
*/
|
|
1639
|
+
tOr(key, fallback, options) {
|
|
1640
|
+
const translated = this.t(key, options);
|
|
1641
|
+
return translated && translated !== key ? translated : fallback;
|
|
1642
|
+
}
|
|
1543
1643
|
/**
|
|
1544
1644
|
* 切换语言
|
|
1545
1645
|
*/
|
|
@@ -1560,6 +1660,9 @@ class I18nManager {
|
|
|
1560
1660
|
return this.instance.language || "zh";
|
|
1561
1661
|
}
|
|
1562
1662
|
}
|
|
1663
|
+
function tOrFallback(i18n, key, fallback) {
|
|
1664
|
+
return (i18n == null ? void 0 : i18n.tOr(key, fallback)) ?? fallback;
|
|
1665
|
+
}
|
|
1563
1666
|
class ExtensionManager {
|
|
1564
1667
|
constructor() {
|
|
1565
1668
|
this.extensions = /* @__PURE__ */ new Map();
|
|
@@ -1568,19 +1671,13 @@ class ExtensionManager {
|
|
|
1568
1671
|
/**
|
|
1569
1672
|
* 注册扩展
|
|
1570
1673
|
*/
|
|
1571
|
-
register(extension
|
|
1674
|
+
register(extension) {
|
|
1572
1675
|
const name = extension.name;
|
|
1573
1676
|
if (this.extensions.has(name)) {
|
|
1574
1677
|
console.warn(`Extension "${name}" already registered, overwriting.`);
|
|
1575
1678
|
}
|
|
1576
1679
|
this.extensions.set(name, extension);
|
|
1577
1680
|
}
|
|
1578
|
-
/**
|
|
1579
|
-
* 批量注册扩展
|
|
1580
|
-
*/
|
|
1581
|
-
registerAll(extensions) {
|
|
1582
|
-
extensions.forEach((ext) => this.register(ext));
|
|
1583
|
-
}
|
|
1584
1681
|
/**
|
|
1585
1682
|
* 注册自定义 NodeView
|
|
1586
1683
|
*/
|
|
@@ -1612,12 +1709,7 @@ class ExtensionManager {
|
|
|
1612
1709
|
// 由 Tiptap 的 MarkdownManager 按节点名调用,签名见 @tiptap/core NodeConfig.renderMarkdown
|
|
1613
1710
|
renderMarkdown: config.markdownSerialize ? (node) => config.markdownSerialize(node) : void 0
|
|
1614
1711
|
});
|
|
1615
|
-
this.register(nodeExtension
|
|
1616
|
-
name: config.name,
|
|
1617
|
-
version: "1.0.0",
|
|
1618
|
-
category: "custom",
|
|
1619
|
-
description: config.description
|
|
1620
|
-
});
|
|
1712
|
+
this.register(nodeExtension);
|
|
1621
1713
|
}
|
|
1622
1714
|
/**
|
|
1623
1715
|
* 批量注册 NodeView
|
|
@@ -1631,13 +1723,6 @@ class ExtensionManager {
|
|
|
1631
1723
|
get(name) {
|
|
1632
1724
|
return this.extensions.get(name);
|
|
1633
1725
|
}
|
|
1634
|
-
/**
|
|
1635
|
-
* 卸载扩展
|
|
1636
|
-
*/
|
|
1637
|
-
unregister(name) {
|
|
1638
|
-
this.nodeViews.delete(name);
|
|
1639
|
-
return this.extensions.delete(name);
|
|
1640
|
-
}
|
|
1641
1726
|
/**
|
|
1642
1727
|
* 获取所有扩展
|
|
1643
1728
|
*/
|
|
@@ -1656,28 +1741,69 @@ class ExtensionManager {
|
|
|
1656
1741
|
has(name) {
|
|
1657
1742
|
return this.extensions.has(name);
|
|
1658
1743
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1744
|
+
}
|
|
1745
|
+
function positionPanel(trigger, panel, isCurrent) {
|
|
1746
|
+
let stopAutoUpdate = null;
|
|
1747
|
+
const update = async () => {
|
|
1748
|
+
if (!isCurrent()) return;
|
|
1749
|
+
try {
|
|
1750
|
+
const { x, y } = await computePosition(trigger, panel, {
|
|
1751
|
+
placement: "bottom-start",
|
|
1752
|
+
middleware: [offset(6), flip(), shift({ padding: 8 })]
|
|
1753
|
+
});
|
|
1754
|
+
if (!isCurrent()) return;
|
|
1755
|
+
panel.style.position = "fixed";
|
|
1756
|
+
panel.style.left = `${x}px`;
|
|
1757
|
+
panel.style.top = `${y}px`;
|
|
1758
|
+
} catch {
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
void update().then(() => {
|
|
1762
|
+
if (isCurrent()) {
|
|
1763
|
+
stopAutoUpdate = autoUpdate(trigger, panel, () => void update());
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
return {
|
|
1767
|
+
stop: () => {
|
|
1768
|
+
stopAutoUpdate == null ? void 0 : stopAutoUpdate();
|
|
1769
|
+
stopAutoUpdate = null;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
function listenPanelDismiss(trigger, panel, onClose) {
|
|
1774
|
+
const onPointerDown = (event) => {
|
|
1775
|
+
const target = event.target;
|
|
1776
|
+
if (!target || panel.contains(target) || trigger.contains(target)) return;
|
|
1777
|
+
onClose();
|
|
1778
|
+
};
|
|
1779
|
+
const onKeyDown = (event) => {
|
|
1780
|
+
if (event.key !== "Escape") return;
|
|
1781
|
+
event.preventDefault();
|
|
1782
|
+
event.stopPropagation();
|
|
1783
|
+
onClose();
|
|
1784
|
+
};
|
|
1785
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
1786
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1787
|
+
return () => {
|
|
1788
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
1789
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
function copyThemeClasses(source, panel) {
|
|
1793
|
+
const root = source == null ? void 0 : source.closest(".atri-editor");
|
|
1794
|
+
const themes = Array.from((root == null ? void 0 : root.classList) ?? []).filter((name) => name.startsWith("atri-theme-"));
|
|
1795
|
+
panel.classList.add(...themes);
|
|
1666
1796
|
}
|
|
1667
1797
|
const FIELDS = {
|
|
1668
1798
|
image: { url: true, alt: true, kind: "image" },
|
|
1669
1799
|
attachment: { url: false, alt: false, kind: "attachment" }
|
|
1670
1800
|
};
|
|
1671
|
-
function filesOf(transfer) {
|
|
1672
|
-
if (!transfer) return [];
|
|
1673
|
-
return Array.from(transfer.files);
|
|
1674
|
-
}
|
|
1675
1801
|
class InsertPanel {
|
|
1676
1802
|
constructor(options) {
|
|
1677
1803
|
var _a;
|
|
1678
1804
|
this.element = null;
|
|
1679
1805
|
this.mode = null;
|
|
1680
|
-
this.
|
|
1806
|
+
this.floating = null;
|
|
1681
1807
|
this.detachDismiss = null;
|
|
1682
1808
|
this.labelled = [];
|
|
1683
1809
|
this.editor = options.editor;
|
|
@@ -1709,8 +1835,8 @@ class InsertPanel {
|
|
|
1709
1835
|
var _a, _b, _c, _d;
|
|
1710
1836
|
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
1711
1837
|
this.detachDismiss = null;
|
|
1712
|
-
(_b = this.
|
|
1713
|
-
this.
|
|
1838
|
+
(_b = this.floating) == null ? void 0 : _b.stop();
|
|
1839
|
+
this.floating = null;
|
|
1714
1840
|
(_c = this.element) == null ? void 0 : _c.remove();
|
|
1715
1841
|
this.element = null;
|
|
1716
1842
|
this.labelled = [];
|
|
@@ -1725,9 +1851,7 @@ class InsertPanel {
|
|
|
1725
1851
|
this.unsubscribeLanguage = void 0;
|
|
1726
1852
|
}
|
|
1727
1853
|
t(key, fallback) {
|
|
1728
|
-
|
|
1729
|
-
const translated = (_a = this.i18n) == null ? void 0 : _a.t(key);
|
|
1730
|
-
return translated && translated !== key ? translated : fallback;
|
|
1854
|
+
return tOrFallback(this.i18n, key, fallback);
|
|
1731
1855
|
}
|
|
1732
1856
|
/**
|
|
1733
1857
|
* 登记一处随语言重画的文案:input 画到 placeholder,其他画成文字
|
|
@@ -1813,19 +1937,9 @@ class InsertPanel {
|
|
|
1813
1937
|
}
|
|
1814
1938
|
document.body.appendChild(panel);
|
|
1815
1939
|
this.element = panel;
|
|
1816
|
-
this.
|
|
1940
|
+
copyThemeClasses(this.editor.view.dom, panel);
|
|
1817
1941
|
(urlInput ?? altInput ?? browse).focus();
|
|
1818
1942
|
}
|
|
1819
|
-
/**
|
|
1820
|
-
* 主题变量写在 .atri-editor 上,浮层挂在 body 上够不着,只能把主题类复制一份
|
|
1821
|
-
*/
|
|
1822
|
-
applyTheme(panel) {
|
|
1823
|
-
const root = this.editor.view.dom.closest(".atri-editor");
|
|
1824
|
-
const themes = Array.from((root == null ? void 0 : root.classList) ?? []).filter(
|
|
1825
|
-
(name) => name.startsWith("atri-theme-")
|
|
1826
|
-
);
|
|
1827
|
-
panel.classList.add(...themes);
|
|
1828
|
-
}
|
|
1829
1943
|
/**
|
|
1830
1944
|
* 外链图片当场就插,不进上传队列:地址本来就是可用的
|
|
1831
1945
|
*/
|
|
@@ -1840,50 +1954,151 @@ class InsertPanel {
|
|
|
1840
1954
|
void this.runtime.handleFiles(files, { kind });
|
|
1841
1955
|
this.close();
|
|
1842
1956
|
}
|
|
1843
|
-
|
|
1957
|
+
position(anchor) {
|
|
1844
1958
|
const panel = this.element;
|
|
1845
1959
|
if (!panel) return;
|
|
1846
|
-
|
|
1847
|
-
if (this.element !== panel) return;
|
|
1848
|
-
try {
|
|
1849
|
-
const { x, y } = await computePosition(anchor, panel, {
|
|
1850
|
-
placement: "bottom-start",
|
|
1851
|
-
middleware: [offset(6), flip(), shift({ padding: 8 })]
|
|
1852
|
-
});
|
|
1853
|
-
if (this.element !== panel) return;
|
|
1854
|
-
panel.style.position = "fixed";
|
|
1855
|
-
panel.style.left = `${x}px`;
|
|
1856
|
-
panel.style.top = `${y}px`;
|
|
1857
|
-
} catch {
|
|
1858
|
-
}
|
|
1859
|
-
};
|
|
1860
|
-
await update();
|
|
1861
|
-
if (this.element === panel) {
|
|
1862
|
-
this.stopAutoUpdate = autoUpdate(anchor, panel, () => void update());
|
|
1863
|
-
}
|
|
1960
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
1864
1961
|
}
|
|
1865
1962
|
/**
|
|
1866
1963
|
* 点外面与 Escape 关闭;锚点按钮交给 toggle,否则关了又立刻被这次点击重开
|
|
1867
1964
|
*/
|
|
1868
1965
|
listenForDismiss(anchor) {
|
|
1869
|
-
const
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1966
|
+
const panel = this.element;
|
|
1967
|
+
if (!panel) return;
|
|
1968
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
const MAX_COLS = 8;
|
|
1972
|
+
const MAX_ROWS = 6;
|
|
1973
|
+
class TableGridPanel {
|
|
1974
|
+
constructor(options) {
|
|
1975
|
+
this.element = null;
|
|
1976
|
+
this.floating = null;
|
|
1977
|
+
this.detachDismiss = null;
|
|
1978
|
+
this.cursorRow = -1;
|
|
1979
|
+
this.cursorCol = -1;
|
|
1980
|
+
this.cells = [];
|
|
1981
|
+
this.editor = options.editor;
|
|
1982
|
+
this.i18n = options.i18n;
|
|
1983
|
+
this.onOpenChange = options.onOpenChange;
|
|
1984
|
+
}
|
|
1985
|
+
get isOpen() {
|
|
1986
|
+
return this.element !== null;
|
|
1987
|
+
}
|
|
1988
|
+
toggle(anchor) {
|
|
1989
|
+
var _a;
|
|
1990
|
+
if (this.isOpen) {
|
|
1873
1991
|
this.close();
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
this.build();
|
|
1995
|
+
const panel = this.element;
|
|
1996
|
+
if (!panel) return;
|
|
1997
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
1998
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
1999
|
+
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
2000
|
+
}
|
|
2001
|
+
close() {
|
|
2002
|
+
var _a, _b, _c;
|
|
2003
|
+
if (!this.element) return;
|
|
2004
|
+
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
2005
|
+
this.detachDismiss = null;
|
|
2006
|
+
(_b = this.floating) == null ? void 0 : _b.stop();
|
|
2007
|
+
this.floating = null;
|
|
2008
|
+
this.element.remove();
|
|
2009
|
+
this.element = null;
|
|
2010
|
+
this.cells = [];
|
|
2011
|
+
this.cursorRow = -1;
|
|
2012
|
+
this.cursorCol = -1;
|
|
2013
|
+
(_c = this.onOpenChange) == null ? void 0 : _c.call(this, false);
|
|
2014
|
+
}
|
|
2015
|
+
destroy() {
|
|
2016
|
+
this.close();
|
|
2017
|
+
}
|
|
2018
|
+
t(key, fallback) {
|
|
2019
|
+
return tOrFallback(this.i18n, key, fallback);
|
|
2020
|
+
}
|
|
2021
|
+
build() {
|
|
2022
|
+
const panel = document.createElement("div");
|
|
2023
|
+
panel.className = "atri-table-panel";
|
|
2024
|
+
panel.setAttribute("role", "dialog");
|
|
2025
|
+
panel.setAttribute("aria-label", this.t("editor.table", "表格"));
|
|
2026
|
+
panel.tabIndex = -1;
|
|
2027
|
+
const grid = document.createElement("div");
|
|
2028
|
+
grid.className = "atri-table-panel-grid";
|
|
2029
|
+
this.cells = [];
|
|
2030
|
+
for (let row = 0; row < MAX_ROWS; row++) {
|
|
2031
|
+
for (let col = 0; col < MAX_COLS; col++) {
|
|
2032
|
+
const cell = document.createElement("div");
|
|
2033
|
+
cell.className = "atri-table-panel-cell";
|
|
2034
|
+
cell.setAttribute("role", "gridcell");
|
|
2035
|
+
cell.dataset.row = String(row);
|
|
2036
|
+
cell.dataset.col = String(col);
|
|
2037
|
+
cell.addEventListener("mouseenter", () => this.highlight(row + 1, col + 1));
|
|
2038
|
+
cell.addEventListener("click", () => this.insert(row + 1, col + 1));
|
|
2039
|
+
grid.appendChild(cell);
|
|
2040
|
+
this.cells.push(cell);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
const hint = document.createElement("div");
|
|
2044
|
+
hint.className = "atri-table-panel-hint";
|
|
2045
|
+
hint.textContent = this.t("editor.table", "表格");
|
|
2046
|
+
panel.addEventListener("keydown", (event) => this.onKeyDown(event));
|
|
2047
|
+
panel.appendChild(grid);
|
|
2048
|
+
panel.appendChild(hint);
|
|
2049
|
+
document.body.appendChild(panel);
|
|
2050
|
+
this.element = panel;
|
|
2051
|
+
copyThemeClasses(this.editor.view.dom, panel);
|
|
2052
|
+
panel.focus();
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* 高亮左上角 (rows × cols) 区域并在提示行显示尺寸;-1 清空
|
|
2056
|
+
*/
|
|
2057
|
+
highlight(rows, cols) {
|
|
2058
|
+
var _a;
|
|
2059
|
+
this.cursorRow = rows < 0 ? -1 : rows - 1;
|
|
2060
|
+
this.cursorCol = cols < 0 ? -1 : cols - 1;
|
|
2061
|
+
this.cells.forEach((cell) => {
|
|
2062
|
+
const r = Number(cell.dataset.row);
|
|
2063
|
+
const c = Number(cell.dataset.col);
|
|
2064
|
+
const active = rows > 0 && cols > 0 && r < rows && c < cols;
|
|
2065
|
+
cell.classList.toggle("atri-table-panel-cell--active", active);
|
|
2066
|
+
if (active) {
|
|
2067
|
+
cell.setAttribute("aria-selected", "true");
|
|
2068
|
+
} else {
|
|
2069
|
+
cell.removeAttribute("aria-selected");
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
const hint = (_a = this.element) == null ? void 0 : _a.querySelector(".atri-table-panel-hint");
|
|
2073
|
+
if (hint) {
|
|
2074
|
+
hint.textContent = rows > 0 ? `${rows} × ${cols}` : this.t("editor.table", "表格");
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
insert(rows, cols) {
|
|
2078
|
+
this.editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run();
|
|
2079
|
+
this.close();
|
|
2080
|
+
}
|
|
2081
|
+
onKeyDown(event) {
|
|
2082
|
+
const deltas = {
|
|
2083
|
+
ArrowLeft: [0, -1],
|
|
2084
|
+
ArrowRight: [0, 1],
|
|
2085
|
+
ArrowUp: [-1, 0],
|
|
2086
|
+
ArrowDown: [1, 0]
|
|
1874
2087
|
};
|
|
1875
|
-
|
|
1876
|
-
if (event.key !== "Escape") return;
|
|
2088
|
+
if (event.key === "Enter") {
|
|
1877
2089
|
event.preventDefault();
|
|
1878
|
-
|
|
1879
|
-
this.
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2090
|
+
if (this.cursorRow < 0 || this.cursorCol < 0) return;
|
|
2091
|
+
this.insert(this.cursorRow + 1, this.cursorCol + 1);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const delta = deltas[event.key];
|
|
2095
|
+
if (!delta) return;
|
|
2096
|
+
event.preventDefault();
|
|
2097
|
+
const baseRow = this.cursorRow < 0 ? 0 : this.cursorRow;
|
|
2098
|
+
const baseCol = this.cursorCol < 0 ? 0 : this.cursorCol;
|
|
2099
|
+
const row = Math.min(MAX_ROWS - 1, Math.max(0, baseRow + delta[0]));
|
|
2100
|
+
const col = Math.min(MAX_COLS - 1, Math.max(0, baseCol + delta[1]));
|
|
2101
|
+
this.highlight(row + 1, col + 1);
|
|
1887
2102
|
}
|
|
1888
2103
|
}
|
|
1889
2104
|
const icons = {
|
|
@@ -1900,15 +2115,31 @@ const icons = {
|
|
|
1900
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>',
|
|
1901
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>',
|
|
1902
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>',
|
|
1903
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>',
|
|
1904
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>',
|
|
1905
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>',
|
|
1906
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>',
|
|
1907
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>',
|
|
1908
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>',
|
|
1909
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>',
|
|
1910
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>',
|
|
1911
|
-
|
|
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>',
|
|
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>'
|
|
1912
2143
|
};
|
|
1913
2144
|
const TOOLTIP_KEYS = {
|
|
1914
2145
|
undo: "editor.undo",
|
|
@@ -1924,28 +2155,35 @@ const TOOLTIP_KEYS = {
|
|
|
1924
2155
|
code: "editor.code",
|
|
1925
2156
|
bulletList: "editor.bulletList",
|
|
1926
2157
|
orderedList: "editor.orderedList",
|
|
2158
|
+
taskList: "editor.taskList",
|
|
1927
2159
|
blockquote: "editor.blockquote",
|
|
1928
2160
|
codeBlock: "editor.codeBlock",
|
|
2161
|
+
horizontalRule: "editor.horizontalRule",
|
|
1929
2162
|
alignLeft: "editor.alignLeft",
|
|
1930
2163
|
alignCenter: "editor.alignCenter",
|
|
1931
2164
|
alignRight: "editor.alignRight",
|
|
2165
|
+
alignJustify: "editor.alignJustify",
|
|
1932
2166
|
insertImage: "editor.image",
|
|
1933
2167
|
insertAttachment: "editor.attachment",
|
|
1934
2168
|
attachmentDisplay: "editor.attachmentDisplay",
|
|
2169
|
+
insertTable: "editor.table",
|
|
2170
|
+
insertLink: "editor.link",
|
|
1935
2171
|
delete: "editor.delete"
|
|
1936
2172
|
};
|
|
1937
2173
|
class ToolbarManager {
|
|
1938
|
-
constructor(editor, container, config, i18n, mediaRuntime) {
|
|
2174
|
+
constructor(editor, container, config, i18n, mediaRuntime, linkPanel) {
|
|
1939
2175
|
var _a;
|
|
1940
2176
|
this.buttons = /* @__PURE__ */ new Map();
|
|
1941
2177
|
this.customTooltips = /* @__PURE__ */ new Map();
|
|
1942
2178
|
this.createdElements = [];
|
|
1943
2179
|
this.panel = null;
|
|
2180
|
+
this.tablePanel = null;
|
|
1944
2181
|
this.editor = editor;
|
|
1945
2182
|
this.container = container;
|
|
1946
2183
|
this.config = config;
|
|
1947
2184
|
this.i18n = i18n;
|
|
1948
2185
|
this.mediaRuntime = mediaRuntime ?? null;
|
|
2186
|
+
this.linkPanel = linkPanel ?? null;
|
|
1949
2187
|
this.itemDefs = this.getDefaultItems();
|
|
1950
2188
|
this.createToolbarDOM();
|
|
1951
2189
|
this.bindEditorEvents();
|
|
@@ -2001,6 +2239,23 @@ class ToolbarManager {
|
|
|
2001
2239
|
var _a;
|
|
2002
2240
|
(_a = this.insertPanel) == null ? void 0 : _a.toggle(button, mode);
|
|
2003
2241
|
}
|
|
2242
|
+
/**
|
|
2243
|
+
* 表格网格面板按需创建:没点开过就不该有一个实例挂在编辑器上
|
|
2244
|
+
*/
|
|
2245
|
+
get gridPanel() {
|
|
2246
|
+
if (!this.tableExtensionRegistered) return null;
|
|
2247
|
+
this.tablePanel ?? (this.tablePanel = new TableGridPanel({
|
|
2248
|
+
editor: this.editor,
|
|
2249
|
+
i18n: this.i18n,
|
|
2250
|
+
// 开合不改文档,等不到 transaction,得主动刷一次按钮状态
|
|
2251
|
+
onOpenChange: () => this.updateButtonStates()
|
|
2252
|
+
}));
|
|
2253
|
+
return this.tablePanel;
|
|
2254
|
+
}
|
|
2255
|
+
/** 内置表格真可用才摆按钮:table:false 未注册、或被用户同名扩展顶掉都不摆(判定见 canInsertTable) */
|
|
2256
|
+
get tableExtensionRegistered() {
|
|
2257
|
+
return canInsertTable(this.editor);
|
|
2258
|
+
}
|
|
2004
2259
|
/**
|
|
2005
2260
|
* 创建工具栏 DOM - 直接将按钮插入 container
|
|
2006
2261
|
*/
|
|
@@ -2021,11 +2276,15 @@ class ToolbarManager {
|
|
|
2021
2276
|
const layout = [
|
|
2022
2277
|
["undo", "redo"],
|
|
2023
2278
|
["heading1", "heading2", "heading3", "paragraph"],
|
|
2024
|
-
|
|
2025
|
-
["
|
|
2026
|
-
["
|
|
2027
|
-
|
|
2028
|
-
|
|
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"] : []],
|
|
2286
|
+
// 「附件样式」不再常驻:切换形态改由选中附件时的浮动工具栏承接(bubble 默认开)
|
|
2287
|
+
...this.itemDefs.has("insertImage") ? [["insertImage", "insertAttachment"]] : []
|
|
2029
2288
|
];
|
|
2030
2289
|
layout.forEach((group, groupIndex) => {
|
|
2031
2290
|
if (groupIndex > 0) {
|
|
@@ -2075,11 +2334,6 @@ class ToolbarManager {
|
|
|
2075
2334
|
} else {
|
|
2076
2335
|
button.innerHTML = itemDef.icon;
|
|
2077
2336
|
}
|
|
2078
|
-
const svg = button.querySelector("svg");
|
|
2079
|
-
if (svg) {
|
|
2080
|
-
svg.setAttribute("width", "18");
|
|
2081
|
-
svg.setAttribute("height", "18");
|
|
2082
|
-
}
|
|
2083
2337
|
if (overrides == null ? void 0 : overrides.tooltip) {
|
|
2084
2338
|
this.customTooltips.set(itemId, overrides.tooltip);
|
|
2085
2339
|
}
|
|
@@ -2111,8 +2365,7 @@ class ToolbarManager {
|
|
|
2111
2365
|
if (!itemDef) return;
|
|
2112
2366
|
const custom = this.customTooltips.get(itemId);
|
|
2113
2367
|
const key = TOOLTIP_KEYS[itemId];
|
|
2114
|
-
const
|
|
2115
|
-
const title = custom ?? (translated && translated !== key ? translated : itemDef.tooltip);
|
|
2368
|
+
const title = custom ?? (key ? ((_a = this.i18n) == null ? void 0 : _a.tOr(key, itemDef.tooltip)) ?? itemDef.tooltip : itemDef.tooltip);
|
|
2116
2369
|
buttons.forEach((button) => {
|
|
2117
2370
|
button.title = title;
|
|
2118
2371
|
});
|
|
@@ -2157,6 +2410,41 @@ class ToolbarManager {
|
|
|
2157
2410
|
isDisabled: (editor) => !editor.isActive("attachment") && !editor.isActive("attachmentLink")
|
|
2158
2411
|
});
|
|
2159
2412
|
}
|
|
2413
|
+
if (this.tableExtensionRegistered) {
|
|
2414
|
+
items.set("insertTable", {
|
|
2415
|
+
id: "insertTable",
|
|
2416
|
+
icon: icons.table,
|
|
2417
|
+
tooltip: "表格",
|
|
2418
|
+
popup: true,
|
|
2419
|
+
command: (_editor, button) => {
|
|
2420
|
+
var _a;
|
|
2421
|
+
return (_a = this.gridPanel) == null ? void 0 : _a.toggle(button);
|
|
2422
|
+
},
|
|
2423
|
+
isActive: () => {
|
|
2424
|
+
var _a;
|
|
2425
|
+
return ((_a = this.tablePanel) == null ? void 0 : _a.isOpen) === true;
|
|
2426
|
+
},
|
|
2427
|
+
isDisabled: (editor) => !editor.isEditable
|
|
2428
|
+
});
|
|
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
|
+
}
|
|
2160
2448
|
items.set("delete", {
|
|
2161
2449
|
id: "delete",
|
|
2162
2450
|
icon: icons.trash,
|
|
@@ -2269,6 +2557,14 @@ class ToolbarManager {
|
|
|
2269
2557
|
isActive: (editor) => editor.isActive("orderedList"),
|
|
2270
2558
|
isDisabled: (editor) => !editor.can().toggleOrderedList()
|
|
2271
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
|
+
});
|
|
2272
2568
|
items.set("blockquote", {
|
|
2273
2569
|
id: "blockquote",
|
|
2274
2570
|
icon: icons.quote,
|
|
@@ -2285,6 +2581,14 @@ class ToolbarManager {
|
|
|
2285
2581
|
isActive: (editor) => editor.isActive("codeBlock"),
|
|
2286
2582
|
isDisabled: (editor) => !editor.can().toggleCodeBlock()
|
|
2287
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
|
+
});
|
|
2288
2592
|
items.set("alignLeft", {
|
|
2289
2593
|
id: "alignLeft",
|
|
2290
2594
|
icon: icons.alignLeft,
|
|
@@ -2309,6 +2613,14 @@ class ToolbarManager {
|
|
|
2309
2613
|
isActive: (editor) => editor.isActive({ textAlign: "right" }),
|
|
2310
2614
|
isDisabled: (editor) => !editor.can().setTextAlign("right")
|
|
2311
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
|
+
});
|
|
2312
2624
|
return items;
|
|
2313
2625
|
}
|
|
2314
2626
|
/**
|
|
@@ -2325,6 +2637,9 @@ class ToolbarManager {
|
|
|
2325
2637
|
}
|
|
2326
2638
|
/**
|
|
2327
2639
|
* 更新所有按钮状态
|
|
2640
|
+
*
|
|
2641
|
+
* 公开是因为浮层开合不改文档,等不到 transaction:挂在门面上的浮层(链接)
|
|
2642
|
+
* 由门面在开合时叫一次,挂在这里的浮层则在 onOpenChange 里叫
|
|
2328
2643
|
*/
|
|
2329
2644
|
updateButtonStates() {
|
|
2330
2645
|
this.buttons.forEach((buttons, itemId) => {
|
|
@@ -2343,55 +2658,527 @@ class ToolbarManager {
|
|
|
2343
2658
|
* 销毁工具栏
|
|
2344
2659
|
*/
|
|
2345
2660
|
destroy() {
|
|
2346
|
-
var _a, _b;
|
|
2661
|
+
var _a, _b, _c;
|
|
2347
2662
|
(_a = this.unsubscribeLanguage) == null ? void 0 : _a.call(this);
|
|
2348
2663
|
this.unsubscribeLanguage = void 0;
|
|
2349
2664
|
(_b = this.panel) == null ? void 0 : _b.destroy();
|
|
2350
2665
|
this.panel = null;
|
|
2666
|
+
(_c = this.tablePanel) == null ? void 0 : _c.destroy();
|
|
2667
|
+
this.tablePanel = null;
|
|
2351
2668
|
this.buttons.clear();
|
|
2352
2669
|
this.customTooltips.clear();
|
|
2353
2670
|
this.createdElements.forEach((el) => el.remove());
|
|
2354
2671
|
this.createdElements = [];
|
|
2355
2672
|
}
|
|
2356
2673
|
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
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);
|
|
2368
2699
|
};
|
|
2369
|
-
this.
|
|
2370
|
-
this.
|
|
2371
|
-
this.
|
|
2372
|
-
this.
|
|
2373
|
-
this.
|
|
2374
|
-
this.
|
|
2375
|
-
this.
|
|
2376
|
-
this.
|
|
2377
|
-
this.retry.className = "atri-media-status-retry";
|
|
2378
|
-
this.retry.hidden = true;
|
|
2379
|
-
this.retry.addEventListener("click", () => void runtime.retryFailed());
|
|
2380
|
-
this.element.append(this.text, this.retry);
|
|
2381
|
-
container.appendChild(this.element);
|
|
2382
|
-
this.unsubscribe = runtime.subscribe((state) => this.render(state));
|
|
2383
|
-
this.unsubscribeLanguage = i18n == null ? void 0 : i18n.onLanguageChanged(() => this.render(runtime.getState()));
|
|
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);
|
|
2384
2708
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
(_a = this.unsubscribe) == null ? void 0 : _a.call(this);
|
|
2388
|
-
(_b = this.unsubscribeLanguage) == null ? void 0 : _b.call(this);
|
|
2389
|
-
this.element.remove();
|
|
2709
|
+
get isOpen() {
|
|
2710
|
+
return this.element !== null;
|
|
2390
2711
|
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
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
|
+
}
|
|
3147
|
+
class MediaStatusStrip {
|
|
3148
|
+
constructor(runtime, container, i18n) {
|
|
3149
|
+
const t = (key, fallback, options) => (i18n == null ? void 0 : i18n.tOr(key, fallback, options)) ?? fallback;
|
|
3150
|
+
this.labels = {
|
|
3151
|
+
uploading: (files, percent) => t("media.uploading", "上传中 {{files}} 个 · {{percent}}%", { files, percent }),
|
|
3152
|
+
failed: (files) => t("media.uploadFailed", "{{files}} 个文件上传失败", { files }),
|
|
3153
|
+
inline: (files) => t("media.inlined", "{{files}} 张图片未上传(已随文档保存)", { files }),
|
|
3154
|
+
retry: () => t("media.retry", "重试")
|
|
3155
|
+
};
|
|
3156
|
+
this.element = document.createElement("div");
|
|
3157
|
+
this.element.className = "atri-media-status";
|
|
3158
|
+
this.element.setAttribute("role", "status");
|
|
3159
|
+
this.element.hidden = true;
|
|
3160
|
+
this.text = document.createElement("span");
|
|
3161
|
+
this.text.className = "atri-media-status-text";
|
|
3162
|
+
this.retry = document.createElement("button");
|
|
3163
|
+
this.retry.type = "button";
|
|
3164
|
+
this.retry.className = "atri-media-status-retry";
|
|
3165
|
+
this.retry.hidden = true;
|
|
3166
|
+
this.retry.addEventListener("click", () => void runtime.retryFailed());
|
|
3167
|
+
this.element.append(this.text, this.retry);
|
|
3168
|
+
container.appendChild(this.element);
|
|
3169
|
+
this.unsubscribe = runtime.subscribe((state) => this.render(state));
|
|
3170
|
+
this.unsubscribeLanguage = i18n == null ? void 0 : i18n.onLanguageChanged(() => this.render(runtime.getState()));
|
|
3171
|
+
}
|
|
3172
|
+
destroy() {
|
|
3173
|
+
var _a, _b;
|
|
3174
|
+
(_a = this.unsubscribe) == null ? void 0 : _a.call(this);
|
|
3175
|
+
(_b = this.unsubscribeLanguage) == null ? void 0 : _b.call(this);
|
|
3176
|
+
this.element.remove();
|
|
3177
|
+
}
|
|
3178
|
+
render(state) {
|
|
3179
|
+
const { failed, inline } = state;
|
|
3180
|
+
const parts = [];
|
|
3181
|
+
if (state.uploading) parts.push(this.labels.uploading(state.uploading, state.percent));
|
|
2395
3182
|
if (failed) parts.push(this.labels.failed(failed));
|
|
2396
3183
|
if (inline) parts.push(this.labels.inline(inline));
|
|
2397
3184
|
const needsAttention = failed > 0 || inline > 0;
|
|
@@ -2817,31 +3604,14 @@ class AICommandMenuManager {
|
|
|
2817
3604
|
this.hide();
|
|
2818
3605
|
}
|
|
2819
3606
|
}
|
|
2820
|
-
function resolveElement(element) {
|
|
2821
|
-
if (typeof element === "string") {
|
|
2822
|
-
const el = document.querySelector(element);
|
|
2823
|
-
if (!el) {
|
|
2824
|
-
throw new Error(`Element not found: ${element}`);
|
|
2825
|
-
}
|
|
2826
|
-
return el;
|
|
2827
|
-
}
|
|
2828
|
-
return element;
|
|
2829
|
-
}
|
|
2830
|
-
function createContainer(className) {
|
|
2831
|
-
const div = document.createElement("div");
|
|
2832
|
-
if (className) {
|
|
2833
|
-
div.className = className;
|
|
2834
|
-
}
|
|
2835
|
-
return div;
|
|
2836
|
-
}
|
|
2837
|
-
function isBrowser() {
|
|
2838
|
-
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
2839
|
-
}
|
|
2840
3607
|
class AtriEditor {
|
|
2841
3608
|
constructor(options) {
|
|
2842
3609
|
this.toolbarManager = null;
|
|
3610
|
+
this.toolbarContainer = null;
|
|
2843
3611
|
this.aiService = null;
|
|
2844
3612
|
this.aiMenuManager = null;
|
|
3613
|
+
this.linkPanel = null;
|
|
3614
|
+
this.tableMenu = null;
|
|
2845
3615
|
this.mediaStatus = null;
|
|
2846
3616
|
this.pendingViewState = null;
|
|
2847
3617
|
this.bubbleElement = null;
|
|
@@ -2855,65 +3625,24 @@ class AtriEditor {
|
|
|
2855
3625
|
if (options.nodeViews && options.nodeViews.length > 0) {
|
|
2856
3626
|
this.extensionManager.registerNodeViews(options.nodeViews);
|
|
2857
3627
|
}
|
|
2858
|
-
let toolbarContainer = null;
|
|
2859
3628
|
if (options.toolbar !== false) {
|
|
2860
|
-
toolbarContainer = createContainer("atri-editor-toolbar");
|
|
2861
|
-
this.container.appendChild(toolbarContainer);
|
|
3629
|
+
this.toolbarContainer = createContainer("atri-editor-toolbar");
|
|
3630
|
+
this.container.appendChild(this.toolbarContainer);
|
|
2862
3631
|
}
|
|
2863
|
-
const bubbleElement = this.ensureBubbleElement(options.toolbar);
|
|
2864
3632
|
const editorElement = createContainer("atri-editor-content-wrapper");
|
|
2865
3633
|
this.container.appendChild(editorElement);
|
|
2866
3634
|
this.mediaRuntime = options.media === false ? null : new MediaRuntime(options.media);
|
|
2867
3635
|
this.mediaStatus = this.mediaRuntime ? new MediaStatusStrip(this.mediaRuntime, this.container, this.i18nManager) : null;
|
|
2868
|
-
this.
|
|
3636
|
+
this.createCoreEditor({
|
|
2869
3637
|
element: editorElement,
|
|
2870
3638
|
content: options.content,
|
|
2871
|
-
contentFormat: options.contentFormat
|
|
2872
|
-
editable: options.editable,
|
|
2873
|
-
placeholder: options.placeholder,
|
|
2874
|
-
extensions: [...options.extensions || [], ...this.extensionManager.getAll()],
|
|
2875
|
-
markdown: options.markdown,
|
|
2876
|
-
media: options.media,
|
|
2877
|
-
mediaRuntime: this.mediaRuntime ?? void 0,
|
|
2878
|
-
bubbleElement,
|
|
2879
|
-
onCreate: () => {
|
|
2880
|
-
this.onEditorCreated();
|
|
2881
|
-
},
|
|
2882
|
-
onUpdate: () => {
|
|
2883
|
-
var _a, _b;
|
|
2884
|
-
(_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this);
|
|
2885
|
-
},
|
|
2886
|
-
onFocus: () => {
|
|
2887
|
-
var _a, _b;
|
|
2888
|
-
(_b = (_a = this.options).onFocus) == null ? void 0 : _b.call(_a, this);
|
|
2889
|
-
},
|
|
2890
|
-
onBlur: () => {
|
|
2891
|
-
var _a, _b;
|
|
2892
|
-
(_b = (_a = this.options).onBlur) == null ? void 0 : _b.call(_a, this);
|
|
2893
|
-
},
|
|
2894
|
-
onDestroy: () => {
|
|
2895
|
-
var _a, _b;
|
|
2896
|
-
(_b = (_a = this.options).onDestroy) == null ? void 0 : _b.call(_a, this);
|
|
2897
|
-
}
|
|
3639
|
+
contentFormat: options.contentFormat
|
|
2898
3640
|
});
|
|
2899
|
-
this.
|
|
2900
|
-
if (toolbarContainer && options.toolbar !== false) {
|
|
2901
|
-
this.toolbarManager = new ToolbarManager(
|
|
2902
|
-
this.editor,
|
|
2903
|
-
toolbarContainer,
|
|
2904
|
-
options.toolbar,
|
|
2905
|
-
this.i18nManager,
|
|
2906
|
-
this.mediaRuntime
|
|
2907
|
-
);
|
|
2908
|
-
if (bubbleElement) this.toolbarManager.attachBubbleToolbar(bubbleElement);
|
|
2909
|
-
}
|
|
2910
|
-
if (options.ai) {
|
|
2911
|
-
this.initAI(options.ai);
|
|
2912
|
-
}
|
|
3641
|
+
this.setupSubsystems();
|
|
2913
3642
|
}
|
|
2914
|
-
/**
|
|
3643
|
+
/** bubble 默认开,显式 false 才不建元素;元素故意不插进文档:挂载与定位全归 BubbleMenu 插件管 */
|
|
2915
3644
|
ensureBubbleElement(toolbar) {
|
|
2916
|
-
if (toolbar === false ||
|
|
3645
|
+
if (toolbar === false || (toolbar == null ? void 0 : toolbar.bubble) === false) return null;
|
|
2917
3646
|
this.bubbleElement ?? (this.bubbleElement = createContainer("atri-editor-bubble-toolbar"));
|
|
2918
3647
|
return this.bubbleElement;
|
|
2919
3648
|
}
|
|
@@ -2932,7 +3661,7 @@ class AtriEditor {
|
|
|
2932
3661
|
this.aiMenuManager = new AICommandMenuManager(
|
|
2933
3662
|
this.editor,
|
|
2934
3663
|
this.options.ai.functions,
|
|
2935
|
-
"/",
|
|
3664
|
+
this.options.ai.triggerChar ?? "/",
|
|
2936
3665
|
(func) => {
|
|
2937
3666
|
var _a2;
|
|
2938
3667
|
(_a2 = this.aiService) == null ? void 0 : _a2.execute(func.id);
|
|
@@ -2945,76 +3674,114 @@ class AtriEditor {
|
|
|
2945
3674
|
this.aiService = new AIService(this.editor, config, this.markdownService);
|
|
2946
3675
|
}
|
|
2947
3676
|
/**
|
|
2948
|
-
*
|
|
2949
|
-
*
|
|
3677
|
+
* 创建核心编辑器:构造函数与 recreateEditor 共用的装配点。
|
|
3678
|
+
* 只有编辑器壳(挂载元素/初始内容/内容格式)随重建变化,其余全部来自 this.options,
|
|
3679
|
+
* 逐项手抄两遍必然漂移——出错的第一现场就是两处不齐
|
|
2950
3680
|
*/
|
|
2951
|
-
|
|
2952
|
-
var _a, _b;
|
|
2953
|
-
const previousEditor = this.coreEditor.getEditor();
|
|
2954
|
-
const currentContent = this.coreEditor.getHTML();
|
|
2955
|
-
const { from, to } = previousEditor.state.selection;
|
|
2956
|
-
this.pendingViewState = { from, to };
|
|
2957
|
-
this.coreEditor.destroy();
|
|
2958
|
-
(_a = this.toolbarManager) == null ? void 0 : _a.destroy();
|
|
2959
|
-
(_b = this.aiMenuManager) == null ? void 0 : _b.destroy();
|
|
2960
|
-
const editorElement = this.container.querySelector(
|
|
2961
|
-
".atri-editor-content-wrapper"
|
|
2962
|
-
);
|
|
2963
|
-
const toolbarContainer = this.container.querySelector(".atri-editor-toolbar");
|
|
2964
|
-
const newEditorElement = createContainer("atri-editor-content-wrapper");
|
|
2965
|
-
if (editorElement) {
|
|
2966
|
-
editorElement.replaceWith(newEditorElement);
|
|
2967
|
-
} else {
|
|
2968
|
-
this.container.appendChild(newEditorElement);
|
|
2969
|
-
}
|
|
2970
|
-
const bubbleElement = this.ensureBubbleElement(this.options.toolbar);
|
|
3681
|
+
createCoreEditor(init) {
|
|
2971
3682
|
this.coreEditor = new CoreEditor({
|
|
2972
|
-
element:
|
|
2973
|
-
content:
|
|
2974
|
-
|
|
2975
|
-
contentFormat: "html",
|
|
3683
|
+
element: init.element,
|
|
3684
|
+
content: init.content,
|
|
3685
|
+
contentFormat: init.contentFormat,
|
|
2976
3686
|
editable: this.options.editable,
|
|
2977
3687
|
placeholder: this.options.placeholder,
|
|
2978
3688
|
extensions: [...this.options.extensions || [], ...this.extensionManager.getAll()],
|
|
2979
3689
|
markdown: this.options.markdown,
|
|
2980
3690
|
media: this.options.media,
|
|
3691
|
+
table: this.options.table,
|
|
3692
|
+
link: this.options.link,
|
|
2981
3693
|
mediaRuntime: this.mediaRuntime ?? void 0,
|
|
2982
|
-
bubbleElement,
|
|
3694
|
+
bubbleElement: this.ensureBubbleElement(this.options.toolbar),
|
|
2983
3695
|
onCreate: () => {
|
|
2984
3696
|
this.onEditorCreated();
|
|
2985
3697
|
},
|
|
2986
3698
|
onUpdate: () => {
|
|
2987
|
-
var
|
|
2988
|
-
(
|
|
3699
|
+
var _a, _b;
|
|
3700
|
+
(_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this);
|
|
2989
3701
|
},
|
|
2990
3702
|
onFocus: () => {
|
|
2991
|
-
var
|
|
2992
|
-
(
|
|
3703
|
+
var _a, _b;
|
|
3704
|
+
(_b = (_a = this.options).onFocus) == null ? void 0 : _b.call(_a, this);
|
|
2993
3705
|
},
|
|
2994
3706
|
onBlur: () => {
|
|
2995
|
-
var
|
|
2996
|
-
(
|
|
3707
|
+
var _a, _b;
|
|
3708
|
+
(_b = (_a = this.options).onBlur) == null ? void 0 : _b.call(_a, this);
|
|
2997
3709
|
},
|
|
2998
3710
|
onDestroy: () => {
|
|
2999
|
-
var
|
|
3000
|
-
(
|
|
3711
|
+
var _a, _b;
|
|
3712
|
+
(_b = (_a = this.options).onDestroy) == null ? void 0 : _b.call(_a, this);
|
|
3001
3713
|
}
|
|
3002
3714
|
});
|
|
3715
|
+
}
|
|
3716
|
+
/** 初始化依赖编辑器实例的子系统:Markdown 服务、链接浮层、工具栏、表格菜单、AI 服务,重建后同样要重新走一遍 */
|
|
3717
|
+
setupSubsystems() {
|
|
3718
|
+
var _a, _b;
|
|
3003
3719
|
this.markdownService = new MarkdownService(this.editor, this.options.markdown);
|
|
3004
|
-
|
|
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;
|
|
3732
|
+
if (this.toolbarContainer && this.options.toolbar !== false) {
|
|
3005
3733
|
this.toolbarManager = new ToolbarManager(
|
|
3006
3734
|
this.editor,
|
|
3007
|
-
toolbarContainer,
|
|
3735
|
+
this.toolbarContainer,
|
|
3008
3736
|
this.options.toolbar,
|
|
3009
3737
|
this.i18nManager,
|
|
3010
|
-
this.mediaRuntime
|
|
3738
|
+
this.mediaRuntime,
|
|
3739
|
+
this.linkPanel
|
|
3011
3740
|
);
|
|
3012
|
-
if (bubbleElement) this.toolbarManager.attachBubbleToolbar(bubbleElement);
|
|
3741
|
+
if (this.bubbleElement) this.toolbarManager.attachBubbleToolbar(this.bubbleElement);
|
|
3013
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;
|
|
3014
3749
|
if (this.options.ai) {
|
|
3015
3750
|
this.initAI(this.options.ai);
|
|
3016
3751
|
}
|
|
3017
3752
|
}
|
|
3753
|
+
/**
|
|
3754
|
+
* 重新创建编辑器(用于动态注册 NodeView 后更新 schema)
|
|
3755
|
+
* 会整体替换编辑器视图,因此内容与选区都要手动恢复
|
|
3756
|
+
*/
|
|
3757
|
+
recreateEditor() {
|
|
3758
|
+
var _a, _b, _c, _d;
|
|
3759
|
+
const previousEditor = this.coreEditor.getEditor();
|
|
3760
|
+
const currentContent = this.coreEditor.getHTML();
|
|
3761
|
+
const { from, to } = previousEditor.state.selection;
|
|
3762
|
+
this.pendingViewState = { from, to };
|
|
3763
|
+
(_a = this.toolbarManager) == null ? void 0 : _a.destroy();
|
|
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();
|
|
3768
|
+
const editorElement = this.container.querySelector(
|
|
3769
|
+
".atri-editor-content-wrapper"
|
|
3770
|
+
);
|
|
3771
|
+
const newEditorElement = createContainer("atri-editor-content-wrapper");
|
|
3772
|
+
if (editorElement) {
|
|
3773
|
+
editorElement.replaceWith(newEditorElement);
|
|
3774
|
+
} else {
|
|
3775
|
+
this.container.appendChild(newEditorElement);
|
|
3776
|
+
}
|
|
3777
|
+
this.createCoreEditor({
|
|
3778
|
+
element: newEditorElement,
|
|
3779
|
+
content: currentContent,
|
|
3780
|
+
// 当前内容取自 getHTML(),重建时按 html 回填,不能沿用用户的 contentFormat
|
|
3781
|
+
contentFormat: "html"
|
|
3782
|
+
});
|
|
3783
|
+
this.setupSubsystems();
|
|
3784
|
+
}
|
|
3018
3785
|
/**
|
|
3019
3786
|
* 获取 Tiptap Editor 实例
|
|
3020
3787
|
*/
|
|
@@ -3103,12 +3870,7 @@ class AtriEditor {
|
|
|
3103
3870
|
* 获取选中文本
|
|
3104
3871
|
*/
|
|
3105
3872
|
getSelectedText() {
|
|
3106
|
-
|
|
3107
|
-
if (selection.empty) return "";
|
|
3108
|
-
return getTextBetween(this.editor.state.doc, {
|
|
3109
|
-
from: selection.from,
|
|
3110
|
-
to: selection.to
|
|
3111
|
-
});
|
|
3873
|
+
return getSelectedText(this.editor);
|
|
3112
3874
|
}
|
|
3113
3875
|
/**
|
|
3114
3876
|
* 插入内容
|
|
@@ -3137,6 +3899,17 @@ class AtriEditor {
|
|
|
3137
3899
|
chain.setAttachment(options).run();
|
|
3138
3900
|
}
|
|
3139
3901
|
}
|
|
3902
|
+
/**
|
|
3903
|
+
* 在选区处插入表格(默认 3×3 带表头)
|
|
3904
|
+
*/
|
|
3905
|
+
insertTable(options) {
|
|
3906
|
+
if (!canInsertTable(this.editor)) {
|
|
3907
|
+
console.warn("[Atri Editor] insertTable() ignored: table extensions are disabled.");
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
const { rows = 3, cols = 3, withHeaderRow = true } = options ?? {};
|
|
3911
|
+
this.editor.chain().focus().insertTable({ rows, cols, withHeaderRow }).run();
|
|
3912
|
+
}
|
|
3140
3913
|
/**
|
|
3141
3914
|
* 走上传管线插入本地文件
|
|
3142
3915
|
* kind 缺省时按 MIME 分流;promise 在这批文件全部落定(成功或失败)后 resolve
|
|
@@ -3275,12 +4048,14 @@ class AtriEditor {
|
|
|
3275
4048
|
* 销毁编辑器
|
|
3276
4049
|
*/
|
|
3277
4050
|
destroy() {
|
|
3278
|
-
var _a, _b, _c, _d;
|
|
4051
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3279
4052
|
(_a = this.toolbarManager) == null ? void 0 : _a.destroy();
|
|
3280
|
-
(_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();
|
|
3281
4056
|
this.coreEditor.destroy();
|
|
3282
|
-
(
|
|
3283
|
-
(
|
|
4057
|
+
(_e = this.mediaStatus) == null ? void 0 : _e.destroy();
|
|
4058
|
+
(_f = this.mediaRuntime) == null ? void 0 : _f.destroy();
|
|
3284
4059
|
this.bubbleElement = null;
|
|
3285
4060
|
this.container.remove();
|
|
3286
4061
|
}
|
|
@@ -3430,6 +4205,13 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
3430
4205
|
var _a;
|
|
3431
4206
|
(_a = this.editor) == null ? void 0 : _a.insertAttachment(options);
|
|
3432
4207
|
}
|
|
4208
|
+
/**
|
|
4209
|
+
* 在选区处插入表格
|
|
4210
|
+
*/
|
|
4211
|
+
insertTable(options) {
|
|
4212
|
+
var _a;
|
|
4213
|
+
(_a = this.editor) == null ? void 0 : _a.insertTable(options);
|
|
4214
|
+
}
|
|
3433
4215
|
/**
|
|
3434
4216
|
* 走上传管线插入本地文件
|
|
3435
4217
|
*/
|
|
@@ -3480,7 +4262,6 @@ export {
|
|
|
3480
4262
|
getCursorContext,
|
|
3481
4263
|
getDocumentText,
|
|
3482
4264
|
getSelectedText,
|
|
3483
|
-
isBrowser,
|
|
3484
4265
|
registerAtriElement,
|
|
3485
4266
|
resolveElement,
|
|
3486
4267
|
simpleHtmlToMarkdown,
|