@atri-editor/core 0.1.0 → 0.2.0

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