@extend-ai/react-docx 0.7.0-alpha.6 → 0.7.0-alpha.7

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/README.md CHANGED
@@ -158,7 +158,7 @@ export function EditorExample() {
158
158
 
159
159
  ## Thumbnail Hook
160
160
 
161
- The library can expose page thumbnails from mounted viewer surfaces so you can build your own page strip, mini-map, or navigation UI.
161
+ The library can expose page thumbnails so you can build your own page strip, mini-map, or navigation UI. Thumbnail painting can render from the live page surface when it is mounted, or from an offscreen one-page surface when viewer virtualization has unmounted that page.
162
162
 
163
163
  ```tsx
164
164
  import * as React from "react";
@@ -194,10 +194,7 @@ export function ThumbnailExample() {
194
194
  ))}
195
195
  </div>
196
196
 
197
- <DocxEditorViewer
198
- editor={editor}
199
- pageVirtualization={{ enabled: false }}
200
- />
197
+ <DocxEditorViewer editor={editor} />
201
198
  </div>
202
199
  );
203
200
  }
@@ -205,10 +202,9 @@ export function ThumbnailExample() {
205
202
 
206
203
  Notes:
207
204
 
208
- - Thumbnails are produced from mounted page DOM.
205
+ - Thumbnail canvases can stay attached in a virtualized sidebar; only canvases you mount request paint work.
209
206
  - Thumbnail sizing is bounded by `maxWidthPx` and `maxHeightPx`, so downstream UIs can bias toward portrait thumbnail rails.
210
- - If page virtualization is enabled, offscreen pages can report `status: "unavailable"`.
211
- - For a full thumbnail rail, disable virtualization or manage the visible page range yourself.
207
+ - If the main viewer has virtualized a page away, thumbnail rendering mounts an isolated offscreen page surface long enough to rasterize that page.
212
208
 
213
209
  ## Useful Hooks
214
210
 
package/dist/index.cjs CHANGED
@@ -23682,13 +23682,14 @@ function ensureDocxViewerPageSurfaceRegistry(editor) {
23682
23682
  registry = {
23683
23683
  pageElements: /* @__PURE__ */ new Map(),
23684
23684
  pageContentKeys: /* @__PURE__ */ new Map(),
23685
+ pageSizes: /* @__PURE__ */ new Map(),
23685
23686
  listeners: /* @__PURE__ */ new Set()
23686
23687
  };
23687
23688
  docxViewerPageSurfaceRegistryByEditor.set(owner, registry);
23688
23689
  }
23689
23690
  return registry;
23690
23691
  }
23691
- function syncDocxViewerPageSurfaceContentKeys(editor, contentKeysByPage) {
23692
+ function syncDocxViewerPageSurfaceContentKeys(editor, contentKeysByPage, pageSizesByPage = []) {
23692
23693
  const registry = ensureDocxViewerPageSurfaceRegistry(editor);
23693
23694
  let changed = false;
23694
23695
  contentKeysByPage.forEach((contentKey, pageIndex) => {
@@ -23697,12 +23698,27 @@ function syncDocxViewerPageSurfaceContentKeys(editor, contentKeysByPage) {
23697
23698
  changed = true;
23698
23699
  }
23699
23700
  });
23701
+ pageSizesByPage.forEach((pageSize, pageIndex) => {
23702
+ const widthPx = Math.max(1, Math.round(pageSize.widthPx));
23703
+ const heightPx = Math.max(1, Math.round(pageSize.heightPx));
23704
+ const previous = registry.pageSizes.get(pageIndex);
23705
+ if (previous?.widthPx !== widthPx || previous?.heightPx !== heightPx) {
23706
+ registry.pageSizes.set(pageIndex, { widthPx, heightPx });
23707
+ changed = true;
23708
+ }
23709
+ });
23700
23710
  registry.pageContentKeys.forEach((_, pageIndex) => {
23701
23711
  if (pageIndex >= contentKeysByPage.length) {
23702
23712
  registry.pageContentKeys.delete(pageIndex);
23703
23713
  changed = true;
23704
23714
  }
23705
23715
  });
23716
+ registry.pageSizes.forEach((_, pageIndex) => {
23717
+ if (pageIndex >= pageSizesByPage.length) {
23718
+ registry.pageSizes.delete(pageIndex);
23719
+ changed = true;
23720
+ }
23721
+ });
23706
23722
  if (changed) {
23707
23723
  notifyDocxViewerPageSurfaceSubscribers(registry);
23708
23724
  }
@@ -23719,7 +23735,7 @@ function notifyDocxViewerPageSurfaceSubscribers(registry) {
23719
23735
  listener();
23720
23736
  });
23721
23737
  }
23722
- function registerDocxViewerPageSurface(editor, pageIndex, element) {
23738
+ function registerDocxViewerPageSurface(editor, pageIndex, element, previousElement) {
23723
23739
  const registry = ensureDocxViewerPageSurfaceRegistry(editor);
23724
23740
  const normalizedPageIndex = Math.max(0, Math.round(pageIndex));
23725
23741
  const currentElement = registry.pageElements.get(normalizedPageIndex);
@@ -23734,9 +23750,22 @@ function registerDocxViewerPageSurface(editor, pageIndex, element) {
23734
23750
  if (!currentElement) {
23735
23751
  return;
23736
23752
  }
23753
+ if (previousElement && currentElement !== previousElement) {
23754
+ return;
23755
+ }
23737
23756
  registry.pageElements.delete(normalizedPageIndex);
23738
23757
  notifyDocxViewerPageSurfaceSubscribers(registry);
23739
23758
  }
23759
+ function resolveDocxViewerRegisteredPageSurfaceSize(registry, pageIndex, fallbackWidthPx, fallbackHeightPx) {
23760
+ const registeredSize = registry.pageSizes.get(pageIndex);
23761
+ if (registeredSize) {
23762
+ return registeredSize;
23763
+ }
23764
+ return {
23765
+ widthPx: Math.max(1, Math.round(fallbackWidthPx)),
23766
+ heightPx: Math.max(1, Math.round(fallbackHeightPx))
23767
+ };
23768
+ }
23740
23769
  function resolveDocxViewerPageSurfaceSize(element, fallbackWidthPx, fallbackHeightPx) {
23741
23770
  if (element) {
23742
23771
  const rect = element.getBoundingClientRect();
@@ -23770,6 +23799,104 @@ function resolveDocxViewerPageSurfaceSize(element, fallbackWidthPx, fallbackHeig
23770
23799
  heightPx: Math.max(1, Math.round(fallbackHeightPx))
23771
23800
  };
23772
23801
  }
23802
+ function DocxDetachedThumbnailPageSurface({
23803
+ editor,
23804
+ pageIndex
23805
+ }) {
23806
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
23807
+ DocxEditorViewer,
23808
+ {
23809
+ editor,
23810
+ mode: "read-only",
23811
+ visiblePageRange: { startPageIndex: pageIndex, endPageIndex: pageIndex },
23812
+ pageVirtualization: { enabled: false },
23813
+ showTrackedChanges: editor.showTrackedChanges,
23814
+ showComments: editor.showComments,
23815
+ style: {
23816
+ background: "transparent",
23817
+ padding: 0,
23818
+ margin: 0
23819
+ }
23820
+ }
23821
+ );
23822
+ }
23823
+ var DocxDetachedThumbnailSurfaceRenderer = class {
23824
+ host;
23825
+ root;
23826
+ activePageIndex;
23827
+ async renderPageSurface(params) {
23828
+ if (typeof document === "undefined" || typeof window === "undefined") {
23829
+ return void 0;
23830
+ }
23831
+ const { editor, registry, pageIndex } = params;
23832
+ await this.ensureRoot();
23833
+ if (!this.root) {
23834
+ return void 0;
23835
+ }
23836
+ if (this.activePageIndex !== pageIndex) {
23837
+ this.activePageIndex = pageIndex;
23838
+ this.root.render(
23839
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
23840
+ DocxDetachedThumbnailPageSurface,
23841
+ {
23842
+ editor,
23843
+ pageIndex
23844
+ }
23845
+ )
23846
+ );
23847
+ }
23848
+ return this.waitForPageSurface(registry, pageIndex);
23849
+ }
23850
+ clear() {
23851
+ if (this.root) {
23852
+ this.root.unmount();
23853
+ this.root = void 0;
23854
+ }
23855
+ if (this.host?.parentNode) {
23856
+ this.host.parentNode.removeChild(this.host);
23857
+ }
23858
+ this.host = void 0;
23859
+ this.activePageIndex = void 0;
23860
+ }
23861
+ async ensureRoot() {
23862
+ if (this.root) {
23863
+ return;
23864
+ }
23865
+ if (typeof document === "undefined") {
23866
+ return;
23867
+ }
23868
+ const host = document.createElement("div");
23869
+ host.setAttribute("data-docx-thumbnail-detached-renderer", "true");
23870
+ Object.assign(host.style, {
23871
+ position: "fixed",
23872
+ left: "-100000px",
23873
+ top: "0",
23874
+ width: "1px",
23875
+ height: "1px",
23876
+ overflow: "visible",
23877
+ opacity: "0",
23878
+ pointerEvents: "none",
23879
+ zIndex: "-1"
23880
+ });
23881
+ document.body.appendChild(host);
23882
+ this.host = host;
23883
+ const { createRoot } = await import("react-dom/client");
23884
+ this.root = createRoot(host);
23885
+ }
23886
+ async waitForPageSurface(registry, pageIndex) {
23887
+ for (let attempt = 0; attempt < 8; attempt += 1) {
23888
+ const pageElement2 = registry.pageElements.get(pageIndex);
23889
+ if (pageElement2?.isConnected) {
23890
+ return pageElement2;
23891
+ }
23892
+ await new Promise((resolve) => {
23893
+ window.requestAnimationFrame(() => resolve());
23894
+ });
23895
+ }
23896
+ const pageElement = registry.pageElements.get(pageIndex);
23897
+ return pageElement?.isConnected ? pageElement : void 0;
23898
+ }
23899
+ };
23773
23900
  var DOCX_THUMBNAIL_SURFACE_CACHE_MAX_ENTRIES = 32;
23774
23901
  var DOCX_THUMBNAIL_MIN_RASTER_INTERVAL_MS = 200;
23775
23902
  function resolveDocxPageThumbnailResolution(options) {
@@ -23847,6 +23974,7 @@ function useDocxPageThumbnails(editor, options = {}) {
23847
23974
  );
23848
23975
  const thumbnailSurfaceCacheRef = React.useRef(void 0);
23849
23976
  const thumbnailRasterQueueRef = React.useRef(void 0);
23977
+ const detachedThumbnailSurfaceRendererRef = React.useRef(void 0);
23850
23978
  const lastPaintedThumbnailKeyByCanvasRef = React.useRef(
23851
23979
  /* @__PURE__ */ new WeakMap()
23852
23980
  );
@@ -23869,8 +23997,17 @@ function useDocxPageThumbnails(editor, options = {}) {
23869
23997
  React.useEffect(() => {
23870
23998
  thumbnailSurfaceCacheRef.current?.clear();
23871
23999
  thumbnailRasterQueueRef.current?.clear();
24000
+ detachedThumbnailSurfaceRendererRef.current?.clear();
24001
+ detachedThumbnailSurfaceRendererRef.current = void 0;
23872
24002
  lastPaintedThumbnailKeyByCanvasRef.current = /* @__PURE__ */ new WeakMap();
23873
24003
  }, [editor.documentLoadNonce, pageSurfaceRegistryOwner]);
24004
+ React.useEffect(
24005
+ () => () => {
24006
+ detachedThumbnailSurfaceRendererRef.current?.clear();
24007
+ detachedThumbnailSurfaceRendererRef.current = void 0;
24008
+ },
24009
+ []
24010
+ );
23874
24011
  const thumbnailResolutionOptionsKey = React.useMemo(() => {
23875
24012
  const bounds = options.resolution;
23876
24013
  const boundsKey = typeof bounds === "number" ? `n${bounds}` : bounds ? `b${bounds.maxWidth ?? ""}x${bounds.maxHeight ?? ""}` : "";
@@ -23898,11 +24035,6 @@ function useDocxPageThumbnails(editor, options = {}) {
23898
24035
  return;
23899
24036
  }
23900
24037
  const requiresAttachedTarget = canvas === void 0;
23901
- const pageElement = pageSurfaceRegistry.pageElements.get(pageIndex);
23902
- if (!pageElement || !pageElement.isConnected) {
23903
- updatePageThumbnailState(pageIndex, "unavailable");
23904
- return;
23905
- }
23906
24038
  const force = renderOptions?.force === true;
23907
24039
  const lastPaintedKey = lastPaintedThumbnailKeyByCanvasRef.current.get(targetCanvas);
23908
24040
  if (!force && lastPaintedKey !== void 0 && lastPaintedKey === thumbnailSkipKeyForPage(pageIndex)) {
@@ -23914,41 +24046,67 @@ function useDocxPageThumbnails(editor, options = {}) {
23914
24046
  if (requiresAttachedTarget && attachedCanvasByPageRef.current.get(pageIndex) !== targetCanvas) {
23915
24047
  return;
23916
24048
  }
23917
- const livePageElement = pageSurfaceRegistry.pageElements.get(pageIndex);
23918
- if (!livePageElement || !livePageElement.isConnected) {
23919
- updatePageThumbnailState(pageIndex, "unavailable");
23920
- return;
23921
- }
23922
24049
  const runSkipKey = thumbnailSkipKeyForPage(pageIndex);
23923
- const sourceSize = resolveDocxViewerPageSurfaceSize(
23924
- livePageElement,
24050
+ const fallbackSourceSize = resolveDocxViewerRegisteredPageSurfaceSize(
24051
+ pageSurfaceRegistry,
24052
+ pageIndex,
23925
24053
  fallbackLayout.pageWidthPx,
23926
24054
  fallbackLayout.pageHeightPx
23927
24055
  );
23928
24056
  const resolution = resolveDocxPageThumbnailResolution({
23929
- sourceWidthPx: sourceSize.widthPx,
23930
- sourceHeightPx: sourceSize.heightPx,
24057
+ sourceWidthPx: fallbackSourceSize.widthPx,
24058
+ sourceHeightPx: fallbackSourceSize.heightPx,
23931
24059
  resolution: options.resolution,
23932
24060
  maxWidthPx: options.maxWidthPx,
23933
24061
  maxHeightPx: options.maxHeightPx,
23934
24062
  pixelRatio: options.pixelRatio
23935
24063
  });
23936
- const surfaceKey = runSkipKey === void 0 ? void 0 : `${runSkipKey}|${sourceSize.widthPx}x${sourceSize.heightPx}|${resolution.pixelWidthPx}x${resolution.pixelHeightPx}`;
24064
+ const surfaceKey = runSkipKey === void 0 ? void 0 : `${runSkipKey}|${fallbackSourceSize.widthPx}x${fallbackSourceSize.heightPx}|${resolution.pixelWidthPx}x${resolution.pixelHeightPx}`;
23937
24065
  const surfaceCache = ensureThumbnailSurfaceCache();
23938
24066
  try {
23939
24067
  let surface = !force && surfaceKey !== void 0 ? surfaceCache.get(surfaceKey) : void 0;
23940
24068
  if (!surface) {
23941
- surface = await rasterizeDocxThumbnailSurface({
23942
- pageElement: livePageElement,
23943
- sourceWidthPx: sourceSize.widthPx,
23944
- sourceHeightPx: sourceSize.heightPx,
23945
- widthPx: resolution.widthPx,
23946
- heightPx: resolution.heightPx,
23947
- pixelWidthPx: resolution.pixelWidthPx,
23948
- pixelHeightPx: resolution.pixelHeightPx
23949
- });
23950
- if (surfaceKey !== void 0) {
23951
- surfaceCache.set(surfaceKey, surface);
24069
+ let livePageElement = pageSurfaceRegistry.pageElements.get(pageIndex);
24070
+ let renderedDetachedSurface = false;
24071
+ try {
24072
+ if (!livePageElement || !livePageElement.isConnected) {
24073
+ if (!detachedThumbnailSurfaceRendererRef.current) {
24074
+ detachedThumbnailSurfaceRendererRef.current = new DocxDetachedThumbnailSurfaceRenderer();
24075
+ }
24076
+ livePageElement = await detachedThumbnailSurfaceRendererRef.current.renderPageSurface(
24077
+ {
24078
+ editor,
24079
+ registry: pageSurfaceRegistry,
24080
+ pageIndex
24081
+ }
24082
+ );
24083
+ renderedDetachedSurface = true;
24084
+ }
24085
+ if (!livePageElement || !livePageElement.isConnected) {
24086
+ updatePageThumbnailState(pageIndex, "unavailable");
24087
+ return;
24088
+ }
24089
+ const sourceSize = resolveDocxViewerPageSurfaceSize(
24090
+ livePageElement,
24091
+ fallbackSourceSize.widthPx,
24092
+ fallbackSourceSize.heightPx
24093
+ );
24094
+ surface = await rasterizeDocxThumbnailSurface({
24095
+ pageElement: livePageElement,
24096
+ sourceWidthPx: sourceSize.widthPx,
24097
+ sourceHeightPx: sourceSize.heightPx,
24098
+ widthPx: resolution.widthPx,
24099
+ heightPx: resolution.heightPx,
24100
+ pixelWidthPx: resolution.pixelWidthPx,
24101
+ pixelHeightPx: resolution.pixelHeightPx
24102
+ });
24103
+ if (surfaceKey !== void 0) {
24104
+ surfaceCache.set(surfaceKey, surface);
24105
+ }
24106
+ } finally {
24107
+ if (renderedDetachedSurface) {
24108
+ detachedThumbnailSurfaceRendererRef.current?.clear();
24109
+ }
23952
24110
  }
23953
24111
  }
23954
24112
  blitDocxThumbnailSurface(surface, targetCanvas, resolution);
@@ -23980,7 +24138,8 @@ function useDocxPageThumbnails(editor, options = {}) {
23980
24138
  options.pixelRatio,
23981
24139
  pageSurfaceRegistry,
23982
24140
  thumbnailSkipKeyForPage,
23983
- updatePageThumbnailState
24141
+ updatePageThumbnailState,
24142
+ editor
23984
24143
  ]
23985
24144
  );
23986
24145
  const requestAttachedThumbnailRenders = React.useCallback(
@@ -24024,10 +24183,15 @@ function useDocxPageThumbnails(editor, options = {}) {
24024
24183
  const totalPages = Math.max(1, editor.totalPages);
24025
24184
  return Array.from({ length: totalPages }, (_, pageIndex) => {
24026
24185
  const pageElement = mountedPageElements.get(pageIndex);
24027
- const sourceSize = resolveDocxViewerPageSurfaceSize(
24186
+ const sourceSize = pageElement && pageElement.isConnected ? resolveDocxViewerPageSurfaceSize(
24028
24187
  pageElement,
24029
24188
  fallbackLayout.pageWidthPx,
24030
24189
  fallbackLayout.pageHeightPx
24190
+ ) : resolveDocxViewerRegisteredPageSurfaceSize(
24191
+ pageSurfaceRegistry,
24192
+ pageIndex,
24193
+ fallbackLayout.pageWidthPx,
24194
+ fallbackLayout.pageHeightPx
24031
24195
  );
24032
24196
  const resolution = resolveDocxPageThumbnailResolution({
24033
24197
  sourceWidthPx: sourceSize.widthPx,
@@ -24097,6 +24261,7 @@ function useDocxPageThumbnails(editor, options = {}) {
24097
24261
  options.maxHeightPx,
24098
24262
  options.maxWidthPx,
24099
24263
  options.pixelRatio,
24264
+ pageSurfaceRegistry,
24100
24265
  pageThumbnailStates
24101
24266
  ]);
24102
24267
  const paintThumbnail = React.useCallback(
@@ -25393,13 +25558,23 @@ function DocxEditorViewer({
25393
25558
  if (cached) {
25394
25559
  return cached;
25395
25560
  }
25561
+ let registeredElement = null;
25396
25562
  const nextRef = (element) => {
25397
25563
  if (element) {
25564
+ registeredElement = element;
25398
25565
  pageElementsRef.current.set(normalizedPageIndex, element);
25399
25566
  } else {
25400
25567
  pageElementsRef.current.delete(normalizedPageIndex);
25401
25568
  }
25402
- registerDocxViewerPageSurface(editor, normalizedPageIndex, element);
25569
+ registerDocxViewerPageSurface(
25570
+ editor,
25571
+ normalizedPageIndex,
25572
+ element,
25573
+ registeredElement
25574
+ );
25575
+ if (!element) {
25576
+ registeredElement = null;
25577
+ }
25403
25578
  };
25404
25579
  pageSurfaceRefCallbacksRef.current.set(normalizedPageIndex, nextRef);
25405
25580
  return nextRef;
@@ -26462,12 +26637,27 @@ function DocxEditorViewer({
26462
26637
  pageSectionInfoByIndex,
26463
26638
  trackedChangesEnabled
26464
26639
  ]);
26640
+ const pageThumbnailSurfaceSizesByPage = React.useMemo(
26641
+ () => pageNodeSegmentsByPage.map((_, pageIndex) => {
26642
+ const pageLayout = pageSectionInfoByIndex[pageIndex]?.layout ?? documentLayout;
26643
+ return {
26644
+ widthPx: pageLayout.pageWidthPx,
26645
+ heightPx: pageLayout.pageHeightPx
26646
+ };
26647
+ }),
26648
+ [documentLayout, pageNodeSegmentsByPage, pageSectionInfoByIndex]
26649
+ );
26465
26650
  React.useEffect(() => {
26466
26651
  syncDocxViewerPageSurfaceContentKeys(
26467
26652
  editor,
26468
- pageThumbnailContentKeysByPage
26653
+ pageThumbnailContentKeysByPage,
26654
+ pageThumbnailSurfaceSizesByPage
26469
26655
  );
26470
- }, [pageSurfaceRegistryOwner, pageThumbnailContentKeysByPage]);
26656
+ }, [
26657
+ pageSurfaceRegistryOwner,
26658
+ pageThumbnailContentKeysByPage,
26659
+ pageThumbnailSurfaceSizesByPage
26660
+ ]);
26471
26661
  const resolveStyleRefFieldValueForPage = React.useMemo(() => {
26472
26662
  const valueCache = /* @__PURE__ */ new Map();
26473
26663
  const nodes = editor.model.nodes;