@beyondwork/docx-react-component 1.0.60 → 1.0.61
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/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-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
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
SurfaceTableRowSnapshot,
|
|
10
10
|
SurfaceTableCellSnapshot,
|
|
11
11
|
} from "../../api/public-types";
|
|
12
|
+
import { shouldRenderAbsoluteFloatingImageInPageOverlay } from "../page-stack/floating-image-overlay-model.ts";
|
|
12
13
|
import { editorSchema } from "./pm-schema";
|
|
13
14
|
import { buildPositionMap, type PositionMap } from "./pm-position-map";
|
|
14
15
|
import { chartModelStore } from "../../runtime/chart/chart-model-store.ts";
|
|
@@ -24,7 +25,7 @@ export interface PMStateResult {
|
|
|
24
25
|
* can pass partial objects in tests.
|
|
25
26
|
*/
|
|
26
27
|
export function buildPmStateFromSnapshot(snapshot: EditorSurfaceSnapshot): EditorState {
|
|
27
|
-
const doc = buildPMDoc(snapshot, {}, false);
|
|
28
|
+
const doc = buildPMDoc(snapshot, {}, false, false);
|
|
28
29
|
return EditorState.create({ doc, plugins: [] });
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -153,8 +154,14 @@ export function createPMStateFromSnapshot(
|
|
|
153
154
|
plugins: Plugin[],
|
|
154
155
|
mediaPreviews: Record<string, MediaPreviewDescriptor> = {},
|
|
155
156
|
showUnsupportedObjectPreviews = false,
|
|
157
|
+
renderAbsoluteFloatingObjectsInPageOverlay = false,
|
|
156
158
|
): PMStateResult {
|
|
157
|
-
const doc = buildPMDoc(
|
|
159
|
+
const doc = buildPMDoc(
|
|
160
|
+
surface,
|
|
161
|
+
mediaPreviews,
|
|
162
|
+
showUnsupportedObjectPreviews,
|
|
163
|
+
renderAbsoluteFloatingObjectsInPageOverlay,
|
|
164
|
+
);
|
|
158
165
|
const positionMap = buildPositionMap(surface);
|
|
159
166
|
const pmSelection = createPMSelectionFromSnapshot(doc, positionMap, selection);
|
|
160
167
|
|
|
@@ -229,8 +236,14 @@ function buildPMDoc(
|
|
|
229
236
|
surface: EditorSurfaceSnapshot,
|
|
230
237
|
mediaPreviews: Record<string, MediaPreviewDescriptor>,
|
|
231
238
|
showUnsupportedObjectPreviews: boolean,
|
|
239
|
+
renderAbsoluteFloatingObjectsInPageOverlay: boolean,
|
|
232
240
|
): PMNode {
|
|
233
|
-
const blocks = buildPMBlocks(
|
|
241
|
+
const blocks = buildPMBlocks(
|
|
242
|
+
surface.blocks,
|
|
243
|
+
mediaPreviews,
|
|
244
|
+
showUnsupportedObjectPreviews,
|
|
245
|
+
renderAbsoluteFloatingObjectsInPageOverlay,
|
|
246
|
+
);
|
|
234
247
|
|
|
235
248
|
// Ensure at least one block (PM requires non-empty doc)
|
|
236
249
|
if (blocks.length === 0) {
|
|
@@ -244,6 +257,7 @@ function buildPMBlocks(
|
|
|
244
257
|
blocks: SurfaceBlockSnapshot[],
|
|
245
258
|
mediaPreviews: Record<string, MediaPreviewDescriptor>,
|
|
246
259
|
showUnsupportedObjectPreviews: boolean,
|
|
260
|
+
renderAbsoluteFloatingObjectsInPageOverlay: boolean,
|
|
247
261
|
): PMNode[] {
|
|
248
262
|
const nodes: PMNode[] = [];
|
|
249
263
|
|
|
@@ -262,12 +276,27 @@ function buildPMBlocks(
|
|
|
262
276
|
nextParagraph,
|
|
263
277
|
mediaPreviews,
|
|
264
278
|
showUnsupportedObjectPreviews,
|
|
279
|
+
renderAbsoluteFloatingObjectsInPageOverlay,
|
|
265
280
|
),
|
|
266
281
|
);
|
|
267
282
|
} else if (block.kind === "table") {
|
|
268
|
-
nodes.push(
|
|
283
|
+
nodes.push(
|
|
284
|
+
buildTable(
|
|
285
|
+
block,
|
|
286
|
+
mediaPreviews,
|
|
287
|
+
showUnsupportedObjectPreviews,
|
|
288
|
+
renderAbsoluteFloatingObjectsInPageOverlay,
|
|
289
|
+
),
|
|
290
|
+
);
|
|
269
291
|
} else if (block.kind === "sdt_block") {
|
|
270
|
-
nodes.push(
|
|
292
|
+
nodes.push(
|
|
293
|
+
buildSdtBlock(
|
|
294
|
+
block,
|
|
295
|
+
mediaPreviews,
|
|
296
|
+
showUnsupportedObjectPreviews,
|
|
297
|
+
renderAbsoluteFloatingObjectsInPageOverlay,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
271
300
|
} else {
|
|
272
301
|
nodes.push(buildOpaqueBlock(block, showUnsupportedObjectPreviews));
|
|
273
302
|
}
|
|
@@ -282,6 +311,7 @@ function buildParagraph(
|
|
|
282
311
|
nextParagraph: Extract<SurfaceBlockSnapshot, { kind: "paragraph" }> | null,
|
|
283
312
|
mediaPreviews: Record<string, MediaPreviewDescriptor>,
|
|
284
313
|
showUnsupportedObjectPreviews: boolean,
|
|
314
|
+
renderAbsoluteFloatingObjectsInPageOverlay: boolean,
|
|
285
315
|
): PMNode {
|
|
286
316
|
const content: PMNode[] = [];
|
|
287
317
|
const paragraphLayout = resolveParagraphLayout(block);
|
|
@@ -318,7 +348,12 @@ function buildParagraph(
|
|
|
318
348
|
);
|
|
319
349
|
tabIndex++;
|
|
320
350
|
} else {
|
|
321
|
-
const nodes = buildInlineContent(
|
|
351
|
+
const nodes = buildInlineContent(
|
|
352
|
+
segment,
|
|
353
|
+
mediaPreviews,
|
|
354
|
+
showUnsupportedObjectPreviews,
|
|
355
|
+
renderAbsoluteFloatingObjectsInPageOverlay,
|
|
356
|
+
);
|
|
322
357
|
content.push(...nodes);
|
|
323
358
|
}
|
|
324
359
|
}
|
|
@@ -429,6 +464,7 @@ function buildInlineContent(
|
|
|
429
464
|
segment: SurfaceInlineSegment,
|
|
430
465
|
mediaPreviews: Record<string, MediaPreviewDescriptor>,
|
|
431
466
|
showUnsupportedObjectPreviews: boolean,
|
|
467
|
+
renderAbsoluteFloatingObjectsInPageOverlay: boolean,
|
|
432
468
|
): PMNode[] {
|
|
433
469
|
switch (segment.kind) {
|
|
434
470
|
case "text": {
|
|
@@ -451,6 +487,9 @@ function buildInlineContent(
|
|
|
451
487
|
case "image":
|
|
452
488
|
{
|
|
453
489
|
const preview = mediaPreviews[segment.mediaId];
|
|
490
|
+
const renderInPageOverlay =
|
|
491
|
+
renderAbsoluteFloatingObjectsInPageOverlay &&
|
|
492
|
+
shouldRenderAbsoluteFloatingImageInPageOverlay(segment.anchor);
|
|
454
493
|
return [
|
|
455
494
|
editorSchema.nodes.image_atom.create({
|
|
456
495
|
mediaId: segment.mediaId,
|
|
@@ -470,6 +509,8 @@ function buildInlineContent(
|
|
|
470
509
|
wrapMode: segment.anchor?.wrapMode ?? null,
|
|
471
510
|
distMargins: segment.anchor?.distMargins ?? null,
|
|
472
511
|
positionH: segment.anchor?.positionH ?? null,
|
|
512
|
+
positionV: segment.anchor?.positionV ?? null,
|
|
513
|
+
renderInPageOverlay,
|
|
473
514
|
// Lane 6d N9.b — polygon clip.
|
|
474
515
|
wrapPolygon: segment.anchor?.wrapPolygon ?? null,
|
|
475
516
|
// Lane 6d N11.b — filter effects.
|
|
@@ -528,6 +569,7 @@ function buildTable(
|
|
|
528
569
|
block: Extract<SurfaceBlockSnapshot, { kind: "table" }>,
|
|
529
570
|
mediaPreviews: Record<string, MediaPreviewDescriptor>,
|
|
530
571
|
showUnsupportedObjectPreviews: boolean,
|
|
572
|
+
renderAbsoluteFloatingObjectsInPageOverlay: boolean,
|
|
531
573
|
): PMNode {
|
|
532
574
|
const rows: PMNode[] = [];
|
|
533
575
|
for (const row of block.rows) {
|
|
@@ -537,6 +579,7 @@ function buildTable(
|
|
|
537
579
|
cell.content,
|
|
538
580
|
mediaPreviews,
|
|
539
581
|
showUnsupportedObjectPreviews,
|
|
582
|
+
renderAbsoluteFloatingObjectsInPageOverlay,
|
|
540
583
|
);
|
|
541
584
|
// Ensure at least one paragraph in cell (PM requires non-empty)
|
|
542
585
|
if (cellContent.length === 0) {
|
|
@@ -558,6 +601,10 @@ function buildTable(
|
|
|
558
601
|
borderRight: cell.borderRight ?? null,
|
|
559
602
|
borderBottom: cell.borderBottom ?? null,
|
|
560
603
|
borderLeft: cell.borderLeft ?? null,
|
|
604
|
+
paddingTop: cell.paddingTop ?? null,
|
|
605
|
+
paddingRight: cell.paddingRight ?? null,
|
|
606
|
+
paddingBottom: cell.paddingBottom ?? null,
|
|
607
|
+
paddingLeft: cell.paddingLeft ?? null,
|
|
561
608
|
// R3.a Phase 2 — per-cell text-flow direction.
|
|
562
609
|
textDirection: cell.textDirection ?? null,
|
|
563
610
|
bandClasses: cell.bandClasses ?? null,
|
|
@@ -651,11 +698,13 @@ function buildSdtBlock(
|
|
|
651
698
|
block: Extract<SurfaceBlockSnapshot, { kind: "sdt_block" }>,
|
|
652
699
|
mediaPreviews: Record<string, MediaPreviewDescriptor>,
|
|
653
700
|
showUnsupportedObjectPreviews: boolean,
|
|
701
|
+
renderAbsoluteFloatingObjectsInPageOverlay: boolean,
|
|
654
702
|
): PMNode {
|
|
655
703
|
const children = buildPMBlocks(
|
|
656
704
|
block.children,
|
|
657
705
|
mediaPreviews,
|
|
658
706
|
showUnsupportedObjectPreviews,
|
|
707
|
+
renderAbsoluteFloatingObjectsInPageOverlay,
|
|
659
708
|
);
|
|
660
709
|
|
|
661
710
|
if (children.length === 0) {
|
|
@@ -22,6 +22,7 @@ export function createSurfaceDocumentBuildKey(input: {
|
|
|
22
22
|
activeStory: EditorStoryTarget;
|
|
23
23
|
mediaPreviewKey: string;
|
|
24
24
|
showUnsupportedObjectPreviews?: boolean;
|
|
25
|
+
isPageWorkspace?: boolean;
|
|
25
26
|
}): string {
|
|
26
27
|
const vp = input.surface?.viewportBlockRange ?? null;
|
|
27
28
|
return JSON.stringify({
|
|
@@ -32,6 +33,7 @@ export function createSurfaceDocumentBuildKey(input: {
|
|
|
32
33
|
activeStory: input.activeStory,
|
|
33
34
|
mediaPreviewKey: input.mediaPreviewKey,
|
|
34
35
|
showUnsupportedObjectPreviews: input.showUnsupportedObjectPreviews ?? false,
|
|
36
|
+
isPageWorkspace: input.isPageWorkspace ?? false,
|
|
35
37
|
viewport: vp ? `${vp.start}:${vp.end}` : "full",
|
|
36
38
|
});
|
|
37
39
|
}
|
|
@@ -223,6 +223,10 @@ function TableBlock({
|
|
|
223
223
|
if (cell.borderRight) cellStyle.borderRight = cell.borderRight;
|
|
224
224
|
if (cell.borderBottom) cellStyle.borderBottom = cell.borderBottom;
|
|
225
225
|
if (cell.borderLeft) cellStyle.borderLeft = cell.borderLeft;
|
|
226
|
+
if (typeof cell.paddingTop === "number") cellStyle.paddingTop = `${cell.paddingTop / 20}pt`;
|
|
227
|
+
if (typeof cell.paddingRight === "number") cellStyle.paddingRight = `${cell.paddingRight / 20}pt`;
|
|
228
|
+
if (typeof cell.paddingBottom === "number") cellStyle.paddingBottom = `${cell.paddingBottom / 20}pt`;
|
|
229
|
+
if (typeof cell.paddingLeft === "number") cellStyle.paddingLeft = `${cell.paddingLeft / 20}pt`;
|
|
226
230
|
|
|
227
231
|
return (
|
|
228
232
|
<td
|
|
@@ -399,8 +399,15 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
399
399
|
activeStory: snapshot.activeStory,
|
|
400
400
|
mediaPreviewKey,
|
|
401
401
|
showUnsupportedObjectPreviews: props.showUnsupportedObjectPreviews,
|
|
402
|
+
isPageWorkspace: props.isPageWorkspace,
|
|
402
403
|
}),
|
|
403
|
-
[
|
|
404
|
+
[
|
|
405
|
+
mediaPreviewKey,
|
|
406
|
+
props.isPageWorkspace,
|
|
407
|
+
props.showUnsupportedObjectPreviews,
|
|
408
|
+
snapshot.activeStory,
|
|
409
|
+
surface,
|
|
410
|
+
],
|
|
404
411
|
);
|
|
405
412
|
const decorationBuildKey = useMemo(
|
|
406
413
|
() =>
|
|
@@ -718,6 +725,7 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
718
725
|
plugins,
|
|
719
726
|
props.mediaPreviews,
|
|
720
727
|
props.showUnsupportedObjectPreviews,
|
|
728
|
+
props.isPageWorkspace,
|
|
721
729
|
);
|
|
722
730
|
positionMapRef.current = positionMap;
|
|
723
731
|
const decorations = buildDecorations(
|
|
@@ -488,6 +488,10 @@ function applyCellAttrs(cell: HTMLTableCellElement, node: PMNode, isHeader: bool
|
|
|
488
488
|
const borderRight = node.attrs.borderRight as string | null | undefined;
|
|
489
489
|
const borderBottom = node.attrs.borderBottom as string | null | undefined;
|
|
490
490
|
const borderLeft = node.attrs.borderLeft as string | null | undefined;
|
|
491
|
+
const paddingTop = node.attrs.paddingTop as number | null | undefined;
|
|
492
|
+
const paddingRight = node.attrs.paddingRight as number | null | undefined;
|
|
493
|
+
const paddingBottom = node.attrs.paddingBottom as number | null | undefined;
|
|
494
|
+
const paddingLeft = node.attrs.paddingLeft as number | null | undefined;
|
|
491
495
|
const bandClasses = node.attrs.bandClasses as string | null | undefined;
|
|
492
496
|
|
|
493
497
|
if (backgroundColor) cell.setAttribute("data-cell-background", backgroundColor);
|
|
@@ -502,6 +506,14 @@ function applyCellAttrs(cell: HTMLTableCellElement, node: PMNode, isHeader: bool
|
|
|
502
506
|
else cell.removeAttribute("data-border-bottom");
|
|
503
507
|
if (borderLeft) cell.setAttribute("data-border-left", borderLeft);
|
|
504
508
|
else cell.removeAttribute("data-border-left");
|
|
509
|
+
if (typeof paddingTop === "number") cell.setAttribute("data-cell-padding-top", String(paddingTop));
|
|
510
|
+
else cell.removeAttribute("data-cell-padding-top");
|
|
511
|
+
if (typeof paddingRight === "number") cell.setAttribute("data-cell-padding-right", String(paddingRight));
|
|
512
|
+
else cell.removeAttribute("data-cell-padding-right");
|
|
513
|
+
if (typeof paddingBottom === "number") cell.setAttribute("data-cell-padding-bottom", String(paddingBottom));
|
|
514
|
+
else cell.removeAttribute("data-cell-padding-bottom");
|
|
515
|
+
if (typeof paddingLeft === "number") cell.setAttribute("data-cell-padding-left", String(paddingLeft));
|
|
516
|
+
else cell.removeAttribute("data-cell-padding-left");
|
|
505
517
|
|
|
506
518
|
// R2b: band classes come from the resolved style cascade. They layer on top
|
|
507
519
|
// of the base Tailwind classes so theme vars (bg-surface-*, text-secondary,
|
|
@@ -522,6 +534,10 @@ function applyCellAttrs(cell: HTMLTableCellElement, node: PMNode, isHeader: bool
|
|
|
522
534
|
cell.style.borderRight = borderRight ?? "";
|
|
523
535
|
cell.style.borderBottom = borderBottom ?? "";
|
|
524
536
|
cell.style.borderLeft = borderLeft ?? "";
|
|
537
|
+
cell.style.paddingTop = typeof paddingTop === "number" ? `${paddingTop / 20}pt` : "";
|
|
538
|
+
cell.style.paddingRight = typeof paddingRight === "number" ? `${paddingRight / 20}pt` : "";
|
|
539
|
+
cell.style.paddingBottom = typeof paddingBottom === "number" ? `${paddingBottom / 20}pt` : "";
|
|
540
|
+
cell.style.paddingLeft = typeof paddingLeft === "number" ? `${paddingLeft / 20}pt` : "";
|
|
525
541
|
|
|
526
542
|
// R3.a Phase 2 — vertical text direction. OOXML: tbRl = top→bottom, right→left
|
|
527
543
|
// (most common for vertical headers, reads when tilting the head right);
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
EditorStoryTarget,
|
|
3
|
+
EditorSurfaceSnapshot,
|
|
4
|
+
PublicPageNode,
|
|
5
|
+
SurfaceBlockSnapshot,
|
|
6
|
+
SurfaceDrawingAnchor,
|
|
7
|
+
SurfaceInlineSegment,
|
|
8
|
+
} from "../../api/public-types.ts";
|
|
9
|
+
import type { WordReviewEditorLayoutFacet } from "../../runtime/layout/index.ts";
|
|
10
|
+
import { storyTargetKey } from "../../runtime/story-targeting.ts";
|
|
11
|
+
import { EMU_PER_PX } from "../../runtime/units.ts";
|
|
12
|
+
import type { PageOverlayRect } from "../chrome-overlay/tw-page-stack-overlay-layer.tsx";
|
|
13
|
+
|
|
14
|
+
const FRAME_PX_PER_TWIP_AT_96DPI = 96 / 1440;
|
|
15
|
+
|
|
16
|
+
export interface FloatingImagePreviewDescriptor {
|
|
17
|
+
src: string;
|
|
18
|
+
widthEmu?: number;
|
|
19
|
+
heightEmu?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FloatingImageOverlayItem {
|
|
23
|
+
key: string;
|
|
24
|
+
mediaId: string;
|
|
25
|
+
from: number;
|
|
26
|
+
to: number;
|
|
27
|
+
pageId: string;
|
|
28
|
+
pageIndex: number;
|
|
29
|
+
topPx: number;
|
|
30
|
+
leftPx: number;
|
|
31
|
+
widthPx: number;
|
|
32
|
+
heightPx: number;
|
|
33
|
+
behindDoc: boolean;
|
|
34
|
+
src: string | null;
|
|
35
|
+
altText: string | null;
|
|
36
|
+
detail: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SUPPORTED_HORIZONTAL_RELATIVE_FROM = new Set(["page", "margin"]);
|
|
40
|
+
const SUPPORTED_VERTICAL_RELATIVE_FROM = new Set(["page", "margin"]);
|
|
41
|
+
|
|
42
|
+
export function shouldRenderAbsoluteFloatingImageInPageOverlay(
|
|
43
|
+
anchor: SurfaceDrawingAnchor | undefined,
|
|
44
|
+
): boolean {
|
|
45
|
+
if (!anchor || anchor.display !== "floating" || anchor.wrapMode !== "none") {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (anchor.layoutInCell) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (!anchor.positionH || !anchor.positionV) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (!SUPPORTED_HORIZONTAL_RELATIVE_FROM.has(anchor.positionH.relativeFrom)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (!SUPPORTED_VERTICAL_RELATIVE_FROM.has(anchor.positionV.relativeFrom)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function collectFloatingImageOverlayItems(input: {
|
|
64
|
+
surface: EditorSurfaceSnapshot | null | undefined;
|
|
65
|
+
activeStory: EditorStoryTarget;
|
|
66
|
+
facet: WordReviewEditorLayoutFacet;
|
|
67
|
+
pageRects: readonly PageOverlayRect[];
|
|
68
|
+
mediaPreviews?: Record<string, FloatingImagePreviewDescriptor>;
|
|
69
|
+
}): FloatingImageOverlayItem[] {
|
|
70
|
+
const { surface, activeStory, facet } = input;
|
|
71
|
+
if (!surface) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const rectByPageIndex = new Map<number, PageOverlayRect>(
|
|
76
|
+
input.pageRects.map((rect) => [rect.pageIndex, rect]),
|
|
77
|
+
);
|
|
78
|
+
const items: FloatingImageOverlayItem[] = [];
|
|
79
|
+
|
|
80
|
+
walkSurfaceBlocks(surface.blocks, (segment) => {
|
|
81
|
+
if (segment.kind !== "image" || !shouldRenderAbsoluteFloatingImageInPageOverlay(segment.anchor)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pages = resolveTargetPages(facet, segment.from, activeStory);
|
|
86
|
+
for (const page of pages) {
|
|
87
|
+
const pageRect = rectByPageIndex.get(page.pageIndex);
|
|
88
|
+
if (!pageRect) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const localRect = resolveFloatingImageLocalRect(page, activeStory, segment);
|
|
92
|
+
if (!localRect) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const preview = input.mediaPreviews?.[segment.mediaId];
|
|
96
|
+
items.push({
|
|
97
|
+
key: `${segment.segmentId}:${page.pageId}`,
|
|
98
|
+
mediaId: segment.mediaId,
|
|
99
|
+
from: segment.from,
|
|
100
|
+
to: segment.to,
|
|
101
|
+
pageId: page.pageId,
|
|
102
|
+
pageIndex: page.pageIndex,
|
|
103
|
+
topPx: pageRect.topPx + localRect.topPx,
|
|
104
|
+
leftPx: localRect.leftPx,
|
|
105
|
+
widthPx: localRect.widthPx,
|
|
106
|
+
heightPx: localRect.heightPx,
|
|
107
|
+
behindDoc: Boolean(segment.anchor?.behindDoc),
|
|
108
|
+
src: preview?.src ?? null,
|
|
109
|
+
altText: segment.altText ?? null,
|
|
110
|
+
detail: segment.detail ?? null,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return items;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function walkSurfaceBlocks(
|
|
119
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
120
|
+
visit: (segment: SurfaceInlineSegment) => void,
|
|
121
|
+
): void {
|
|
122
|
+
for (const block of blocks) {
|
|
123
|
+
switch (block.kind) {
|
|
124
|
+
case "paragraph":
|
|
125
|
+
block.segments.forEach(visit);
|
|
126
|
+
break;
|
|
127
|
+
case "table":
|
|
128
|
+
for (const row of block.rows) {
|
|
129
|
+
for (const cell of row.cells) {
|
|
130
|
+
walkSurfaceBlocks(cell.content, visit);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
case "sdt_block":
|
|
135
|
+
walkSurfaceBlocks(block.children, visit);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function resolveTargetPages(
|
|
142
|
+
facet: WordReviewEditorLayoutFacet,
|
|
143
|
+
offset: number,
|
|
144
|
+
activeStory: EditorStoryTarget,
|
|
145
|
+
): PublicPageNode[] {
|
|
146
|
+
if (activeStory.kind === "main") {
|
|
147
|
+
const page = facet.getPageForOffset(offset, activeStory);
|
|
148
|
+
return page ? [page] : [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const activeKey = storyTargetKey(activeStory);
|
|
152
|
+
return facet.getPages().filter((page) => {
|
|
153
|
+
const target =
|
|
154
|
+
activeStory.kind === "header"
|
|
155
|
+
? page.stories.header
|
|
156
|
+
: activeStory.kind === "footer"
|
|
157
|
+
? page.stories.footer
|
|
158
|
+
: undefined;
|
|
159
|
+
return target ? storyTargetKey(target) === activeKey : false;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveFloatingImageLocalRect(
|
|
164
|
+
page: PublicPageNode,
|
|
165
|
+
activeStory: EditorStoryTarget,
|
|
166
|
+
segment: Extract<SurfaceInlineSegment, { kind: "image" }>,
|
|
167
|
+
): {
|
|
168
|
+
topPx: number;
|
|
169
|
+
leftPx: number;
|
|
170
|
+
widthPx: number;
|
|
171
|
+
heightPx: number;
|
|
172
|
+
} | null {
|
|
173
|
+
const anchor = segment.anchor;
|
|
174
|
+
if (!anchor) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const widthPx = Math.max(24, Math.round(anchor.extent.widthEmu / EMU_PER_PX));
|
|
179
|
+
const heightPx = Math.max(24, Math.round(anchor.extent.heightEmu / EMU_PER_PX));
|
|
180
|
+
const horizontalSpace = resolveHorizontalSpace(page, activeStory, anchor);
|
|
181
|
+
const verticalSpace = resolveVerticalSpace(page, activeStory, anchor);
|
|
182
|
+
|
|
183
|
+
if (!horizontalSpace || !verticalSpace) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
topPx: resolveAxisPosition(verticalSpace, heightPx, anchor.positionV, page, "vertical"),
|
|
189
|
+
leftPx: resolveAxisPosition(horizontalSpace, widthPx, anchor.positionH, page, "horizontal"),
|
|
190
|
+
widthPx,
|
|
191
|
+
heightPx,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolveHorizontalSpace(
|
|
196
|
+
page: PublicPageNode,
|
|
197
|
+
activeStory: EditorStoryTarget,
|
|
198
|
+
anchor: SurfaceDrawingAnchor,
|
|
199
|
+
): { startPx: number; sizePx: number } | null {
|
|
200
|
+
const pageWidthPx = twipsToPx(page.layout.pageWidth);
|
|
201
|
+
const storyHost = resolveStoryHostSpace(page, activeStory);
|
|
202
|
+
switch (anchor.positionH?.relativeFrom) {
|
|
203
|
+
case "page":
|
|
204
|
+
return { startPx: 0, sizePx: pageWidthPx };
|
|
205
|
+
case "margin":
|
|
206
|
+
return storyHost ? { startPx: storyHost.leftPx, sizePx: storyHost.widthPx } : null;
|
|
207
|
+
default:
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolveVerticalSpace(
|
|
213
|
+
page: PublicPageNode,
|
|
214
|
+
activeStory: EditorStoryTarget,
|
|
215
|
+
anchor: SurfaceDrawingAnchor,
|
|
216
|
+
): { startPx: number; sizePx: number } | null {
|
|
217
|
+
const pageHeightPx = twipsToPx(page.layout.pageHeight);
|
|
218
|
+
const storyHost = resolveStoryHostSpace(page, activeStory);
|
|
219
|
+
switch (anchor.positionV?.relativeFrom) {
|
|
220
|
+
case "page":
|
|
221
|
+
return { startPx: 0, sizePx: pageHeightPx };
|
|
222
|
+
case "margin":
|
|
223
|
+
return storyHost ? { startPx: storyHost.topPx, sizePx: storyHost.heightPx } : null;
|
|
224
|
+
default:
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resolveStoryHostSpace(
|
|
230
|
+
page: PublicPageNode,
|
|
231
|
+
activeStory: EditorStoryTarget,
|
|
232
|
+
): { topPx: number; leftPx: number; widthPx: number; heightPx: number } | null {
|
|
233
|
+
switch (activeStory.kind) {
|
|
234
|
+
case "main":
|
|
235
|
+
return {
|
|
236
|
+
topPx: twipsToPx(page.regions.body.originTwips),
|
|
237
|
+
leftPx: twipsToPx(page.layout.marginLeft),
|
|
238
|
+
widthPx: twipsToPx(page.regions.body.widthTwips),
|
|
239
|
+
heightPx: twipsToPx(page.regions.body.heightTwips),
|
|
240
|
+
};
|
|
241
|
+
case "header":
|
|
242
|
+
return page.regions.header
|
|
243
|
+
? {
|
|
244
|
+
topPx: twipsToPx(page.regions.header.originTwips),
|
|
245
|
+
leftPx: twipsToPx(page.layout.marginLeft),
|
|
246
|
+
widthPx: twipsToPx(page.regions.header.widthTwips),
|
|
247
|
+
heightPx: twipsToPx(page.regions.header.heightTwips),
|
|
248
|
+
}
|
|
249
|
+
: null;
|
|
250
|
+
case "footer":
|
|
251
|
+
return page.regions.footer
|
|
252
|
+
? {
|
|
253
|
+
topPx: twipsToPx(page.regions.footer.originTwips),
|
|
254
|
+
leftPx: twipsToPx(page.layout.marginLeft),
|
|
255
|
+
widthPx: twipsToPx(page.regions.footer.widthTwips),
|
|
256
|
+
heightPx: twipsToPx(page.regions.footer.heightTwips),
|
|
257
|
+
}
|
|
258
|
+
: null;
|
|
259
|
+
default:
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveAxisPosition(
|
|
265
|
+
space: { startPx: number; sizePx: number },
|
|
266
|
+
objectSizePx: number,
|
|
267
|
+
axis:
|
|
268
|
+
| { relativeFrom: string; align?: string; offset?: number }
|
|
269
|
+
| undefined,
|
|
270
|
+
page: PublicPageNode,
|
|
271
|
+
orientation: "horizontal" | "vertical",
|
|
272
|
+
): number {
|
|
273
|
+
if (!axis) {
|
|
274
|
+
return space.startPx;
|
|
275
|
+
}
|
|
276
|
+
if (axis.align) {
|
|
277
|
+
return alignAxisPosition(space, objectSizePx, axis.align, page, orientation);
|
|
278
|
+
}
|
|
279
|
+
if (axis.offset !== undefined) {
|
|
280
|
+
return space.startPx + Math.round(axis.offset / EMU_PER_PX);
|
|
281
|
+
}
|
|
282
|
+
return space.startPx;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function alignAxisPosition(
|
|
286
|
+
space: { startPx: number; sizePx: number },
|
|
287
|
+
objectSizePx: number,
|
|
288
|
+
align: string,
|
|
289
|
+
page: PublicPageNode,
|
|
290
|
+
orientation: "horizontal" | "vertical",
|
|
291
|
+
): number {
|
|
292
|
+
const remainder = Math.max(0, space.sizePx - objectSizePx);
|
|
293
|
+
switch (align) {
|
|
294
|
+
case "left":
|
|
295
|
+
case "top":
|
|
296
|
+
return space.startPx;
|
|
297
|
+
case "center":
|
|
298
|
+
return space.startPx + remainder / 2;
|
|
299
|
+
case "right":
|
|
300
|
+
case "bottom":
|
|
301
|
+
return space.startPx + remainder;
|
|
302
|
+
case "inside":
|
|
303
|
+
if (orientation === "horizontal") {
|
|
304
|
+
return page.isEvenPage ? space.startPx + remainder : space.startPx;
|
|
305
|
+
}
|
|
306
|
+
return space.startPx;
|
|
307
|
+
case "outside":
|
|
308
|
+
if (orientation === "horizontal") {
|
|
309
|
+
return page.isEvenPage ? space.startPx : space.startPx + remainder;
|
|
310
|
+
}
|
|
311
|
+
return space.startPx + remainder;
|
|
312
|
+
default:
|
|
313
|
+
return space.startPx;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function twipsToPx(value: number): number {
|
|
318
|
+
return value * FRAME_PX_PER_TWIP_AT_96DPI;
|
|
319
|
+
}
|