@atri-editor/core 0.1.0 → 0.2.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 +8 -0
- package/README.md +5 -5
- package/dist/AtriEditor.d.ts +20 -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 +2 -0
- 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/TableGridPanel.d.ts +41 -0
- package/dist/core/TableGridPanel.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 +7 -0
- 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/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +429 -302
- 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 +62 -0
- package/dist/types/ai.d.ts +2 -0
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/editor.d.ts +23 -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 +17 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/package.json +23 -5
- package/dist/types/events.d.ts +0 -26
- package/dist/types/events.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ 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 { Table, TableRow, TableHeader, TableCell } from "@tiptap/extension-table";
|
|
7
8
|
import Image from "@tiptap/extension-image";
|
|
8
9
|
import { NodeSelection, Plugin, PluginKey } from "@tiptap/pm/state";
|
|
9
10
|
import { Node as Node$1, DOMParser } from "@tiptap/pm/model";
|
|
@@ -196,7 +197,50 @@ function attachmentAttributes() {
|
|
|
196
197
|
}
|
|
197
198
|
};
|
|
198
199
|
}
|
|
199
|
-
|
|
200
|
+
function makeAttachmentMarkdown(options) {
|
|
201
|
+
const { name, level, nodeName } = options;
|
|
202
|
+
const directive = `!${name}`;
|
|
203
|
+
const trailing = level === "block" ? "(?:\\n|$)" : "";
|
|
204
|
+
const pattern = new RegExp(
|
|
205
|
+
`^${directive.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\[([^\\]]*)\\]\\(\\s*([^\\s)]+)(?:\\s+"([^"]*)")?\\s*\\)${trailing}`
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
markdownTokenName: name,
|
|
209
|
+
markdownTokenizer: {
|
|
210
|
+
name,
|
|
211
|
+
level,
|
|
212
|
+
start: (src) => src.indexOf(`${directive}[`),
|
|
213
|
+
tokenize: (src) => {
|
|
214
|
+
const match = pattern.exec(src);
|
|
215
|
+
if (!match) return void 0;
|
|
216
|
+
return {
|
|
217
|
+
type: name,
|
|
218
|
+
raw: match[0],
|
|
219
|
+
text: match[1] ?? "",
|
|
220
|
+
href: match[2] ?? "",
|
|
221
|
+
title: match[3]
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
parseMarkdown: (token, helpers) => helpers.createNode(
|
|
226
|
+
nodeName,
|
|
227
|
+
{
|
|
228
|
+
src: token.href,
|
|
229
|
+
name: token.text || null,
|
|
230
|
+
size: parseFileSize(token.title) ?? null,
|
|
231
|
+
mime: null
|
|
232
|
+
},
|
|
233
|
+
[]
|
|
234
|
+
),
|
|
235
|
+
renderMarkdown: (node) => {
|
|
236
|
+
var _a, _b, _c;
|
|
237
|
+
const src = ((_a = node.attrs) == null ? void 0 : _a.src) ?? "";
|
|
238
|
+
const label = ((_b = node.attrs) == null ? void 0 : _b.name) ?? "";
|
|
239
|
+
const size = (_c = node.attrs) == null ? void 0 : _c.size;
|
|
240
|
+
return size ? `${directive}[${label}](${src} "${formatFileSize(size)}")` : `${directive}[${label}](${src})`;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
200
244
|
const FORM_TYPES = {
|
|
201
245
|
card: "attachment",
|
|
202
246
|
link: "attachmentLink"
|
|
@@ -351,42 +395,12 @@ const Attachment = Node.create({
|
|
|
351
395
|
})
|
|
352
396
|
];
|
|
353
397
|
},
|
|
354
|
-
|
|
355
|
-
markdownTokenizer: {
|
|
398
|
+
...makeAttachmentMarkdown({
|
|
356
399
|
name: "file",
|
|
357
400
|
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
|
-
}
|
|
401
|
+
nodeName: "attachment"
|
|
402
|
+
})
|
|
388
403
|
});
|
|
389
|
-
const markdownPattern = /^!filelink\[([^\]]*)\]\(\s*([^\s)]+)(?:\s+"([^"]*)")?\s*\)/;
|
|
390
404
|
const AttachmentLink = Node.create({
|
|
391
405
|
name: "attachmentLink",
|
|
392
406
|
inline: true,
|
|
@@ -429,42 +443,30 @@ const AttachmentLink = Node.create({
|
|
|
429
443
|
setAttachmentLink: (options) => (props) => insertAttachmentContent(props, this.name, options)
|
|
430
444
|
};
|
|
431
445
|
},
|
|
432
|
-
|
|
433
|
-
markdownTokenizer: {
|
|
446
|
+
...makeAttachmentMarkdown({
|
|
434
447
|
name: "filelink",
|
|
435
448
|
level: "inline",
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
href: match[2] ?? "",
|
|
445
|
-
title: match[3]
|
|
446
|
-
};
|
|
449
|
+
nodeName: "attachmentLink"
|
|
450
|
+
})
|
|
451
|
+
});
|
|
452
|
+
function resolveElement(element) {
|
|
453
|
+
if (typeof element === "string") {
|
|
454
|
+
const el = document.querySelector(element);
|
|
455
|
+
if (!el) {
|
|
456
|
+
throw new Error(`Element not found: ${element}`);
|
|
447
457
|
}
|
|
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})`;
|
|
458
|
+
return el;
|
|
465
459
|
}
|
|
466
|
-
|
|
467
|
-
|
|
460
|
+
return element;
|
|
461
|
+
}
|
|
462
|
+
function createContainer(className) {
|
|
463
|
+
const div = document.createElement("div");
|
|
464
|
+
if (className) {
|
|
465
|
+
div.className = className;
|
|
466
|
+
}
|
|
467
|
+
return div;
|
|
468
|
+
}
|
|
469
|
+
function filesOf(transfer) {
|
|
468
470
|
if (!transfer) return [];
|
|
469
471
|
return Array.from(transfer.files);
|
|
470
472
|
}
|
|
@@ -486,7 +488,7 @@ const MediaFiles = Extension.create({
|
|
|
486
488
|
props: {
|
|
487
489
|
/** 先于 PM 的剪贴板解析:带文件的粘贴(截图)交给上传管线,纯文本照常走默认链路 */
|
|
488
490
|
handlePaste: (view, event) => {
|
|
489
|
-
const files = filesOf
|
|
491
|
+
const files = filesOf(event.clipboardData);
|
|
490
492
|
if (!files.length) return false;
|
|
491
493
|
event.preventDefault();
|
|
492
494
|
if (view.editable) void runtime.handleFiles(files);
|
|
@@ -494,7 +496,7 @@ const MediaFiles = Extension.create({
|
|
|
494
496
|
},
|
|
495
497
|
handleDrop: (view, event) => {
|
|
496
498
|
const dragEvent = event;
|
|
497
|
-
const files = filesOf
|
|
499
|
+
const files = filesOf(dragEvent.dataTransfer);
|
|
498
500
|
if (!files.length) return false;
|
|
499
501
|
dragEvent.preventDefault();
|
|
500
502
|
if (!view.editable) return true;
|
|
@@ -533,11 +535,17 @@ function createMediaExtensions(media, runtime) {
|
|
|
533
535
|
];
|
|
534
536
|
}
|
|
535
537
|
function pickUrl(body) {
|
|
536
|
-
var _a;
|
|
537
538
|
if (!body || typeof body !== "object") return void 0;
|
|
538
539
|
const record = body;
|
|
539
|
-
const
|
|
540
|
-
const
|
|
540
|
+
const nested = record.data;
|
|
541
|
+
const candidates = [
|
|
542
|
+
record.url,
|
|
543
|
+
record.location,
|
|
544
|
+
nested && typeof nested === "object" ? nested.url : void 0
|
|
545
|
+
];
|
|
546
|
+
const url = candidates.find(
|
|
547
|
+
(value) => typeof value === "string" && value !== ""
|
|
548
|
+
);
|
|
541
549
|
return url;
|
|
542
550
|
}
|
|
543
551
|
function uploadViaEndpoint(config, file, context) {
|
|
@@ -1061,6 +1069,9 @@ class CoreEditor {
|
|
|
1061
1069
|
}
|
|
1062
1070
|
const contentType = contentFormat === "markdown" && !markdownEnabled ? void 0 : contentFormat;
|
|
1063
1071
|
editorExtensions.push(...createMediaExtensions(this.config.media, this.mediaRuntime));
|
|
1072
|
+
if (this.config.table !== false) {
|
|
1073
|
+
editorExtensions.push(Table, TableRow, TableHeader, TableCell);
|
|
1074
|
+
}
|
|
1064
1075
|
if (this.config.bubbleElement) {
|
|
1065
1076
|
editorExtensions.push(
|
|
1066
1077
|
BubbleMenu.configure({
|
|
@@ -1137,9 +1148,14 @@ class CoreEditor {
|
|
|
1137
1148
|
try {
|
|
1138
1149
|
return this.editor.getHTML();
|
|
1139
1150
|
} catch (err) {
|
|
1140
|
-
console.error(
|
|
1141
|
-
|
|
1142
|
-
|
|
1151
|
+
console.error(
|
|
1152
|
+
"[CoreEditor] getHTML() failed:",
|
|
1153
|
+
err,
|
|
1154
|
+
"\nSchema nodes:",
|
|
1155
|
+
Object.keys(this.editor.schema.nodes),
|
|
1156
|
+
"\nSchema marks:",
|
|
1157
|
+
Object.keys(this.editor.schema.marks)
|
|
1158
|
+
);
|
|
1143
1159
|
throw err;
|
|
1144
1160
|
}
|
|
1145
1161
|
}
|
|
@@ -1372,23 +1388,13 @@ class ThemeManager {
|
|
|
1372
1388
|
this.applyTheme();
|
|
1373
1389
|
}
|
|
1374
1390
|
/**
|
|
1375
|
-
*
|
|
1391
|
+
* 应用当前主题:内置主题与自定义主题走同一条 class 规则,只是名字不同
|
|
1376
1392
|
*/
|
|
1377
1393
|
applyTheme() {
|
|
1378
1394
|
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
|
-
}
|
|
1395
|
+
this.container.classList.add(`atri-theme-${this.currentTheme}`);
|
|
1384
1396
|
this.container.setAttribute("data-atri-theme", this.currentTheme);
|
|
1385
1397
|
}
|
|
1386
|
-
/**
|
|
1387
|
-
* 获取当前主题
|
|
1388
|
-
*/
|
|
1389
|
-
getTheme() {
|
|
1390
|
-
return this.currentTheme;
|
|
1391
|
-
}
|
|
1392
1398
|
/**
|
|
1393
1399
|
* 设置主题
|
|
1394
1400
|
*/
|
|
@@ -1408,12 +1414,6 @@ class ThemeManager {
|
|
|
1408
1414
|
this.setTheme("light");
|
|
1409
1415
|
}
|
|
1410
1416
|
}
|
|
1411
|
-
/**
|
|
1412
|
-
* 是否为暗色主题
|
|
1413
|
-
*/
|
|
1414
|
-
isDark() {
|
|
1415
|
-
return this.currentTheme === "dark";
|
|
1416
|
-
}
|
|
1417
1417
|
}
|
|
1418
1418
|
const zhResources = {
|
|
1419
1419
|
editor: {
|
|
@@ -1540,6 +1540,13 @@ class I18nManager {
|
|
|
1540
1540
|
t(key, options) {
|
|
1541
1541
|
return this.instance.t(key, options);
|
|
1542
1542
|
}
|
|
1543
|
+
/**
|
|
1544
|
+
* 翻译,词条缺失时回退到 fallback:i18next 缺词条时原样返回 key
|
|
1545
|
+
*/
|
|
1546
|
+
tOr(key, fallback, options) {
|
|
1547
|
+
const translated = this.t(key, options);
|
|
1548
|
+
return translated && translated !== key ? translated : fallback;
|
|
1549
|
+
}
|
|
1543
1550
|
/**
|
|
1544
1551
|
* 切换语言
|
|
1545
1552
|
*/
|
|
@@ -1560,6 +1567,9 @@ class I18nManager {
|
|
|
1560
1567
|
return this.instance.language || "zh";
|
|
1561
1568
|
}
|
|
1562
1569
|
}
|
|
1570
|
+
function tOrFallback(i18n, key, fallback) {
|
|
1571
|
+
return (i18n == null ? void 0 : i18n.tOr(key, fallback)) ?? fallback;
|
|
1572
|
+
}
|
|
1563
1573
|
class ExtensionManager {
|
|
1564
1574
|
constructor() {
|
|
1565
1575
|
this.extensions = /* @__PURE__ */ new Map();
|
|
@@ -1568,19 +1578,13 @@ class ExtensionManager {
|
|
|
1568
1578
|
/**
|
|
1569
1579
|
* 注册扩展
|
|
1570
1580
|
*/
|
|
1571
|
-
register(extension
|
|
1581
|
+
register(extension) {
|
|
1572
1582
|
const name = extension.name;
|
|
1573
1583
|
if (this.extensions.has(name)) {
|
|
1574
1584
|
console.warn(`Extension "${name}" already registered, overwriting.`);
|
|
1575
1585
|
}
|
|
1576
1586
|
this.extensions.set(name, extension);
|
|
1577
1587
|
}
|
|
1578
|
-
/**
|
|
1579
|
-
* 批量注册扩展
|
|
1580
|
-
*/
|
|
1581
|
-
registerAll(extensions) {
|
|
1582
|
-
extensions.forEach((ext) => this.register(ext));
|
|
1583
|
-
}
|
|
1584
1588
|
/**
|
|
1585
1589
|
* 注册自定义 NodeView
|
|
1586
1590
|
*/
|
|
@@ -1612,12 +1616,7 @@ class ExtensionManager {
|
|
|
1612
1616
|
// 由 Tiptap 的 MarkdownManager 按节点名调用,签名见 @tiptap/core NodeConfig.renderMarkdown
|
|
1613
1617
|
renderMarkdown: config.markdownSerialize ? (node) => config.markdownSerialize(node) : void 0
|
|
1614
1618
|
});
|
|
1615
|
-
this.register(nodeExtension
|
|
1616
|
-
name: config.name,
|
|
1617
|
-
version: "1.0.0",
|
|
1618
|
-
category: "custom",
|
|
1619
|
-
description: config.description
|
|
1620
|
-
});
|
|
1619
|
+
this.register(nodeExtension);
|
|
1621
1620
|
}
|
|
1622
1621
|
/**
|
|
1623
1622
|
* 批量注册 NodeView
|
|
@@ -1631,13 +1630,6 @@ class ExtensionManager {
|
|
|
1631
1630
|
get(name) {
|
|
1632
1631
|
return this.extensions.get(name);
|
|
1633
1632
|
}
|
|
1634
|
-
/**
|
|
1635
|
-
* 卸载扩展
|
|
1636
|
-
*/
|
|
1637
|
-
unregister(name) {
|
|
1638
|
-
this.nodeViews.delete(name);
|
|
1639
|
-
return this.extensions.delete(name);
|
|
1640
|
-
}
|
|
1641
1633
|
/**
|
|
1642
1634
|
* 获取所有扩展
|
|
1643
1635
|
*/
|
|
@@ -1656,28 +1648,69 @@ class ExtensionManager {
|
|
|
1656
1648
|
has(name) {
|
|
1657
1649
|
return this.extensions.has(name);
|
|
1658
1650
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1651
|
+
}
|
|
1652
|
+
function positionPanel(trigger, panel, isCurrent) {
|
|
1653
|
+
let stopAutoUpdate = null;
|
|
1654
|
+
const update = async () => {
|
|
1655
|
+
if (!isCurrent()) return;
|
|
1656
|
+
try {
|
|
1657
|
+
const { x, y } = await computePosition(trigger, panel, {
|
|
1658
|
+
placement: "bottom-start",
|
|
1659
|
+
middleware: [offset(6), flip(), shift({ padding: 8 })]
|
|
1660
|
+
});
|
|
1661
|
+
if (!isCurrent()) return;
|
|
1662
|
+
panel.style.position = "fixed";
|
|
1663
|
+
panel.style.left = `${x}px`;
|
|
1664
|
+
panel.style.top = `${y}px`;
|
|
1665
|
+
} catch {
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
void update().then(() => {
|
|
1669
|
+
if (isCurrent()) {
|
|
1670
|
+
stopAutoUpdate = autoUpdate(trigger, panel, () => void update());
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
return {
|
|
1674
|
+
stop: () => {
|
|
1675
|
+
stopAutoUpdate == null ? void 0 : stopAutoUpdate();
|
|
1676
|
+
stopAutoUpdate = null;
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function listenPanelDismiss(trigger, panel, onClose) {
|
|
1681
|
+
const onPointerDown = (event) => {
|
|
1682
|
+
const target = event.target;
|
|
1683
|
+
if (!target || panel.contains(target) || trigger.contains(target)) return;
|
|
1684
|
+
onClose();
|
|
1685
|
+
};
|
|
1686
|
+
const onKeyDown = (event) => {
|
|
1687
|
+
if (event.key !== "Escape") return;
|
|
1688
|
+
event.preventDefault();
|
|
1689
|
+
event.stopPropagation();
|
|
1690
|
+
onClose();
|
|
1691
|
+
};
|
|
1692
|
+
document.addEventListener("pointerdown", onPointerDown, true);
|
|
1693
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1694
|
+
return () => {
|
|
1695
|
+
document.removeEventListener("pointerdown", onPointerDown, true);
|
|
1696
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function copyThemeClasses(source, panel) {
|
|
1700
|
+
const root = source == null ? void 0 : source.closest(".atri-editor");
|
|
1701
|
+
const themes = Array.from((root == null ? void 0 : root.classList) ?? []).filter((name) => name.startsWith("atri-theme-"));
|
|
1702
|
+
panel.classList.add(...themes);
|
|
1666
1703
|
}
|
|
1667
1704
|
const FIELDS = {
|
|
1668
1705
|
image: { url: true, alt: true, kind: "image" },
|
|
1669
1706
|
attachment: { url: false, alt: false, kind: "attachment" }
|
|
1670
1707
|
};
|
|
1671
|
-
function filesOf(transfer) {
|
|
1672
|
-
if (!transfer) return [];
|
|
1673
|
-
return Array.from(transfer.files);
|
|
1674
|
-
}
|
|
1675
1708
|
class InsertPanel {
|
|
1676
1709
|
constructor(options) {
|
|
1677
1710
|
var _a;
|
|
1678
1711
|
this.element = null;
|
|
1679
1712
|
this.mode = null;
|
|
1680
|
-
this.
|
|
1713
|
+
this.floating = null;
|
|
1681
1714
|
this.detachDismiss = null;
|
|
1682
1715
|
this.labelled = [];
|
|
1683
1716
|
this.editor = options.editor;
|
|
@@ -1709,8 +1742,8 @@ class InsertPanel {
|
|
|
1709
1742
|
var _a, _b, _c, _d;
|
|
1710
1743
|
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
1711
1744
|
this.detachDismiss = null;
|
|
1712
|
-
(_b = this.
|
|
1713
|
-
this.
|
|
1745
|
+
(_b = this.floating) == null ? void 0 : _b.stop();
|
|
1746
|
+
this.floating = null;
|
|
1714
1747
|
(_c = this.element) == null ? void 0 : _c.remove();
|
|
1715
1748
|
this.element = null;
|
|
1716
1749
|
this.labelled = [];
|
|
@@ -1725,9 +1758,7 @@ class InsertPanel {
|
|
|
1725
1758
|
this.unsubscribeLanguage = void 0;
|
|
1726
1759
|
}
|
|
1727
1760
|
t(key, fallback) {
|
|
1728
|
-
|
|
1729
|
-
const translated = (_a = this.i18n) == null ? void 0 : _a.t(key);
|
|
1730
|
-
return translated && translated !== key ? translated : fallback;
|
|
1761
|
+
return tOrFallback(this.i18n, key, fallback);
|
|
1731
1762
|
}
|
|
1732
1763
|
/**
|
|
1733
1764
|
* 登记一处随语言重画的文案:input 画到 placeholder,其他画成文字
|
|
@@ -1813,19 +1844,9 @@ class InsertPanel {
|
|
|
1813
1844
|
}
|
|
1814
1845
|
document.body.appendChild(panel);
|
|
1815
1846
|
this.element = panel;
|
|
1816
|
-
this.
|
|
1847
|
+
copyThemeClasses(this.editor.view.dom, panel);
|
|
1817
1848
|
(urlInput ?? altInput ?? browse).focus();
|
|
1818
1849
|
}
|
|
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
1850
|
/**
|
|
1830
1851
|
* 外链图片当场就插,不进上传队列:地址本来就是可用的
|
|
1831
1852
|
*/
|
|
@@ -1840,52 +1861,159 @@ class InsertPanel {
|
|
|
1840
1861
|
void this.runtime.handleFiles(files, { kind });
|
|
1841
1862
|
this.close();
|
|
1842
1863
|
}
|
|
1843
|
-
|
|
1864
|
+
position(anchor) {
|
|
1844
1865
|
const panel = this.element;
|
|
1845
1866
|
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
|
-
}
|
|
1867
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
1864
1868
|
}
|
|
1865
1869
|
/**
|
|
1866
1870
|
* 点外面与 Escape 关闭;锚点按钮交给 toggle,否则关了又立刻被这次点击重开
|
|
1867
1871
|
*/
|
|
1868
1872
|
listenForDismiss(anchor) {
|
|
1869
|
-
const
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
+
const panel = this.element;
|
|
1874
|
+
if (!panel) return;
|
|
1875
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
const MAX_COLS = 8;
|
|
1879
|
+
const MAX_ROWS = 6;
|
|
1880
|
+
class TableGridPanel {
|
|
1881
|
+
constructor(options) {
|
|
1882
|
+
this.element = null;
|
|
1883
|
+
this.floating = null;
|
|
1884
|
+
this.detachDismiss = null;
|
|
1885
|
+
this.cursorRow = -1;
|
|
1886
|
+
this.cursorCol = -1;
|
|
1887
|
+
this.cells = [];
|
|
1888
|
+
this.editor = options.editor;
|
|
1889
|
+
this.i18n = options.i18n;
|
|
1890
|
+
this.onOpenChange = options.onOpenChange;
|
|
1891
|
+
}
|
|
1892
|
+
get isOpen() {
|
|
1893
|
+
return this.element !== null;
|
|
1894
|
+
}
|
|
1895
|
+
toggle(anchor) {
|
|
1896
|
+
var _a;
|
|
1897
|
+
if (this.isOpen) {
|
|
1873
1898
|
this.close();
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
this.build();
|
|
1902
|
+
const panel = this.element;
|
|
1903
|
+
if (!panel) return;
|
|
1904
|
+
this.floating = positionPanel(anchor, panel, () => this.element === panel);
|
|
1905
|
+
this.detachDismiss = listenPanelDismiss(anchor, panel, () => this.close());
|
|
1906
|
+
(_a = this.onOpenChange) == null ? void 0 : _a.call(this, true);
|
|
1907
|
+
}
|
|
1908
|
+
close() {
|
|
1909
|
+
var _a, _b, _c;
|
|
1910
|
+
if (!this.element) return;
|
|
1911
|
+
(_a = this.detachDismiss) == null ? void 0 : _a.call(this);
|
|
1912
|
+
this.detachDismiss = null;
|
|
1913
|
+
(_b = this.floating) == null ? void 0 : _b.stop();
|
|
1914
|
+
this.floating = null;
|
|
1915
|
+
this.element.remove();
|
|
1916
|
+
this.element = null;
|
|
1917
|
+
this.cells = [];
|
|
1918
|
+
this.cursorRow = -1;
|
|
1919
|
+
this.cursorCol = -1;
|
|
1920
|
+
(_c = this.onOpenChange) == null ? void 0 : _c.call(this, false);
|
|
1921
|
+
}
|
|
1922
|
+
destroy() {
|
|
1923
|
+
this.close();
|
|
1924
|
+
}
|
|
1925
|
+
t(key, fallback) {
|
|
1926
|
+
return tOrFallback(this.i18n, key, fallback);
|
|
1927
|
+
}
|
|
1928
|
+
build() {
|
|
1929
|
+
const panel = document.createElement("div");
|
|
1930
|
+
panel.className = "atri-table-panel";
|
|
1931
|
+
panel.setAttribute("role", "dialog");
|
|
1932
|
+
panel.setAttribute("aria-label", this.t("editor.table", "表格"));
|
|
1933
|
+
panel.tabIndex = -1;
|
|
1934
|
+
const grid = document.createElement("div");
|
|
1935
|
+
grid.className = "atri-table-panel-grid";
|
|
1936
|
+
this.cells = [];
|
|
1937
|
+
for (let row = 0; row < MAX_ROWS; row++) {
|
|
1938
|
+
for (let col = 0; col < MAX_COLS; col++) {
|
|
1939
|
+
const cell = document.createElement("div");
|
|
1940
|
+
cell.className = "atri-table-panel-cell";
|
|
1941
|
+
cell.setAttribute("role", "gridcell");
|
|
1942
|
+
cell.dataset.row = String(row);
|
|
1943
|
+
cell.dataset.col = String(col);
|
|
1944
|
+
cell.addEventListener("mouseenter", () => this.highlight(row + 1, col + 1));
|
|
1945
|
+
cell.addEventListener("click", () => this.insert(row + 1, col + 1));
|
|
1946
|
+
grid.appendChild(cell);
|
|
1947
|
+
this.cells.push(cell);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
const hint = document.createElement("div");
|
|
1951
|
+
hint.className = "atri-table-panel-hint";
|
|
1952
|
+
hint.textContent = this.t("editor.table", "表格");
|
|
1953
|
+
panel.addEventListener("keydown", (event) => this.onKeyDown(event));
|
|
1954
|
+
panel.appendChild(grid);
|
|
1955
|
+
panel.appendChild(hint);
|
|
1956
|
+
document.body.appendChild(panel);
|
|
1957
|
+
this.element = panel;
|
|
1958
|
+
copyThemeClasses(this.editor.view.dom, panel);
|
|
1959
|
+
panel.focus();
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* 高亮左上角 (rows × cols) 区域并在提示行显示尺寸;-1 清空
|
|
1963
|
+
*/
|
|
1964
|
+
highlight(rows, cols) {
|
|
1965
|
+
var _a;
|
|
1966
|
+
this.cursorRow = rows < 0 ? -1 : rows - 1;
|
|
1967
|
+
this.cursorCol = cols < 0 ? -1 : cols - 1;
|
|
1968
|
+
this.cells.forEach((cell) => {
|
|
1969
|
+
const r = Number(cell.dataset.row);
|
|
1970
|
+
const c = Number(cell.dataset.col);
|
|
1971
|
+
const active = rows > 0 && cols > 0 && r < rows && c < cols;
|
|
1972
|
+
cell.classList.toggle("atri-table-panel-cell--active", active);
|
|
1973
|
+
if (active) {
|
|
1974
|
+
cell.setAttribute("aria-selected", "true");
|
|
1975
|
+
} else {
|
|
1976
|
+
cell.removeAttribute("aria-selected");
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
const hint = (_a = this.element) == null ? void 0 : _a.querySelector(".atri-table-panel-hint");
|
|
1980
|
+
if (hint) {
|
|
1981
|
+
hint.textContent = rows > 0 ? `${rows} × ${cols}` : this.t("editor.table", "表格");
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
insert(rows, cols) {
|
|
1985
|
+
this.editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run();
|
|
1986
|
+
this.close();
|
|
1987
|
+
}
|
|
1988
|
+
onKeyDown(event) {
|
|
1989
|
+
const deltas = {
|
|
1990
|
+
ArrowLeft: [0, -1],
|
|
1991
|
+
ArrowRight: [0, 1],
|
|
1992
|
+
ArrowUp: [-1, 0],
|
|
1993
|
+
ArrowDown: [1, 0]
|
|
1874
1994
|
};
|
|
1875
|
-
|
|
1876
|
-
if (event.key !== "Escape") return;
|
|
1995
|
+
if (event.key === "Enter") {
|
|
1877
1996
|
event.preventDefault();
|
|
1878
|
-
|
|
1879
|
-
this.
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1997
|
+
if (this.cursorRow < 0 || this.cursorCol < 0) return;
|
|
1998
|
+
this.insert(this.cursorRow + 1, this.cursorCol + 1);
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
const delta = deltas[event.key];
|
|
2002
|
+
if (!delta) return;
|
|
2003
|
+
event.preventDefault();
|
|
2004
|
+
const baseRow = this.cursorRow < 0 ? 0 : this.cursorRow;
|
|
2005
|
+
const baseCol = this.cursorCol < 0 ? 0 : this.cursorCol;
|
|
2006
|
+
const row = Math.min(MAX_ROWS - 1, Math.max(0, baseRow + delta[0]));
|
|
2007
|
+
const col = Math.min(MAX_COLS - 1, Math.max(0, baseCol + delta[1]));
|
|
2008
|
+
this.highlight(row + 1, col + 1);
|
|
1887
2009
|
}
|
|
1888
2010
|
}
|
|
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
|
+
}
|
|
1889
2017
|
const icons = {
|
|
1890
2018
|
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>',
|
|
1891
2019
|
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>',
|
|
@@ -1908,6 +2036,7 @@ const icons = {
|
|
|
1908
2036
|
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
2037
|
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
2038
|
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
|
+
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>',
|
|
1911
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>'
|
|
1912
2041
|
};
|
|
1913
2042
|
const TOOLTIP_KEYS = {
|
|
@@ -1932,6 +2061,7 @@ const TOOLTIP_KEYS = {
|
|
|
1932
2061
|
insertImage: "editor.image",
|
|
1933
2062
|
insertAttachment: "editor.attachment",
|
|
1934
2063
|
attachmentDisplay: "editor.attachmentDisplay",
|
|
2064
|
+
insertTable: "editor.table",
|
|
1935
2065
|
delete: "editor.delete"
|
|
1936
2066
|
};
|
|
1937
2067
|
class ToolbarManager {
|
|
@@ -1941,6 +2071,7 @@ class ToolbarManager {
|
|
|
1941
2071
|
this.customTooltips = /* @__PURE__ */ new Map();
|
|
1942
2072
|
this.createdElements = [];
|
|
1943
2073
|
this.panel = null;
|
|
2074
|
+
this.tablePanel = null;
|
|
1944
2075
|
this.editor = editor;
|
|
1945
2076
|
this.container = container;
|
|
1946
2077
|
this.config = config;
|
|
@@ -2001,6 +2132,23 @@ class ToolbarManager {
|
|
|
2001
2132
|
var _a;
|
|
2002
2133
|
(_a = this.insertPanel) == null ? void 0 : _a.toggle(button, mode);
|
|
2003
2134
|
}
|
|
2135
|
+
/**
|
|
2136
|
+
* 表格网格面板按需创建:没点开过就不该有一个实例挂在编辑器上
|
|
2137
|
+
*/
|
|
2138
|
+
get gridPanel() {
|
|
2139
|
+
if (!this.tableExtensionRegistered) return null;
|
|
2140
|
+
this.tablePanel ?? (this.tablePanel = new TableGridPanel({
|
|
2141
|
+
editor: this.editor,
|
|
2142
|
+
i18n: this.i18n,
|
|
2143
|
+
// 开合不改文档,等不到 transaction,得主动刷一次按钮状态
|
|
2144
|
+
onOpenChange: () => this.updateButtonStates()
|
|
2145
|
+
}));
|
|
2146
|
+
return this.tablePanel;
|
|
2147
|
+
}
|
|
2148
|
+
/** 内置表格真可用才摆按钮:table:false 未注册、或被用户同名扩展顶掉都不摆(判定见 canInsertTable) */
|
|
2149
|
+
get tableExtensionRegistered() {
|
|
2150
|
+
return canInsertTable(this.editor);
|
|
2151
|
+
}
|
|
2004
2152
|
/**
|
|
2005
2153
|
* 创建工具栏 DOM - 直接将按钮插入 container
|
|
2006
2154
|
*/
|
|
@@ -2024,8 +2172,10 @@ class ToolbarManager {
|
|
|
2024
2172
|
["bold", "italic", "underline", "strike", "code"],
|
|
2025
2173
|
["bulletList", "orderedList", "blockquote", "codeBlock"],
|
|
2026
2174
|
["alignLeft", "alignCenter", "alignRight"],
|
|
2027
|
-
//
|
|
2028
|
-
...this.itemDefs.has("
|
|
2175
|
+
// 同媒体组的道理:表格节点没注册时这项不存在,默认布局里也不摆空位
|
|
2176
|
+
...this.itemDefs.has("insertTable") ? [["insertTable"]] : [],
|
|
2177
|
+
// 「附件样式」不再常驻:切换形态改由选中附件时的浮动工具栏承接(bubble 默认开)
|
|
2178
|
+
...this.itemDefs.has("insertImage") ? [["insertImage", "insertAttachment"]] : []
|
|
2029
2179
|
];
|
|
2030
2180
|
layout.forEach((group, groupIndex) => {
|
|
2031
2181
|
if (groupIndex > 0) {
|
|
@@ -2075,11 +2225,6 @@ class ToolbarManager {
|
|
|
2075
2225
|
} else {
|
|
2076
2226
|
button.innerHTML = itemDef.icon;
|
|
2077
2227
|
}
|
|
2078
|
-
const svg = button.querySelector("svg");
|
|
2079
|
-
if (svg) {
|
|
2080
|
-
svg.setAttribute("width", "18");
|
|
2081
|
-
svg.setAttribute("height", "18");
|
|
2082
|
-
}
|
|
2083
2228
|
if (overrides == null ? void 0 : overrides.tooltip) {
|
|
2084
2229
|
this.customTooltips.set(itemId, overrides.tooltip);
|
|
2085
2230
|
}
|
|
@@ -2111,8 +2256,7 @@ class ToolbarManager {
|
|
|
2111
2256
|
if (!itemDef) return;
|
|
2112
2257
|
const custom = this.customTooltips.get(itemId);
|
|
2113
2258
|
const key = TOOLTIP_KEYS[itemId];
|
|
2114
|
-
const
|
|
2115
|
-
const title = custom ?? (translated && translated !== key ? translated : itemDef.tooltip);
|
|
2259
|
+
const title = custom ?? (key ? ((_a = this.i18n) == null ? void 0 : _a.tOr(key, itemDef.tooltip)) ?? itemDef.tooltip : itemDef.tooltip);
|
|
2116
2260
|
buttons.forEach((button) => {
|
|
2117
2261
|
button.title = title;
|
|
2118
2262
|
});
|
|
@@ -2157,6 +2301,23 @@ class ToolbarManager {
|
|
|
2157
2301
|
isDisabled: (editor) => !editor.isActive("attachment") && !editor.isActive("attachmentLink")
|
|
2158
2302
|
});
|
|
2159
2303
|
}
|
|
2304
|
+
if (this.tableExtensionRegistered) {
|
|
2305
|
+
items.set("insertTable", {
|
|
2306
|
+
id: "insertTable",
|
|
2307
|
+
icon: icons.table,
|
|
2308
|
+
tooltip: "表格",
|
|
2309
|
+
popup: true,
|
|
2310
|
+
command: (_editor, button) => {
|
|
2311
|
+
var _a;
|
|
2312
|
+
return (_a = this.gridPanel) == null ? void 0 : _a.toggle(button);
|
|
2313
|
+
},
|
|
2314
|
+
isActive: () => {
|
|
2315
|
+
var _a;
|
|
2316
|
+
return ((_a = this.tablePanel) == null ? void 0 : _a.isOpen) === true;
|
|
2317
|
+
},
|
|
2318
|
+
isDisabled: (editor) => !editor.isEditable
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2160
2321
|
items.set("delete", {
|
|
2161
2322
|
id: "delete",
|
|
2162
2323
|
icon: icons.trash,
|
|
@@ -2343,11 +2504,13 @@ class ToolbarManager {
|
|
|
2343
2504
|
* 销毁工具栏
|
|
2344
2505
|
*/
|
|
2345
2506
|
destroy() {
|
|
2346
|
-
var _a, _b;
|
|
2507
|
+
var _a, _b, _c;
|
|
2347
2508
|
(_a = this.unsubscribeLanguage) == null ? void 0 : _a.call(this);
|
|
2348
2509
|
this.unsubscribeLanguage = void 0;
|
|
2349
2510
|
(_b = this.panel) == null ? void 0 : _b.destroy();
|
|
2350
2511
|
this.panel = null;
|
|
2512
|
+
(_c = this.tablePanel) == null ? void 0 : _c.destroy();
|
|
2513
|
+
this.tablePanel = null;
|
|
2351
2514
|
this.buttons.clear();
|
|
2352
2515
|
this.customTooltips.clear();
|
|
2353
2516
|
this.createdElements.forEach((el) => el.remove());
|
|
@@ -2356,10 +2519,7 @@ class ToolbarManager {
|
|
|
2356
2519
|
}
|
|
2357
2520
|
class MediaStatusStrip {
|
|
2358
2521
|
constructor(runtime, container, i18n) {
|
|
2359
|
-
const t = (key, fallback, options) =>
|
|
2360
|
-
const translated = i18n == null ? void 0 : i18n.t(key, options);
|
|
2361
|
-
return translated && translated !== key ? translated : fallback;
|
|
2362
|
-
};
|
|
2522
|
+
const t = (key, fallback, options) => (i18n == null ? void 0 : i18n.tOr(key, fallback, options)) ?? fallback;
|
|
2363
2523
|
this.labels = {
|
|
2364
2524
|
uploading: (files, percent) => t("media.uploading", "上传中 {{files}} 个 · {{percent}}%", { files, percent }),
|
|
2365
2525
|
failed: (files) => t("media.uploadFailed", "{{files}} 个文件上传失败", { files }),
|
|
@@ -2817,29 +2977,10 @@ class AICommandMenuManager {
|
|
|
2817
2977
|
this.hide();
|
|
2818
2978
|
}
|
|
2819
2979
|
}
|
|
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
2980
|
class AtriEditor {
|
|
2841
2981
|
constructor(options) {
|
|
2842
2982
|
this.toolbarManager = null;
|
|
2983
|
+
this.toolbarContainer = null;
|
|
2843
2984
|
this.aiService = null;
|
|
2844
2985
|
this.aiMenuManager = null;
|
|
2845
2986
|
this.mediaStatus = null;
|
|
@@ -2855,65 +2996,24 @@ class AtriEditor {
|
|
|
2855
2996
|
if (options.nodeViews && options.nodeViews.length > 0) {
|
|
2856
2997
|
this.extensionManager.registerNodeViews(options.nodeViews);
|
|
2857
2998
|
}
|
|
2858
|
-
let toolbarContainer = null;
|
|
2859
2999
|
if (options.toolbar !== false) {
|
|
2860
|
-
toolbarContainer = createContainer("atri-editor-toolbar");
|
|
2861
|
-
this.container.appendChild(toolbarContainer);
|
|
3000
|
+
this.toolbarContainer = createContainer("atri-editor-toolbar");
|
|
3001
|
+
this.container.appendChild(this.toolbarContainer);
|
|
2862
3002
|
}
|
|
2863
|
-
const bubbleElement = this.ensureBubbleElement(options.toolbar);
|
|
2864
3003
|
const editorElement = createContainer("atri-editor-content-wrapper");
|
|
2865
3004
|
this.container.appendChild(editorElement);
|
|
2866
3005
|
this.mediaRuntime = options.media === false ? null : new MediaRuntime(options.media);
|
|
2867
3006
|
this.mediaStatus = this.mediaRuntime ? new MediaStatusStrip(this.mediaRuntime, this.container, this.i18nManager) : null;
|
|
2868
|
-
this.
|
|
3007
|
+
this.createCoreEditor({
|
|
2869
3008
|
element: editorElement,
|
|
2870
3009
|
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
|
-
}
|
|
3010
|
+
contentFormat: options.contentFormat
|
|
2898
3011
|
});
|
|
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
|
-
}
|
|
3012
|
+
this.setupSubsystems();
|
|
2913
3013
|
}
|
|
2914
|
-
/**
|
|
3014
|
+
/** bubble 默认开,显式 false 才不建元素;元素故意不插进文档:挂载与定位全归 BubbleMenu 插件管 */
|
|
2915
3015
|
ensureBubbleElement(toolbar) {
|
|
2916
|
-
if (toolbar === false ||
|
|
3016
|
+
if (toolbar === false || (toolbar == null ? void 0 : toolbar.bubble) === false) return null;
|
|
2917
3017
|
this.bubbleElement ?? (this.bubbleElement = createContainer("atri-editor-bubble-toolbar"));
|
|
2918
3018
|
return this.bubbleElement;
|
|
2919
3019
|
}
|
|
@@ -2932,7 +3032,7 @@ class AtriEditor {
|
|
|
2932
3032
|
this.aiMenuManager = new AICommandMenuManager(
|
|
2933
3033
|
this.editor,
|
|
2934
3034
|
this.options.ai.functions,
|
|
2935
|
-
"/",
|
|
3035
|
+
this.options.ai.triggerChar ?? "/",
|
|
2936
3036
|
(func) => {
|
|
2937
3037
|
var _a2;
|
|
2938
3038
|
(_a2 = this.aiService) == null ? void 0 : _a2.execute(func.id);
|
|
@@ -2945,76 +3045,91 @@ class AtriEditor {
|
|
|
2945
3045
|
this.aiService = new AIService(this.editor, config, this.markdownService);
|
|
2946
3046
|
}
|
|
2947
3047
|
/**
|
|
2948
|
-
*
|
|
2949
|
-
*
|
|
3048
|
+
* 创建核心编辑器:构造函数与 recreateEditor 共用的装配点。
|
|
3049
|
+
* 只有编辑器壳(挂载元素/初始内容/内容格式)随重建变化,其余全部来自 this.options,
|
|
3050
|
+
* 逐项手抄两遍必然漂移——出错的第一现场就是两处不齐
|
|
2950
3051
|
*/
|
|
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);
|
|
3052
|
+
createCoreEditor(init) {
|
|
2971
3053
|
this.coreEditor = new CoreEditor({
|
|
2972
|
-
element:
|
|
2973
|
-
content:
|
|
2974
|
-
|
|
2975
|
-
contentFormat: "html",
|
|
3054
|
+
element: init.element,
|
|
3055
|
+
content: init.content,
|
|
3056
|
+
contentFormat: init.contentFormat,
|
|
2976
3057
|
editable: this.options.editable,
|
|
2977
3058
|
placeholder: this.options.placeholder,
|
|
2978
3059
|
extensions: [...this.options.extensions || [], ...this.extensionManager.getAll()],
|
|
2979
3060
|
markdown: this.options.markdown,
|
|
2980
3061
|
media: this.options.media,
|
|
3062
|
+
table: this.options.table,
|
|
2981
3063
|
mediaRuntime: this.mediaRuntime ?? void 0,
|
|
2982
|
-
bubbleElement,
|
|
3064
|
+
bubbleElement: this.ensureBubbleElement(this.options.toolbar),
|
|
2983
3065
|
onCreate: () => {
|
|
2984
3066
|
this.onEditorCreated();
|
|
2985
3067
|
},
|
|
2986
3068
|
onUpdate: () => {
|
|
2987
|
-
var
|
|
2988
|
-
(
|
|
3069
|
+
var _a, _b;
|
|
3070
|
+
(_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this);
|
|
2989
3071
|
},
|
|
2990
3072
|
onFocus: () => {
|
|
2991
|
-
var
|
|
2992
|
-
(
|
|
3073
|
+
var _a, _b;
|
|
3074
|
+
(_b = (_a = this.options).onFocus) == null ? void 0 : _b.call(_a, this);
|
|
2993
3075
|
},
|
|
2994
3076
|
onBlur: () => {
|
|
2995
|
-
var
|
|
2996
|
-
(
|
|
3077
|
+
var _a, _b;
|
|
3078
|
+
(_b = (_a = this.options).onBlur) == null ? void 0 : _b.call(_a, this);
|
|
2997
3079
|
},
|
|
2998
3080
|
onDestroy: () => {
|
|
2999
|
-
var
|
|
3000
|
-
(
|
|
3081
|
+
var _a, _b;
|
|
3082
|
+
(_b = (_a = this.options).onDestroy) == null ? void 0 : _b.call(_a, this);
|
|
3001
3083
|
}
|
|
3002
3084
|
});
|
|
3085
|
+
}
|
|
3086
|
+
/** 初始化依赖编辑器实例的子系统:Markdown 服务、工具栏、AI 服务,重建后同样要重新走一遍 */
|
|
3087
|
+
setupSubsystems() {
|
|
3003
3088
|
this.markdownService = new MarkdownService(this.editor, this.options.markdown);
|
|
3004
|
-
if (toolbarContainer && this.options.toolbar !== false) {
|
|
3089
|
+
if (this.toolbarContainer && this.options.toolbar !== false) {
|
|
3005
3090
|
this.toolbarManager = new ToolbarManager(
|
|
3006
3091
|
this.editor,
|
|
3007
|
-
toolbarContainer,
|
|
3092
|
+
this.toolbarContainer,
|
|
3008
3093
|
this.options.toolbar,
|
|
3009
3094
|
this.i18nManager,
|
|
3010
3095
|
this.mediaRuntime
|
|
3011
3096
|
);
|
|
3012
|
-
if (bubbleElement) this.toolbarManager.attachBubbleToolbar(bubbleElement);
|
|
3097
|
+
if (this.bubbleElement) this.toolbarManager.attachBubbleToolbar(this.bubbleElement);
|
|
3013
3098
|
}
|
|
3014
3099
|
if (this.options.ai) {
|
|
3015
3100
|
this.initAI(this.options.ai);
|
|
3016
3101
|
}
|
|
3017
3102
|
}
|
|
3103
|
+
/**
|
|
3104
|
+
* 重新创建编辑器(用于动态注册 NodeView 后更新 schema)
|
|
3105
|
+
* 会整体替换编辑器视图,因此内容与选区都要手动恢复
|
|
3106
|
+
*/
|
|
3107
|
+
recreateEditor() {
|
|
3108
|
+
var _a, _b;
|
|
3109
|
+
const previousEditor = this.coreEditor.getEditor();
|
|
3110
|
+
const currentContent = this.coreEditor.getHTML();
|
|
3111
|
+
const { from, to } = previousEditor.state.selection;
|
|
3112
|
+
this.pendingViewState = { from, to };
|
|
3113
|
+
this.coreEditor.destroy();
|
|
3114
|
+
(_a = this.toolbarManager) == null ? void 0 : _a.destroy();
|
|
3115
|
+
(_b = this.aiMenuManager) == null ? void 0 : _b.destroy();
|
|
3116
|
+
const editorElement = this.container.querySelector(
|
|
3117
|
+
".atri-editor-content-wrapper"
|
|
3118
|
+
);
|
|
3119
|
+
const newEditorElement = createContainer("atri-editor-content-wrapper");
|
|
3120
|
+
if (editorElement) {
|
|
3121
|
+
editorElement.replaceWith(newEditorElement);
|
|
3122
|
+
} else {
|
|
3123
|
+
this.container.appendChild(newEditorElement);
|
|
3124
|
+
}
|
|
3125
|
+
this.createCoreEditor({
|
|
3126
|
+
element: newEditorElement,
|
|
3127
|
+
content: currentContent,
|
|
3128
|
+
// 当前内容取自 getHTML(),重建时按 html 回填,不能沿用用户的 contentFormat
|
|
3129
|
+
contentFormat: "html"
|
|
3130
|
+
});
|
|
3131
|
+
this.setupSubsystems();
|
|
3132
|
+
}
|
|
3018
3133
|
/**
|
|
3019
3134
|
* 获取 Tiptap Editor 实例
|
|
3020
3135
|
*/
|
|
@@ -3103,12 +3218,7 @@ class AtriEditor {
|
|
|
3103
3218
|
* 获取选中文本
|
|
3104
3219
|
*/
|
|
3105
3220
|
getSelectedText() {
|
|
3106
|
-
|
|
3107
|
-
if (selection.empty) return "";
|
|
3108
|
-
return getTextBetween(this.editor.state.doc, {
|
|
3109
|
-
from: selection.from,
|
|
3110
|
-
to: selection.to
|
|
3111
|
-
});
|
|
3221
|
+
return getSelectedText(this.editor);
|
|
3112
3222
|
}
|
|
3113
3223
|
/**
|
|
3114
3224
|
* 插入内容
|
|
@@ -3137,6 +3247,17 @@ class AtriEditor {
|
|
|
3137
3247
|
chain.setAttachment(options).run();
|
|
3138
3248
|
}
|
|
3139
3249
|
}
|
|
3250
|
+
/**
|
|
3251
|
+
* 在选区处插入表格(默认 3×3 带表头)
|
|
3252
|
+
*/
|
|
3253
|
+
insertTable(options) {
|
|
3254
|
+
if (!canInsertTable(this.editor)) {
|
|
3255
|
+
console.warn("[Atri Editor] insertTable() ignored: table extensions are disabled.");
|
|
3256
|
+
return;
|
|
3257
|
+
}
|
|
3258
|
+
const { rows = 3, cols = 3, withHeaderRow = true } = options ?? {};
|
|
3259
|
+
this.editor.chain().focus().insertTable({ rows, cols, withHeaderRow }).run();
|
|
3260
|
+
}
|
|
3140
3261
|
/**
|
|
3141
3262
|
* 走上传管线插入本地文件
|
|
3142
3263
|
* kind 缺省时按 MIME 分流;promise 在这批文件全部落定(成功或失败)后 resolve
|
|
@@ -3430,6 +3551,13 @@ class AtriEditorElement extends AtriEditorBase {
|
|
|
3430
3551
|
var _a;
|
|
3431
3552
|
(_a = this.editor) == null ? void 0 : _a.insertAttachment(options);
|
|
3432
3553
|
}
|
|
3554
|
+
/**
|
|
3555
|
+
* 在选区处插入表格
|
|
3556
|
+
*/
|
|
3557
|
+
insertTable(options) {
|
|
3558
|
+
var _a;
|
|
3559
|
+
(_a = this.editor) == null ? void 0 : _a.insertTable(options);
|
|
3560
|
+
}
|
|
3433
3561
|
/**
|
|
3434
3562
|
* 走上传管线插入本地文件
|
|
3435
3563
|
*/
|
|
@@ -3480,7 +3608,6 @@ export {
|
|
|
3480
3608
|
getCursorContext,
|
|
3481
3609
|
getDocumentText,
|
|
3482
3610
|
getSelectedText,
|
|
3483
|
-
isBrowser,
|
|
3484
3611
|
registerAtriElement,
|
|
3485
3612
|
resolveElement,
|
|
3486
3613
|
simpleHtmlToMarkdown,
|