@atri-editor/core 0.2.0 → 0.3.0

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