@beyondwork/docx-react-component 1.0.54 → 1.0.56

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.
@@ -220,6 +220,12 @@ import {
220
220
  getCursorColorForUser,
221
221
  setLocalCursorState,
222
222
  } from "../runtime/collab/remote-cursor-awareness.ts";
223
+ import {
224
+ stableChartId,
225
+ } from "../runtime/chart/chart-model-store.ts";
226
+ import {
227
+ projectChartSnapshot,
228
+ } from "../runtime/chart/chart-snapshot.ts";
223
229
 
224
230
  export {
225
231
  __createFallbackRuntime,
@@ -466,6 +472,40 @@ async function runConvertScopesToExternal(args: {
466
472
 
467
473
  // ---------------------------------------------------------------------------
468
474
 
475
+ type CanonicalDocType = ReturnType<WordReviewEditorRuntime["getCanonicalDocument"]>;
476
+
477
+ function collectChartSnapshots(doc: CanonicalDocType): import("../api/public-types").ChartSnapshot[] {
478
+ const results: import("../api/public-types").ChartSnapshot[] = [];
479
+ collectChartSnapshotsFromBlocks(doc.content.children, results);
480
+ return results;
481
+ }
482
+
483
+ function collectChartSnapshotsFromBlocks(
484
+ blocks: CanonicalDocType["content"]["children"],
485
+ results: import("../api/public-types").ChartSnapshot[],
486
+ ): void {
487
+ for (const block of blocks) {
488
+ if (block.type === "paragraph") {
489
+ for (const inline of block.children) {
490
+ if (inline.type === "chart_preview" && inline.parsedData) {
491
+ const chartId = stableChartId(inline.rawXml);
492
+ results.push(projectChartSnapshot(chartId, inline.parsedData));
493
+ }
494
+ }
495
+ } else if (block.type === "table") {
496
+ for (const row of block.rows) {
497
+ for (const cell of row.cells) {
498
+ collectChartSnapshotsFromBlocks(cell.children, results);
499
+ }
500
+ }
501
+ } else if (block.type === "sdt" || block.type === "custom_xml") {
502
+ collectChartSnapshotsFromBlocks(block.children, results);
503
+ }
504
+ }
505
+ }
506
+
507
+ // ---------------------------------------------------------------------------
508
+
469
509
  export function __createWordReviewEditorRefBridge(
470
510
  runtime: WordReviewEditorRuntime,
471
511
  mountedSurface?: TwProseMirrorSurfaceRef | null,
@@ -514,6 +554,12 @@ export function __createWordReviewEditorRefBridge(
514
554
  getRenderSnapshot: () => clonePublicValue(runtime.getRenderSnapshot()),
515
555
  getCompatibilityReport: () => runtime.getCompatibilityReport(),
516
556
  getWarnings: () => runtime.getWarnings(),
557
+ getChartSnapshot: (chartId) => {
558
+ return collectChartSnapshots(runtime.getCanonicalDocument()).find(
559
+ (s) => s.chartId === chartId,
560
+ ) ?? null;
561
+ },
562
+ getChartSnapshots: () => collectChartSnapshots(runtime.getCanonicalDocument()),
517
563
  getCommentSidebarSnapshot: () =>
518
564
  clonePublicValue(runtime.getRenderSnapshot().comments),
519
565
  getTrackedChangesSnapshot: () =>
@@ -1537,6 +1583,11 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
1537
1583
  getRenderSnapshot: () => clonePublicValue(activeRuntime.getRenderSnapshot()),
1538
1584
  getCompatibilityReport: () => activeRuntime.getCompatibilityReport(),
1539
1585
  getWarnings: () => activeRuntime.getWarnings(),
1586
+ getChartSnapshot: (chartId) =>
1587
+ collectChartSnapshots(activeRuntime.getCanonicalDocument()).find(
1588
+ (s) => s.chartId === chartId,
1589
+ ) ?? null,
1590
+ getChartSnapshots: () => collectChartSnapshots(activeRuntime.getCanonicalDocument()),
1540
1591
  getCommentSidebarSnapshot: () =>
1541
1592
  clonePublicValue(activeRuntime.getRenderSnapshot().comments),
1542
1593
  getTrackedChangesSnapshot: () =>
@@ -0,0 +1,90 @@
1
+ /**
2
+ * ProseMirror NodeView for `chart_atom` (Stage 6).
3
+ *
4
+ * When `parsedChartId` is present in the node's attrs, looks up the
5
+ * `ChartStoreEntry` from `chartModelStore` and renders `ChartSurface` into the
6
+ * PM-provided DOM node using `ReactDOM.createRoot`. Falls back to the static
7
+ * `toDOM` output (bitmap preview or badge) when no parsed model is available.
8
+ *
9
+ * Each ChartNodeView instance owns a separate React root so chart renders are
10
+ * isolated and don't interfere with the host React tree. The root is unmounted
11
+ * in `destroy()`.
12
+ */
13
+
14
+ import React from "react";
15
+ import { createRoot, type Root } from "react-dom/client";
16
+ import type { Node as PMNode } from "prosemirror-model";
17
+ import type { NodeViewConstructor } from "prosemirror-view";
18
+ import { ChartSurface } from "../chart/ChartSurface.tsx";
19
+ import { chartModelStore } from "../../runtime/chart/chart-model-store.ts";
20
+
21
+ const DEFAULT_WIDTH = 576;
22
+ const DEFAULT_HEIGHT = 336;
23
+
24
+ class ChartNodeViewInstance {
25
+ readonly dom: HTMLElement;
26
+ private _root: Root | null = null;
27
+
28
+ constructor(node: PMNode) {
29
+ this.dom = document.createElement("span");
30
+ this.dom.setAttribute("contenteditable", "false");
31
+ this.dom.setAttribute("data-node-type", "chart_atom");
32
+ this.dom.className = "inline-block align-baseline mx-0.5 max-w-full";
33
+ this._mount(node);
34
+ }
35
+
36
+ private _mount(node: PMNode): void {
37
+ const parsedChartId = node.attrs.parsedChartId as string | null;
38
+ if (!parsedChartId) return;
39
+
40
+ const entry = chartModelStore.get(parsedChartId);
41
+ if (!entry) return;
42
+
43
+ const width = (node.attrs.widthPx as number | null) ?? entry.widthPx ?? DEFAULT_WIDTH;
44
+ const height = (node.attrs.heightPx as number | null) ?? entry.heightPx ?? DEFAULT_HEIGHT;
45
+
46
+ const el = React.createElement(ChartSurface, {
47
+ model: entry.model,
48
+ width,
49
+ height,
50
+ theme: entry.theme,
51
+ });
52
+
53
+ if (!this._root) {
54
+ this._root = createRoot(this.dom);
55
+ }
56
+ this._root.render(el);
57
+ }
58
+
59
+ update(node: PMNode): boolean {
60
+ const parsedChartId = node.attrs.parsedChartId as string | null;
61
+ if (!parsedChartId) return false;
62
+ this._mount(node);
63
+ return true;
64
+ }
65
+
66
+ destroy(): void {
67
+ if (this._root) {
68
+ this._root.unmount();
69
+ this._root = null;
70
+ }
71
+ }
72
+
73
+ stopEvent(): boolean {
74
+ return false;
75
+ }
76
+
77
+ ignoreMutation(): boolean {
78
+ return true;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * NodeView factory map for `chart_atom`. Merge with `tableNodeViews` when
84
+ * constructing the `EditorView`:
85
+ *
86
+ * new EditorView(mount, { nodeViews: { ...tableNodeViews, ...chartNodeViews }, ... })
87
+ */
88
+ export const chartNodeViews: { [node: string]: NodeViewConstructor } = {
89
+ chart_atom: (node: PMNode) => new ChartNodeViewInstance(node),
90
+ };
@@ -653,9 +653,14 @@ export const editorSchema = new Schema({
653
653
  previewMediaId: { default: null },
654
654
  previewSrc: { default: null },
655
655
  detail: { default: null },
656
+ /** Stage 6: stable chart ID keying into chartModelStore for native SVG render. */
657
+ parsedChartId: { default: null },
658
+ widthPx: { default: null },
659
+ heightPx: { default: null },
656
660
  },
657
661
  toDOM(node) {
658
662
  const previewSrc = node.attrs.previewSrc as string | null;
663
+ const previewMediaId = node.attrs.previewMediaId as string | null;
659
664
  const detail = (node.attrs.detail as string) ?? "Chart";
660
665
  if (previewSrc) {
661
666
  // Bitmap-backed: render the fallback image Word cached in mc:Fallback.
@@ -694,6 +699,7 @@ export const editorSchema = new Schema({
694
699
  {
695
700
  class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-blue-700 bg-blue-50 border border-blue-200",
696
701
  "data-node-type": "chart_atom",
702
+ ...(previewMediaId ? { "data-preview-media-id": previewMediaId } : {}),
697
703
  contenteditable: "false",
698
704
  title: detail,
699
705
  },
@@ -714,6 +720,7 @@ export const editorSchema = new Schema({
714
720
  },
715
721
  toDOM(node) {
716
722
  const previewSrc = node.attrs.previewSrc as string | null;
723
+ const previewMediaId = node.attrs.previewMediaId as string | null;
717
724
  const detail = (node.attrs.detail as string) ?? "SmartArt";
718
725
  if (previewSrc) {
719
726
  return [
@@ -749,6 +756,7 @@ export const editorSchema = new Schema({
749
756
  {
750
757
  class: "inline-flex items-center gap-1 mx-0.5 px-1.5 py-0.5 rounded text-xs text-purple-700 bg-purple-50 border border-purple-200",
751
758
  "data-node-type": "smartart_atom",
759
+ ...(previewMediaId ? { "data-preview-media-id": previewMediaId } : {}),
752
760
  contenteditable: "false",
753
761
  title: detail,
754
762
  },
@@ -11,6 +11,7 @@ import type {
11
11
  } from "../../api/public-types";
12
12
  import { editorSchema } from "./pm-schema";
13
13
  import { buildPositionMap, type PositionMap } from "./pm-position-map";
14
+ import { chartModelStore } from "../../runtime/chart/chart-model-store.ts";
14
15
 
15
16
  export interface PMStateResult {
16
17
  state: EditorState;
@@ -667,7 +668,6 @@ function buildOpaqueInlineOrComplexAtom(
667
668
  ): PMNode {
668
669
  const label = segment.label;
669
670
  const detail = segment.detail;
670
-
671
671
  if (segment.presentation === "text-box" || segment.presentation === "checkbox") {
672
672
  return editorSchema.nodes.opaque_inline.create({
673
673
  fragmentId: segment.fragmentId,
@@ -691,10 +691,23 @@ function buildOpaqueInlineOrComplexAtom(
691
691
  : null;
692
692
 
693
693
  if (label === "Embedded chart") {
694
+ const parsedChartId = segment.parsedChartId ?? null;
695
+ let widthPx: number | null = null;
696
+ let heightPx: number | null = null;
697
+ if (parsedChartId) {
698
+ const entry = chartModelStore.get(parsedChartId);
699
+ if (entry) {
700
+ widthPx = entry.widthPx;
701
+ heightPx = entry.heightPx;
702
+ }
703
+ }
694
704
  return editorSchema.nodes.chart_atom.create({
695
705
  previewMediaId: segment.previewMediaId ?? null,
696
706
  previewSrc,
697
707
  detail,
708
+ parsedChartId,
709
+ widthPx,
710
+ heightPx,
698
711
  });
699
712
  }
700
713
  if (label === "SmartArt diagram") {
@@ -73,6 +73,7 @@ import {
73
73
  createSurfaceDocumentBuildKey,
74
74
  } from "./surface-build-keys";
75
75
  import { tableNodeViews } from "./tw-table-node-view";
76
+ import { chartNodeViews } from "./chart-node-view.tsx";
76
77
  import type { SelectionToolbarAnchor } from "../../ui/headless/selection-toolbar-model";
77
78
  import type { MediaPreviewDescriptor } from "./pm-state-from-snapshot";
78
79
 
@@ -742,7 +743,7 @@ export const TwProseMirrorSurface = forwardRef<
742
743
  if (!viewRef.current) {
743
744
  const view = new EditorView(mountRef.current, {
744
745
  state,
745
- nodeViews: tableNodeViews,
746
+ nodeViews: { ...tableNodeViews, ...chartNodeViews },
746
747
  editable: () => canEdit,
747
748
  decorations: () => decorations,
748
749
  });