@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +5 -5
  3. package/dist/AtriEditor.d.ts +23 -5
  4. package/dist/AtriEditor.d.ts.map +1 -1
  5. package/dist/AtriEditorElement.d.ts +5 -1
  6. package/dist/AtriEditorElement.d.ts.map +1 -1
  7. package/dist/ai/AICommandMenu.d.ts.map +1 -1
  8. package/dist/core/CoreEditor.d.ts +5 -1
  9. package/dist/core/CoreEditor.d.ts.map +1 -1
  10. package/dist/core/ExtensionManager.d.ts +2 -14
  11. package/dist/core/ExtensionManager.d.ts.map +1 -1
  12. package/dist/core/I18nManager.d.ts +9 -0
  13. package/dist/core/I18nManager.d.ts.map +1 -1
  14. package/dist/core/LinkPanel.d.ts +73 -0
  15. package/dist/core/LinkPanel.d.ts.map +1 -0
  16. package/dist/core/TableGridPanel.d.ts +41 -0
  17. package/dist/core/TableGridPanel.d.ts.map +1 -0
  18. package/dist/core/TableMenu.d.ts +67 -0
  19. package/dist/core/TableMenu.d.ts.map +1 -0
  20. package/dist/core/ThemeManager.d.ts +3 -10
  21. package/dist/core/ThemeManager.d.ts.map +1 -1
  22. package/dist/core/ToolbarManager.d.ts +15 -2
  23. package/dist/core/ToolbarManager.d.ts.map +1 -1
  24. package/dist/core/floating-panel.d.ts +20 -0
  25. package/dist/core/floating-panel.d.ts.map +1 -0
  26. package/dist/core/icons.d.ts.map +1 -1
  27. package/dist/extensions/Attachment.d.ts.map +1 -1
  28. package/dist/extensions/AttachmentLink.d.ts.map +1 -1
  29. package/dist/extensions/MediaFiles.d.ts.map +1 -1
  30. package/dist/extensions/attachment-markdown.d.ts +22 -0
  31. package/dist/extensions/attachment-markdown.d.ts.map +1 -0
  32. package/dist/extensions/table.d.ts +23 -0
  33. package/dist/extensions/table.d.ts.map +1 -0
  34. package/dist/index.d.ts +1 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1131 -350
  37. package/dist/index.js.map +1 -1
  38. package/dist/media/InsertPanel.d.ts +5 -6
  39. package/dist/media/InsertPanel.d.ts.map +1 -1
  40. package/dist/media/MediaStatusStrip.d.ts.map +1 -1
  41. package/dist/media/Uploader.d.ts.map +1 -1
  42. package/dist/style.css +183 -0
  43. package/dist/types/ai.d.ts +2 -0
  44. package/dist/types/ai.d.ts.map +1 -1
  45. package/dist/types/editor.d.ts +52 -7
  46. package/dist/types/editor.d.ts.map +1 -1
  47. package/dist/types/extension.d.ts +16 -34
  48. package/dist/types/extension.d.ts.map +1 -1
  49. package/dist/types/index.d.ts +0 -1
  50. package/dist/types/index.d.ts.map +1 -1
  51. package/dist/utils/dom.d.ts +3 -2
  52. package/dist/utils/dom.d.ts.map +1 -1
  53. package/dist/utils/table.d.ts +24 -0
  54. package/dist/utils/table.d.ts.map +1 -0
  55. package/package.json +24 -5
  56. package/dist/types/events.d.ts +0 -26
  57. 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
