@beyondwork/docx-react-component 1.0.109 → 1.0.110
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 +1 -1
- package/src/model/layout/runtime-page-graph-types.ts +25 -0
- package/src/runtime/document-runtime.ts +46 -0
- package/src/runtime/geometry/adjacent-geometry-intake.ts +450 -13
- package/src/runtime/geometry/geometry-index.ts +17 -2
- package/src/runtime/layout/layout-engine-instance.ts +231 -4
- package/src/runtime/layout/layout-engine-version.ts +16 -1
- package/src/runtime/layout/page-graph.ts +7 -0
- package/src/runtime/layout/paginated-layout-engine.ts +34 -0
- package/src/runtime/layout/public-facet.ts +68 -9
- package/src/runtime/layout/resolve-page-previews.ts +46 -8
- package/src/ui-tailwind/chrome-overlay/tw-table-split-row-carry-overlay.tsx +62 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +1 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +62 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +1 -0
|
@@ -31,6 +31,16 @@ export interface PagePreviewMaps {
|
|
|
31
31
|
footerPreviewByPageId: Map<string, string>;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
type PageLocalFieldLedger = readonly {
|
|
35
|
+
readonly family: string;
|
|
36
|
+
readonly displayText: string;
|
|
37
|
+
}[];
|
|
38
|
+
|
|
39
|
+
interface PagePreviewFieldState {
|
|
40
|
+
readonly ledger: PageLocalFieldLedger | undefined;
|
|
41
|
+
ordinal: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
export function buildPagePreviewMaps(
|
|
35
45
|
graph: RuntimePageGraph,
|
|
36
46
|
subParts: {
|
|
@@ -49,7 +59,10 @@ export function buildPagePreviewMaps(
|
|
|
49
59
|
if (source) {
|
|
50
60
|
headerPreviewByPageId.set(
|
|
51
61
|
page.pageId,
|
|
52
|
-
flattenBlocksToPreview(source.blocks, page, graph
|
|
62
|
+
flattenBlocksToPreview(source.blocks, page, graph, {
|
|
63
|
+
ledger: findPageLocalStoryFieldLedger(page, headerStory),
|
|
64
|
+
ordinal: 0,
|
|
65
|
+
}),
|
|
53
66
|
);
|
|
54
67
|
}
|
|
55
68
|
}
|
|
@@ -59,7 +72,10 @@ export function buildPagePreviewMaps(
|
|
|
59
72
|
if (source) {
|
|
60
73
|
footerPreviewByPageId.set(
|
|
61
74
|
page.pageId,
|
|
62
|
-
flattenBlocksToPreview(source.blocks, page, graph
|
|
75
|
+
flattenBlocksToPreview(source.blocks, page, graph, {
|
|
76
|
+
ledger: findPageLocalStoryFieldLedger(page, footerStory),
|
|
77
|
+
ordinal: 0,
|
|
78
|
+
}),
|
|
63
79
|
);
|
|
64
80
|
}
|
|
65
81
|
}
|
|
@@ -76,14 +92,30 @@ function findSubPart<T extends HeaderDocument | FooterDocument>(
|
|
|
76
92
|
return parts.find((p) => p.relationshipId === relationshipId);
|
|
77
93
|
}
|
|
78
94
|
|
|
95
|
+
function findPageLocalStoryFieldLedger(
|
|
96
|
+
page: RuntimePageNode,
|
|
97
|
+
story: NonNullable<RuntimePageNode["stories"]["header" | "footer"]>,
|
|
98
|
+
): PageLocalFieldLedger | undefined {
|
|
99
|
+
const pageLocalStory = page.frame?.pageLocalStories.find(
|
|
100
|
+
(candidate) =>
|
|
101
|
+
candidate.kind === story.kind &&
|
|
102
|
+
candidate.relationshipId === story.relationshipId &&
|
|
103
|
+
candidate.variant === story.variant &&
|
|
104
|
+
(story.sectionIndex === undefined ||
|
|
105
|
+
candidate.sectionIndex === story.sectionIndex),
|
|
106
|
+
);
|
|
107
|
+
return pageLocalStory?.resolvedFields;
|
|
108
|
+
}
|
|
109
|
+
|
|
79
110
|
function flattenBlocksToPreview(
|
|
80
111
|
blocks: ReadonlyArray<BlockNode>,
|
|
81
112
|
page: RuntimePageNode,
|
|
82
113
|
graph: RuntimePageGraph,
|
|
114
|
+
fieldState: PagePreviewFieldState,
|
|
83
115
|
): string {
|
|
84
116
|
const parts: string[] = [];
|
|
85
117
|
for (const block of blocks) {
|
|
86
|
-
collectBlockText(block, page, graph, parts);
|
|
118
|
+
collectBlockText(block, page, graph, fieldState, parts);
|
|
87
119
|
if (joinPreview(parts).length >= MAX_PREVIEW_CHARS) break;
|
|
88
120
|
}
|
|
89
121
|
return truncate(joinPreview(parts));
|
|
@@ -93,26 +125,27 @@ function collectBlockText(
|
|
|
93
125
|
block: BlockNode,
|
|
94
126
|
page: RuntimePageNode,
|
|
95
127
|
graph: RuntimePageGraph,
|
|
128
|
+
fieldState: PagePreviewFieldState,
|
|
96
129
|
out: string[],
|
|
97
130
|
): void {
|
|
98
131
|
switch (block.type) {
|
|
99
132
|
case "paragraph":
|
|
100
133
|
for (const child of block.children) {
|
|
101
|
-
collectInlineText(child, page, graph, out);
|
|
134
|
+
collectInlineText(child, page, graph, fieldState, out);
|
|
102
135
|
}
|
|
103
136
|
break;
|
|
104
137
|
case "table":
|
|
105
138
|
for (const row of block.rows) {
|
|
106
139
|
for (const cell of row.cells) {
|
|
107
140
|
for (const childBlock of cell.children) {
|
|
108
|
-
collectBlockText(childBlock, page, graph, out);
|
|
141
|
+
collectBlockText(childBlock, page, graph, fieldState, out);
|
|
109
142
|
}
|
|
110
143
|
}
|
|
111
144
|
}
|
|
112
145
|
break;
|
|
113
146
|
case "sdt":
|
|
114
147
|
for (const child of block.children) {
|
|
115
|
-
collectBlockText(child, page, graph, out);
|
|
148
|
+
collectBlockText(child, page, graph, fieldState, out);
|
|
116
149
|
}
|
|
117
150
|
break;
|
|
118
151
|
// opaque_block / section_break / custom_xml / alt_chunk — no preview text.
|
|
@@ -125,6 +158,7 @@ function collectInlineText(
|
|
|
125
158
|
inline: InlineNode,
|
|
126
159
|
page: RuntimePageNode,
|
|
127
160
|
graph: RuntimePageGraph,
|
|
161
|
+
fieldState: PagePreviewFieldState,
|
|
128
162
|
out: string[],
|
|
129
163
|
): void {
|
|
130
164
|
switch (inline.type) {
|
|
@@ -141,8 +175,12 @@ function collectInlineText(
|
|
|
141
175
|
const family = inline.fieldFamily ?? classifyFieldInstructionLocal(inline.instruction);
|
|
142
176
|
const cached = flattenInline(inline.children);
|
|
143
177
|
if (family === "PAGE" || family === "NUMPAGES" || family === "SECTIONPAGES") {
|
|
178
|
+
const ledgerField = fieldState.ledger?.[fieldState.ordinal];
|
|
179
|
+
fieldState.ordinal += 1;
|
|
144
180
|
out.push(
|
|
145
|
-
|
|
181
|
+
ledgerField?.family === family
|
|
182
|
+
? ledgerField.displayText
|
|
183
|
+
: resolvePageFieldDisplayText(family, cached, { page, graph }),
|
|
146
184
|
);
|
|
147
185
|
} else {
|
|
148
186
|
out.push(cached);
|
|
@@ -151,7 +189,7 @@ function collectInlineText(
|
|
|
151
189
|
}
|
|
152
190
|
case "hyperlink":
|
|
153
191
|
for (const child of inline.children) {
|
|
154
|
-
collectInlineText(child, page, graph, out);
|
|
192
|
+
collectInlineText(child, page, graph, fieldState, out);
|
|
155
193
|
}
|
|
156
194
|
break;
|
|
157
195
|
// bookmark_start/end, opaque_inline, note_ref etc — skip.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export interface TableSplitRowCarryOverlayEntry {
|
|
4
|
+
blockId: string;
|
|
5
|
+
rowIndex: number;
|
|
6
|
+
topPx: number;
|
|
7
|
+
leftPx: number;
|
|
8
|
+
widthPx: number;
|
|
9
|
+
heightPx: number;
|
|
10
|
+
continuesFromPreviousPage?: boolean;
|
|
11
|
+
continuesToNextPage?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TwTableSplitRowCarryOverlayProps {
|
|
15
|
+
entries: readonly TableSplitRowCarryOverlayEntry[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function TwTableSplitRowCarryOverlayInner({
|
|
19
|
+
entries,
|
|
20
|
+
}: TwTableSplitRowCarryOverlayProps): React.ReactElement | null {
|
|
21
|
+
if (entries.length === 0) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{entries.map((entry) => (
|
|
26
|
+
<div
|
|
27
|
+
key={`${entry.blockId}:${entry.rowIndex}:${entry.topPx}:${entry.heightPx}`}
|
|
28
|
+
aria-hidden
|
|
29
|
+
data-table-split-row-carry=""
|
|
30
|
+
data-block-id={entry.blockId}
|
|
31
|
+
data-row-index={entry.rowIndex}
|
|
32
|
+
data-continues-from-previous-page={
|
|
33
|
+
entry.continuesFromPreviousPage ? "true" : undefined
|
|
34
|
+
}
|
|
35
|
+
data-continues-to-next-page={
|
|
36
|
+
entry.continuesToNextPage ? "true" : undefined
|
|
37
|
+
}
|
|
38
|
+
style={{
|
|
39
|
+
position: "absolute",
|
|
40
|
+
top: `${entry.topPx}px`,
|
|
41
|
+
left: `${entry.leftPx}px`,
|
|
42
|
+
width: `${entry.widthPx}px`,
|
|
43
|
+
height: `${entry.heightPx}px`,
|
|
44
|
+
pointerEvents: "none",
|
|
45
|
+
boxSizing: "border-box",
|
|
46
|
+
borderTop: entry.continuesFromPreviousPage
|
|
47
|
+
? "1px dashed var(--color-border-accent)"
|
|
48
|
+
: undefined,
|
|
49
|
+
borderBottom: entry.continuesToNextPage
|
|
50
|
+
? "1px dashed var(--color-border-accent)"
|
|
51
|
+
: undefined,
|
|
52
|
+
backgroundColor: "color-mix(in srgb, var(--color-accent) 8%, transparent)",
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
))}
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const TwTableSplitRowCarryOverlay = React.memo(
|
|
61
|
+
TwTableSplitRowCarryOverlayInner,
|
|
62
|
+
);
|
|
@@ -151,6 +151,7 @@ function buildPageBreakDecorationsFromProps(
|
|
|
151
151
|
pageIndex: p.page.pageIndex,
|
|
152
152
|
startOffset: p.page.startOffset,
|
|
153
153
|
isBlankFiller: p.page.isBlankFiller,
|
|
154
|
+
frame: p.page.frame,
|
|
154
155
|
stories: {
|
|
155
156
|
displayPageNumber: p.page.stories.displayPageNumber,
|
|
156
157
|
header: p.page.stories.header,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import React from "react";
|
|
17
17
|
import type {
|
|
18
18
|
EditorStoryTarget,
|
|
19
|
+
GeometryFacet,
|
|
19
20
|
PublicPageNode,
|
|
20
21
|
SurfaceTableRowSnapshot,
|
|
21
22
|
WordReviewEditorLayoutFacet,
|
|
@@ -23,6 +24,10 @@ import type {
|
|
|
23
24
|
import { buildPageAnchorAttributes } from "../../api/v3/_page-anchor-id.ts";
|
|
24
25
|
import type { PageOverlayRect } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
|
|
25
26
|
import { TwTableContinuationHeader } from "../chrome-overlay/tw-table-continuation-header.tsx";
|
|
27
|
+
import {
|
|
28
|
+
TwTableSplitRowCarryOverlay,
|
|
29
|
+
type TableSplitRowCarryOverlayEntry,
|
|
30
|
+
} from "../chrome-overlay/tw-table-split-row-carry-overlay.tsx";
|
|
26
31
|
import { FRAME_PX_PER_TWIP_AT_96DPI } from "../tw-review-workspace.tsx";
|
|
27
32
|
import { TwPageHeaderBand } from "./tw-page-header-band.tsx";
|
|
28
33
|
import { TwPageFooterBand } from "./tw-page-footer-band.tsx";
|
|
@@ -35,6 +40,7 @@ export interface TwPageChromeEntryProps {
|
|
|
35
40
|
pageIndex: number;
|
|
36
41
|
page: PublicPageNode;
|
|
37
42
|
facet: WordReviewEditorLayoutFacet;
|
|
43
|
+
geometryFacet?: GeometryFacet;
|
|
38
44
|
activeStory: EditorStoryTarget;
|
|
39
45
|
activeStoryPageIndex?: number | null;
|
|
40
46
|
onOpenStory?: (target: EditorStoryTarget, pageIndex: number) => void;
|
|
@@ -60,6 +66,7 @@ function TwPageChromeEntryInner({
|
|
|
60
66
|
pageIndex,
|
|
61
67
|
page,
|
|
62
68
|
facet,
|
|
69
|
+
geometryFacet,
|
|
63
70
|
activeStory,
|
|
64
71
|
activeStoryPageIndex,
|
|
65
72
|
onOpenStory,
|
|
@@ -134,6 +141,16 @@ function TwPageChromeEntryInner({
|
|
|
134
141
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
135
142
|
}, [facet, pageIndex, page, renderFrameRevision]);
|
|
136
143
|
|
|
144
|
+
const splitRowCarryEntries = React.useMemo(
|
|
145
|
+
() =>
|
|
146
|
+
collectSplitRowCarryOverlayEntries({
|
|
147
|
+
geometryFacet,
|
|
148
|
+
pageIndex,
|
|
149
|
+
pageTopPx: rect.topPx,
|
|
150
|
+
}),
|
|
151
|
+
[geometryFacet, pageIndex, rect.topPx, renderFrameRevision],
|
|
152
|
+
);
|
|
153
|
+
|
|
137
154
|
const handleHeaderDoubleClick = React.useCallback(
|
|
138
155
|
() => headerStory && onOpenStory?.(headerStory, pageIndex),
|
|
139
156
|
[onOpenStory, headerStory, pageIndex],
|
|
@@ -257,6 +274,7 @@ function TwPageChromeEntryInner({
|
|
|
257
274
|
bodyOriginTopPx={px(layout.marginTop)}
|
|
258
275
|
/>
|
|
259
276
|
))}
|
|
277
|
+
<TwTableSplitRowCarryOverlay entries={splitRowCarryEntries} />
|
|
260
278
|
</div>
|
|
261
279
|
);
|
|
262
280
|
}
|
|
@@ -269,6 +287,7 @@ function propsAreEqual(
|
|
|
269
287
|
prev.pageIndex === next.pageIndex &&
|
|
270
288
|
prev.page === next.page &&
|
|
271
289
|
prev.facet === next.facet &&
|
|
290
|
+
prev.geometryFacet === next.geometryFacet &&
|
|
272
291
|
prev.activeStory === next.activeStory &&
|
|
273
292
|
prev.activeStoryPageIndex === next.activeStoryPageIndex &&
|
|
274
293
|
prev.onOpenStory === next.onOpenStory &&
|
|
@@ -285,6 +304,49 @@ function propsAreEqual(
|
|
|
285
304
|
|
|
286
305
|
export const TwPageChromeEntry = React.memo(TwPageChromeEntryInner, propsAreEqual);
|
|
287
306
|
|
|
307
|
+
function collectSplitRowCarryOverlayEntries(input: {
|
|
308
|
+
geometryFacet?: GeometryFacet;
|
|
309
|
+
pageIndex: number;
|
|
310
|
+
pageTopPx: number;
|
|
311
|
+
}): readonly TableSplitRowCarryOverlayEntry[] {
|
|
312
|
+
const index = input.geometryFacet?.getGeometryIndex();
|
|
313
|
+
if (!index) return [];
|
|
314
|
+
|
|
315
|
+
const entries: TableSplitRowCarryOverlayEntry[] = [];
|
|
316
|
+
const seen = new Set<string>();
|
|
317
|
+
for (const entry of index.semanticEntries) {
|
|
318
|
+
if (entry.kind !== "table-row") continue;
|
|
319
|
+
if (entry.pageIndex !== input.pageIndex) continue;
|
|
320
|
+
if (entry.rect.space !== "frame") continue;
|
|
321
|
+
const carries = entry.tableContinuation?.splitRowCarry ?? [];
|
|
322
|
+
if (carries.length === 0) continue;
|
|
323
|
+
const blockId = entry.blockId;
|
|
324
|
+
if (!blockId) continue;
|
|
325
|
+
for (const carry of carries) {
|
|
326
|
+
const rowIndex = entry.rowIndex ?? carry.rowIndex;
|
|
327
|
+
if (rowIndex === undefined) continue;
|
|
328
|
+
const topPx = Math.max(0, entry.rect.topPx - input.pageTopPx);
|
|
329
|
+
const leftPx = Math.max(0, entry.rect.leftPx);
|
|
330
|
+
const widthPx = Math.max(1, entry.rect.widthPx);
|
|
331
|
+
const heightPx = Math.max(1, entry.rect.heightPx);
|
|
332
|
+
const key = `${blockId}:${rowIndex}:${topPx}:${leftPx}:${widthPx}:${heightPx}`;
|
|
333
|
+
if (seen.has(key)) continue;
|
|
334
|
+
seen.add(key);
|
|
335
|
+
entries.push({
|
|
336
|
+
blockId,
|
|
337
|
+
rowIndex,
|
|
338
|
+
topPx,
|
|
339
|
+
leftPx,
|
|
340
|
+
widthPx,
|
|
341
|
+
heightPx,
|
|
342
|
+
continuesFromPreviousPage: carry.continuesFromPreviousPage,
|
|
343
|
+
continuesToNextPage: carry.continuesToNextPage,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return entries;
|
|
348
|
+
}
|
|
349
|
+
|
|
288
350
|
function isActiveStoryMatch(
|
|
289
351
|
active: EditorStoryTarget,
|
|
290
352
|
candidate: EditorStoryTarget,
|
|
@@ -503,6 +503,7 @@ const TwPageStackChromeLayerInner: React.FC<TwPageStackChromeLayerProps> = ({
|
|
|
503
503
|
pageIndex={rect.pageIndex}
|
|
504
504
|
page={page}
|
|
505
505
|
facet={facet}
|
|
506
|
+
geometryFacet={geometryFacet}
|
|
506
507
|
activeStory={activeStory}
|
|
507
508
|
activeStoryPageIndex={activeStoryPageIndex}
|
|
508
509
|
onOpenStory={handleOpenStoryForPage}
|