@beyondwork/docx-react-component 1.0.60 → 1.0.62
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/package.json +33 -44
- package/src/api/public-types.ts +41 -0
- package/src/io/docx-session.ts +167 -8
- package/src/io/export/serialize-footnotes.ts +36 -5
- package/src/io/export/serialize-headers-footers.ts +7 -0
- package/src/io/export/serialize-main-document.ts +25 -18
- package/src/io/export/serialize-paragraph-formatting.ts +6 -0
- package/src/io/export/serialize-settings.ts +130 -3
- package/src/io/normalize/normalize-text.ts +8 -4
- package/src/io/ooxml/classify-embedding.ts +193 -0
- package/src/io/ooxml/parse-footnotes.ts +11 -0
- package/src/io/ooxml/parse-headers-footers.ts +117 -42
- package/src/io/ooxml/parse-main-document.ts +20 -8
- package/src/io/ooxml/parse-object.ts +23 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +25 -1
- package/src/io/ooxml/parse-settings.ts +91 -1
- package/src/model/canonical-document.ts +36 -2
- package/src/runtime/document-runtime.ts +424 -0
- package/src/runtime/footnote-resolver.ts +32 -8
- package/src/runtime/layout/layout-engine-version.ts +7 -1
- package/src/runtime/layout/measurement-backend-canvas.ts +1 -1
- package/src/runtime/layout/measurement-backend-empirical.ts +1 -1
- package/src/runtime/layout/paginated-layout-engine.ts +41 -8
- package/src/runtime/layout/resolved-formatting-document.ts +11 -9
- package/src/runtime/layout/resolved-formatting-state.ts +4 -0
- package/src/runtime/numbering-prefix.ts +26 -2
- package/src/runtime/surface-projection.ts +75 -14
- package/src/runtime/table-schema.ts +26 -0
- package/src/ui/WordReviewEditor.tsx +25 -0
- package/src/ui/editor-runtime-boundary.ts +1 -0
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui-tailwind/chrome/tw-runtime-repl-dialog.tsx +514 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +14 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +55 -6
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -1
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +16 -0
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +319 -0
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +248 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +4 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +54 -3
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
EditorStoryTarget,
|
|
5
|
+
RuntimeRenderSnapshot,
|
|
6
|
+
} from "../../api/public-types.ts";
|
|
7
|
+
import type { WordReviewEditorLayoutFacet } from "../../runtime/layout/index.ts";
|
|
8
|
+
import { preserveEditorSelectionMouseDown } from "../../ui/headless/preserve-editor-selection.ts";
|
|
9
|
+
import {
|
|
10
|
+
measureWidgetsViaBoundingRect,
|
|
11
|
+
resolvePageOverlayRects,
|
|
12
|
+
type PageOverlayRect,
|
|
13
|
+
type VisiblePageIndexRange,
|
|
14
|
+
} from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
|
|
15
|
+
import {
|
|
16
|
+
collectFloatingImageOverlayItems,
|
|
17
|
+
type FloatingImagePreviewDescriptor,
|
|
18
|
+
} from "./floating-image-overlay-model.ts";
|
|
19
|
+
|
|
20
|
+
export interface TwFloatingImageLayerProps {
|
|
21
|
+
facet: WordReviewEditorLayoutFacet;
|
|
22
|
+
scrollRoot: HTMLElement | null;
|
|
23
|
+
renderFrameRevision: number;
|
|
24
|
+
visiblePageIndexRange?: VisiblePageIndexRange | null;
|
|
25
|
+
snapshot: RuntimeRenderSnapshot;
|
|
26
|
+
mediaPreviews?: Record<string, FloatingImagePreviewDescriptor>;
|
|
27
|
+
plane: "behind" | "front";
|
|
28
|
+
onActivateFloatingImage?: (payload: {
|
|
29
|
+
mediaId: string;
|
|
30
|
+
from: number;
|
|
31
|
+
to: number;
|
|
32
|
+
storyTarget: EditorStoryTarget;
|
|
33
|
+
}) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const TwFloatingImageLayer: React.FC<TwFloatingImageLayerProps> = ({
|
|
37
|
+
facet,
|
|
38
|
+
scrollRoot,
|
|
39
|
+
renderFrameRevision,
|
|
40
|
+
visiblePageIndexRange,
|
|
41
|
+
snapshot,
|
|
42
|
+
mediaPreviews,
|
|
43
|
+
plane,
|
|
44
|
+
onActivateFloatingImage,
|
|
45
|
+
}) => {
|
|
46
|
+
const overlayRootRef = React.useRef<HTMLDivElement | null>(null);
|
|
47
|
+
const rafHandleRef = React.useRef<number | null>(null);
|
|
48
|
+
const [pageRects, setPageRects] = React.useState<readonly PageOverlayRect[]>([]);
|
|
49
|
+
|
|
50
|
+
const refreshPageRectsNow = React.useCallback(() => {
|
|
51
|
+
if (!scrollRoot) {
|
|
52
|
+
setPageRects([]);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const origin = overlayRootRef.current;
|
|
56
|
+
if (!origin) {
|
|
57
|
+
setPageRects([]);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const pageCount = facet.getPageCount();
|
|
61
|
+
const widgets = measureWidgetsViaBoundingRect(scrollRoot, origin, {
|
|
62
|
+
pageCount,
|
|
63
|
+
visiblePageIndexRange,
|
|
64
|
+
});
|
|
65
|
+
const originRect = origin.getBoundingClientRect();
|
|
66
|
+
setPageRects(
|
|
67
|
+
resolvePageOverlayRects({
|
|
68
|
+
widgets,
|
|
69
|
+
pageCount,
|
|
70
|
+
scrollHeight:
|
|
71
|
+
origin.clientHeight > 0 ? origin.clientHeight : originRect.height,
|
|
72
|
+
visiblePageIndexRange,
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
}, [facet, scrollRoot, visiblePageIndexRange]);
|
|
76
|
+
|
|
77
|
+
const refreshPageRects = React.useCallback(() => {
|
|
78
|
+
if (!scrollRoot) {
|
|
79
|
+
setPageRects([]);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const runtime = scrollRoot.ownerDocument?.defaultView as
|
|
83
|
+
| (Window & {
|
|
84
|
+
requestAnimationFrame?: (cb: () => void) => number;
|
|
85
|
+
cancelAnimationFrame?: (handle: number) => void;
|
|
86
|
+
})
|
|
87
|
+
| null;
|
|
88
|
+
const raf = runtime?.requestAnimationFrame;
|
|
89
|
+
if (!raf) {
|
|
90
|
+
refreshPageRectsNow();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (rafHandleRef.current !== null) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
rafHandleRef.current = raf(() => {
|
|
97
|
+
rafHandleRef.current = null;
|
|
98
|
+
refreshPageRectsNow();
|
|
99
|
+
});
|
|
100
|
+
}, [refreshPageRectsNow, scrollRoot]);
|
|
101
|
+
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
refreshPageRects();
|
|
104
|
+
return () => {
|
|
105
|
+
const runtime = scrollRoot?.ownerDocument?.defaultView as
|
|
106
|
+
| (Window & { cancelAnimationFrame?: (handle: number) => void })
|
|
107
|
+
| null;
|
|
108
|
+
if (rafHandleRef.current !== null && runtime?.cancelAnimationFrame) {
|
|
109
|
+
runtime.cancelAnimationFrame(rafHandleRef.current);
|
|
110
|
+
rafHandleRef.current = null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}, [refreshPageRects, renderFrameRevision, scrollRoot]);
|
|
114
|
+
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
if (!scrollRoot) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const runtime = scrollRoot.ownerDocument?.defaultView as
|
|
120
|
+
| (Window & { ResizeObserver?: typeof ResizeObserver })
|
|
121
|
+
| null;
|
|
122
|
+
if (!runtime?.ResizeObserver) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const observer = new runtime.ResizeObserver(() => refreshPageRects());
|
|
126
|
+
observer.observe(scrollRoot);
|
|
127
|
+
return () => observer.disconnect();
|
|
128
|
+
}, [refreshPageRects, scrollRoot]);
|
|
129
|
+
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
if (!scrollRoot) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const runtime = scrollRoot.ownerDocument?.defaultView as
|
|
135
|
+
| (Window & { MutationObserver?: typeof MutationObserver })
|
|
136
|
+
| null;
|
|
137
|
+
if (!runtime?.MutationObserver) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const observer = new runtime.MutationObserver((records) => {
|
|
141
|
+
const overlay = overlayRootRef.current;
|
|
142
|
+
if (overlay) {
|
|
143
|
+
const allSelf = records.every(
|
|
144
|
+
(record) => record.target instanceof Node && overlay.contains(record.target),
|
|
145
|
+
);
|
|
146
|
+
if (allSelf) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
refreshPageRects();
|
|
151
|
+
});
|
|
152
|
+
observer.observe(scrollRoot, { childList: true, subtree: false });
|
|
153
|
+
return () => observer.disconnect();
|
|
154
|
+
}, [refreshPageRects, scrollRoot]);
|
|
155
|
+
|
|
156
|
+
const items = React.useMemo(() => {
|
|
157
|
+
const allItems = collectFloatingImageOverlayItems({
|
|
158
|
+
surface: snapshot.surface,
|
|
159
|
+
activeStory: snapshot.activeStory,
|
|
160
|
+
facet,
|
|
161
|
+
pageRects,
|
|
162
|
+
mediaPreviews,
|
|
163
|
+
});
|
|
164
|
+
return allItems.filter((item) =>
|
|
165
|
+
plane === "behind" ? item.behindDoc : !item.behindDoc,
|
|
166
|
+
);
|
|
167
|
+
}, [facet, mediaPreviews, pageRects, plane, snapshot.activeStory, snapshot.surface]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
ref={overlayRootRef}
|
|
172
|
+
className={
|
|
173
|
+
plane === "behind"
|
|
174
|
+
? "pointer-events-none absolute inset-0 z-[5]"
|
|
175
|
+
: "pointer-events-none absolute inset-0 z-20"
|
|
176
|
+
}
|
|
177
|
+
aria-hidden={plane === "behind" ? "true" : undefined}
|
|
178
|
+
data-testid={plane === "behind" ? "floating-image-layer-behind" : "floating-image-layer-front"}
|
|
179
|
+
data-floating-image-count={items.length}
|
|
180
|
+
>
|
|
181
|
+
{items.map((item) => {
|
|
182
|
+
const interactive = plane === "front" && typeof onActivateFloatingImage === "function";
|
|
183
|
+
const commonProps = {
|
|
184
|
+
key: item.key,
|
|
185
|
+
className: interactive
|
|
186
|
+
? "pointer-events-auto absolute m-0 border-0 bg-transparent p-0"
|
|
187
|
+
: "pointer-events-none absolute m-0 border-0 bg-transparent p-0",
|
|
188
|
+
"data-floating-image-overlay": "",
|
|
189
|
+
"data-floating-image-id": item.mediaId,
|
|
190
|
+
style: {
|
|
191
|
+
top: `${item.topPx}px`,
|
|
192
|
+
left: `${item.leftPx}px`,
|
|
193
|
+
width: `${item.widthPx}px`,
|
|
194
|
+
height: `${item.heightPx}px`,
|
|
195
|
+
},
|
|
196
|
+
} as const;
|
|
197
|
+
const content = item.src ? (
|
|
198
|
+
<img
|
|
199
|
+
src={item.src}
|
|
200
|
+
alt={interactive ? item.altText ?? "" : ""}
|
|
201
|
+
title={item.detail ?? item.altText ?? "Floating image"}
|
|
202
|
+
draggable={false}
|
|
203
|
+
style={{
|
|
204
|
+
display: "block",
|
|
205
|
+
width: "100%",
|
|
206
|
+
height: "100%",
|
|
207
|
+
objectFit: "fill",
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
) : (
|
|
211
|
+
<span
|
|
212
|
+
className="flex h-full w-full items-center justify-center rounded border border-border/70 bg-surface/90 px-2 text-[10px] font-medium uppercase tracking-[0.12em] text-secondary"
|
|
213
|
+
title={item.detail ?? item.altText ?? "Floating image"}
|
|
214
|
+
>
|
|
215
|
+
Image
|
|
216
|
+
</span>
|
|
217
|
+
);
|
|
218
|
+
if (!interactive) {
|
|
219
|
+
return (
|
|
220
|
+
<div {...commonProps} aria-hidden="true">
|
|
221
|
+
{content}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return (
|
|
226
|
+
<button
|
|
227
|
+
{...commonProps}
|
|
228
|
+
type="button"
|
|
229
|
+
tabIndex={-1}
|
|
230
|
+
aria-label={item.altText ?? "Select floating image"}
|
|
231
|
+
onMouseDown={preserveEditorSelectionMouseDown}
|
|
232
|
+
onClick={() =>
|
|
233
|
+
onActivateFloatingImage?.({
|
|
234
|
+
mediaId: item.mediaId,
|
|
235
|
+
from: item.from,
|
|
236
|
+
to: item.to,
|
|
237
|
+
storyTarget: snapshot.activeStory,
|
|
238
|
+
})}
|
|
239
|
+
>
|
|
240
|
+
{content}
|
|
241
|
+
</button>
|
|
242
|
+
);
|
|
243
|
+
})}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export default TwFloatingImageLayer;
|
|
@@ -260,6 +260,10 @@ function RegionTable({
|
|
|
260
260
|
if (cell.borderRight) cellStyle.borderRight = cell.borderRight;
|
|
261
261
|
if (cell.borderBottom) cellStyle.borderBottom = cell.borderBottom;
|
|
262
262
|
if (cell.borderLeft) cellStyle.borderLeft = cell.borderLeft;
|
|
263
|
+
if (typeof cell.paddingTop === "number") cellStyle.paddingTop = `${cell.paddingTop / 20}pt`;
|
|
264
|
+
if (typeof cell.paddingRight === "number") cellStyle.paddingRight = `${cell.paddingRight / 20}pt`;
|
|
265
|
+
if (typeof cell.paddingBottom === "number") cellStyle.paddingBottom = `${cell.paddingBottom / 20}pt`;
|
|
266
|
+
if (typeof cell.paddingLeft === "number") cellStyle.paddingLeft = `${cell.paddingLeft / 20}pt`;
|
|
263
267
|
|
|
264
268
|
return (
|
|
265
269
|
<td
|
|
@@ -91,6 +91,8 @@ import { TwReviewRail, type ReviewRailTab } from "./review/tw-review-rail";
|
|
|
91
91
|
import { TwStatusBar } from "./status/tw-status-bar";
|
|
92
92
|
import { type ToolbarInteractionPolicy } from "./toolbar/tw-toolbar";
|
|
93
93
|
import { TwChromeOverlay, TwPageStackOverlayLayer } from "./chrome-overlay";
|
|
94
|
+
import { TwFloatingImageLayer } from "./page-stack/tw-floating-image-layer.tsx";
|
|
95
|
+
import type { MediaPreviewDescriptor } from "./editor-surface/pm-state-from-snapshot.ts";
|
|
94
96
|
import {
|
|
95
97
|
cycleScopeIndex,
|
|
96
98
|
shouldHandleScopeNavKey,
|
|
@@ -104,6 +106,7 @@ export interface TwReviewWorkspaceProps {
|
|
|
104
106
|
markupDisplay: MarkupDisplay;
|
|
105
107
|
currentUserId?: string;
|
|
106
108
|
capabilities?: SessionCapabilities;
|
|
109
|
+
mediaPreviews?: Record<string, MediaPreviewDescriptor>;
|
|
107
110
|
reviewMode?: "editing" | "review";
|
|
108
111
|
/**
|
|
109
112
|
* Runtime-owned layout facet. Optional so existing tests + host apps
|
|
@@ -192,6 +195,12 @@ export interface TwReviewWorkspaceProps {
|
|
|
192
195
|
selectionContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
193
196
|
currentScopeContextAnalytics?: RuntimeContextAnalyticsSnapshot | null;
|
|
194
197
|
commands: EditorCommandBag;
|
|
198
|
+
onActivateFloatingImage?: (payload: {
|
|
199
|
+
mediaId: string;
|
|
200
|
+
from: number;
|
|
201
|
+
to: number;
|
|
202
|
+
storyTarget: EditorStoryTarget;
|
|
203
|
+
}) => void;
|
|
195
204
|
/** N6 — release the grabbed image/shape. Wired to `runtime.deselectObject()` by the host. */
|
|
196
205
|
onDeselectObject?: () => void;
|
|
197
206
|
activeSelectionTool?: ActiveSelectionToolModel | null;
|
|
@@ -903,6 +912,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
903
912
|
// of N times.
|
|
904
913
|
useEffect(() => {
|
|
905
914
|
if (!props.layoutFacet) return;
|
|
915
|
+
let pendingBump = false;
|
|
916
|
+
let cancelled = false;
|
|
917
|
+
const scheduleBump = () => {
|
|
918
|
+
if (pendingBump || cancelled) return;
|
|
919
|
+
pendingBump = true;
|
|
920
|
+
queueMicrotask(() => {
|
|
921
|
+
pendingBump = false;
|
|
922
|
+
if (cancelled) return;
|
|
923
|
+
setRenderFrameRevision((n) => n + 1);
|
|
924
|
+
});
|
|
925
|
+
};
|
|
906
926
|
const unsub = props.layoutFacet.subscribe((event) => {
|
|
907
927
|
switch (event.kind) {
|
|
908
928
|
case "zoom_changed":
|
|
@@ -913,13 +933,16 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
913
933
|
case "page_field_dirtied":
|
|
914
934
|
case "measurement_backend_ready":
|
|
915
935
|
case "layout_committed":
|
|
916
|
-
|
|
936
|
+
scheduleBump();
|
|
917
937
|
break;
|
|
918
938
|
default:
|
|
919
939
|
break;
|
|
920
940
|
}
|
|
921
941
|
});
|
|
922
|
-
return
|
|
942
|
+
return () => {
|
|
943
|
+
cancelled = true;
|
|
944
|
+
unsub();
|
|
945
|
+
};
|
|
923
946
|
}, [props.layoutFacet]);
|
|
924
947
|
|
|
925
948
|
useEffect(() => {
|
|
@@ -1077,7 +1100,12 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1077
1100
|
useEffect(() => {
|
|
1078
1101
|
if (!props.layoutFacet) return;
|
|
1079
1102
|
const facet = props.layoutFacet;
|
|
1080
|
-
|
|
1103
|
+
const fontSet = document.fonts;
|
|
1104
|
+
if (!fontSet?.ready) {
|
|
1105
|
+
facet.swapMeasurementProvider(createCanvasBackend());
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
void fontSet.ready.then(() => {
|
|
1081
1109
|
facet.swapMeasurementProvider(createCanvasBackend());
|
|
1082
1110
|
});
|
|
1083
1111
|
}, [props.layoutFacet]);
|
|
@@ -1634,6 +1662,17 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1634
1662
|
data-layer="page-card-backgrounds"
|
|
1635
1663
|
/>
|
|
1636
1664
|
) : null}
|
|
1665
|
+
{isPageWorkspace && chromeVisibility.pageChrome && props.layoutFacet ? (
|
|
1666
|
+
<TwFloatingImageLayer
|
|
1667
|
+
facet={props.layoutFacet}
|
|
1668
|
+
scrollRoot={pageStackScrollRoot}
|
|
1669
|
+
renderFrameRevision={renderFrameRevision}
|
|
1670
|
+
visiblePageIndexRange={visiblePageIndexRange}
|
|
1671
|
+
snapshot={snapshot}
|
|
1672
|
+
mediaPreviews={props.mediaPreviews}
|
|
1673
|
+
plane="behind"
|
|
1674
|
+
/>
|
|
1675
|
+
) : null}
|
|
1637
1676
|
{isPageWorkspace && chromeVisibility.pageChrome && pageChromeModel.lineNumberingEnabled ? (
|
|
1638
1677
|
<div
|
|
1639
1678
|
aria-hidden="true"
|
|
@@ -1688,6 +1727,18 @@ export function TwReviewWorkspace(inputProps: TwReviewWorkspaceProps) {
|
|
|
1688
1727
|
>
|
|
1689
1728
|
{props.document}
|
|
1690
1729
|
</div>
|
|
1730
|
+
{isPageWorkspace && chromeVisibility.pageChrome && props.layoutFacet ? (
|
|
1731
|
+
<TwFloatingImageLayer
|
|
1732
|
+
facet={props.layoutFacet}
|
|
1733
|
+
scrollRoot={pageStackScrollRoot}
|
|
1734
|
+
renderFrameRevision={renderFrameRevision}
|
|
1735
|
+
visiblePageIndexRange={visiblePageIndexRange}
|
|
1736
|
+
snapshot={snapshot}
|
|
1737
|
+
mediaPreviews={props.mediaPreviews}
|
|
1738
|
+
plane="front"
|
|
1739
|
+
onActivateFloatingImage={props.onActivateFloatingImage}
|
|
1740
|
+
/>
|
|
1741
|
+
) : null}
|
|
1691
1742
|
{props.layoutFacet ? (
|
|
1692
1743
|
<TwChromeOverlay
|
|
1693
1744
|
facet={props.layoutFacet}
|