- const markdownPattern$1 = /^!file\[([^\]]*)\]\(\s*([^\s)]+)(?:\s+"([^"]*)")?\s*\)(?:\n|$)/;
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
- markdownTokenName: "file",
355
- markdownTokenizer: {
426
+ ...makeAttachmentMarkdown({
356
427
  name: "file",
357
428
  level: "block",
358
- start: (src) => src.indexOf("!file["),
359
- tokenize: (src) => {
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
- markdownTokenName: "filelink",
433
- markdownTokenizer: {
474
+ ...makeAttachmentMarkdown({
434
475
  name: "filelink",
435
476
  level: "inline",
436
- start: (src) => src.indexOf("!filelink["),
437
- tokenize: (src) => {
438
- const match = markdownPattern.exec(src);
439
- if (!match) return void 0;
440
- return {
441
- type: "filelink",
442
- raw: match[0],
443
- text: match[1] ?? "",
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
- function filesOf$1(transfer) {
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$1(event.clipboardData);
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$1(dragEvent.dataTransfer);
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 candidates = [record.url, record.location, (_a = record.data) == null ? void 0 : _a.url];
540
- const url = candidates.find((value) => typeof value === "string" && value !== "");
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 = ((_a = this.config.markdown) == null ? void 0 : _a.enabled) !== false;
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: (_b = this.config.markdown) == null ? void 0 : _b.indentation,
1058
- markedOptions: (_c = this.config.markdown) == null ? void 0 : _c.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
- const enableInputRules = ((_d = this.config.markdown) == null ? void 0 : _d.shortcuts) !== false;
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
- (_e = this.mediaRuntime) == null ? void 0 : _e.bind(editor);
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("[CoreEditor] getHTML() failed:", err);
1141
- console.error("[CoreEditor] Schema nodes:", Object.keys(this.editor.schema.nodes));
1142
- console.error("[CoreEditor] Schema marks:", Object.keys(this.editor.schema.marks));
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
- if (this.currentTheme === "light" || this.currentTheme === "dark") {
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, _meta) {
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
- clear() {
1663
- this.extensions.clear();
1664
- this.nodeViews.clear();
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.stopAutoUpdate = null;
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.stopAutoUpdate) == null ? void 0 : _b.call(this);
1713
- this.stopAutoUpdate = null;
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
- var _a;
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.applyTheme(panel);
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
- async position(anchor) {
1957
+ position(anchor) {
1844
1958
  const panel = this.element;
1845
1959
  if (!panel) return;
1846
- const update = async () => {
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 onPointerDown = (event) => {
1870
- var _a;
1871
- const target = event.target;
1872
- if (!target || ((_a = this.element) == null ? void 0 : _a.contains(target)) || anchor.contains(target)) return;
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
- const onKeyDown = (event) => {
1876
- if (event.key !== "Escape") return;
2088
+ if (event.key === "Enter") {
1877
2089
  event.preventDefault();
1878
- event.stopPropagation();
1879
- this.close();
1880
- };
1881
- document.addEventListener("pointerdown", onPointerDown, true);
1882
- document.addEventListener("keydown", onKeyDown, true);
1883
- this.detachDismiss = () => {
1884
- document.removeEventListener("pointerdown", onPointerDown, true);
1885
- document.removeEventListener("keydown", onKeyDown, true);
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
- 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>'
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
- ["bold", "italic", "underline", "strike", "code"],
2025
- ["bulletList", "orderedList", "blockquote", "codeBlock"],
2026
- ["alignLeft", "alignCenter", "alignRight"],
2027
- // 媒体节点没注册时这两项压根不存在,默认布局里也不摆空位
2028
- ...this.itemDefs.has("insertImage") ? [["insertImage", "insertAttachment", "attachmentDisplay"]] : []
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 translated = key ? (_a = this.i18n) == null ? void 0 : _a.t(key) : void 0;
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
- class MediaStatusStrip {
2358
- 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
- };
2363
- this.labels = {
2364
- uploading: (files, percent) => t("media.uploading", "上传中 {{files}} 个 · {{percent}}%", { files, percent }),
2365
- failed: (files) => t("media.uploadFailed", "{{files}} 个文件上传失败", { files }),
2366
- inline: (files) => t("media.inlined", "{{files}} 张图片未上传(已随文档保存)", { files }),
2367
- retry: () => t("media.retry", "重试")
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.element = document.createElement("div");
2370
- this.element.className = "atri-media-status";
2371
- this.element.setAttribute("role", "status");
2372
- this.element.hidden = true;
2373
- this.text = document.createElement("span");
2374
- this.text.className = "atri-media-status-text";
2375
- this.retry = document.createElement("button");
2376
- this.retry.type = "button";
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
- destroy() {
2386
- var _a, _b;
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
- render(state) {
2392
- const { failed, inline } = state;
2393
- const parts = [];
2394
- if (state.uploading) parts.push(this.labels.uploading(state.uploading, state.percent));
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.coreEditor = new CoreEditor({
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.markdownService = new MarkdownService(this.editor, options.markdown);
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
- /** 开了 bubble 才建元素,且故意不插进文档:挂载与定位全归 BubbleMenu 插件管 */
3643
+ /** bubble 默认开,显式 false 才不建元素;元素故意不插进文档:挂载与定位全归 BubbleMenu 插件管 */
2915
3644
  ensureBubbleElement(toolbar) {
2916
- if (toolbar === false || !(toolbar == null ? void 0 : toolbar.bubble)) return null;
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
- * 重新创建编辑器(用于动态注册 NodeView 后更新 schema)
2949
- * 会整体替换编辑器视图,因此内容与选区都要手动恢复
3677
+ * 创建核心编辑器:构造函数与 recreateEditor 共用的装配点。
3678
+ * 只有编辑器壳(挂载元素/初始内容/内容格式)随重建变化,其余全部来自 this.options,
3679
+ * 逐项手抄两遍必然漂移——出错的第一现场就是两处不齐
2950
3680
  */
2951
- recreateEditor() {
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: newEditorElement,
2973
- content: currentContent,
2974
- // 当前内容取自 getHTML(),重建时按 html 回填,不能沿用用户的 contentFormat
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 _a2, _b2;
2988
- (_b2 = (_a2 = this.options).onChange) == null ? void 0 : _b2.call(_a2, this);
3699
+ var _a, _b;
3700
+ (_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a, this);
2989
3701
  },
2990
3702
  onFocus: () => {
2991
- var _a2, _b2;
2992
- (_b2 = (_a2 = this.options).onFocus) == null ? void 0 : _b2.call(_a2, this);
3703
+ var _a, _b;
3704
+ (_b = (_a = this.options).onFocus) == null ? void 0 : _b.call(_a, this);
2993
3705
  },
2994
3706
  onBlur: () => {
2995
- var _a2, _b2;
2996
- (_b2 = (_a2 = this.options).onBlur) == null ? void 0 : _b2.call(_a2, this);
3707
+ var _a, _b;
3708
+ (_b = (_a = this.options).onBlur) == null ? void 0 : _b.call(_a, this);
2997
3709
  },
2998
3710
  onDestroy: () => {
2999
- var _a2, _b2;
3000
- (_b2 = (_a2 = this.options).onDestroy) == null ? void 0 : _b2.call(_a2, this);
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
- if (toolbarContainer && this.options.toolbar !== false) {
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
- const { selection } = this.editor.state;
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.aiMenuManager) == null ? void 0 : _b.destroy();
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
- (_c = this.mediaStatus) == null ? void 0 : _c.destroy();
3283
- (_d = this.mediaRuntime) == null ? void 0 : _d.destroy();
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,