@beyondwork/docx-react-component 1.0.42 → 1.0.43
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 +30 -41
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/public-types.ts +194 -1
- package/src/core/commands/index.ts +33 -8
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +13 -0
- package/src/io/docx-session.ts +672 -2
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -0
- package/src/io/ooxml/workflow-payload-validator.ts +97 -1
- package/src/io/ooxml/workflow-payload.ts +172 -1
- package/src/runtime/collab-session.ts +1 -1
- package/src/runtime/document-runtime.ts +364 -36
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +17 -2
- package/src/runtime/layout/paginated-layout-engine.ts +211 -14
- package/src/runtime/layout/public-facet.ts +400 -1
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/workflow-markup.ts +71 -16
- package/src/ui/WordReviewEditor.tsx +67 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +110 -11
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-schema.ts +152 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/index.ts +5 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/tw-review-workspace.tsx +172 -94
|
@@ -17,7 +17,10 @@ import { MAIN_STORY_TARGET } from "../../core/selection/mapping.ts";
|
|
|
17
17
|
import { recordPerfSample } from "../../ui-tailwind/editor-surface/perf-probe.ts";
|
|
18
18
|
import type { EditorStoryTarget } from "../../api/public-types";
|
|
19
19
|
import type {
|
|
20
|
+
PublicBlockFragment,
|
|
20
21
|
PublicPageNode,
|
|
22
|
+
PublicPageRegion,
|
|
23
|
+
PublicRegionBlock,
|
|
21
24
|
WordReviewEditorLayoutFacet,
|
|
22
25
|
} from "../layout/public-facet.ts";
|
|
23
26
|
import type {
|
|
@@ -305,6 +308,7 @@ function buildPage(
|
|
|
305
308
|
zoom,
|
|
306
309
|
"header",
|
|
307
310
|
page.stories.header,
|
|
311
|
+
facet,
|
|
308
312
|
);
|
|
309
313
|
}
|
|
310
314
|
if (page.stories.footer) {
|
|
@@ -314,9 +318,18 @@ function buildPage(
|
|
|
314
318
|
zoom,
|
|
315
319
|
"footer",
|
|
316
320
|
page.stories.footer,
|
|
321
|
+
facet,
|
|
317
322
|
);
|
|
318
323
|
}
|
|
319
324
|
|
|
325
|
+
const footnoteRegions = buildFootnoteRegions(page, topPx, zoom, facet);
|
|
326
|
+
if (footnoteRegions.length > 0) {
|
|
327
|
+
regions.footnotes = footnoteRegions;
|
|
328
|
+
}
|
|
329
|
+
// Endnotes intentionally skipped — per-page endnote projection is not
|
|
330
|
+
// populated; endnotes use document-end placement via
|
|
331
|
+
// `facet.getDocumentEndnoteBlocks()`.
|
|
332
|
+
|
|
320
333
|
const chromeReservations: PageChromeReservations = {
|
|
321
334
|
...defaultChromeReservations(layout, zoom),
|
|
322
335
|
};
|
|
@@ -440,51 +453,181 @@ function buildHeaderFooterRegion(
|
|
|
440
453
|
zoom: RenderZoom,
|
|
441
454
|
kind: "header" | "footer",
|
|
442
455
|
storyTarget: EditorStoryTarget,
|
|
456
|
+
facet: WordReviewEditorLayoutFacet,
|
|
443
457
|
): RenderStoryRegion {
|
|
444
458
|
const layout = page.layout;
|
|
445
|
-
const
|
|
459
|
+
const fallbackWidthTwips =
|
|
446
460
|
layout.pageWidth - layout.marginLeft - layout.marginRight;
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
461
|
+
const fallbackTopTwips =
|
|
462
|
+
kind === "header"
|
|
463
|
+
? (layout.headerMargin ?? 720)
|
|
464
|
+
: layout.pageHeight - layout.marginBottom;
|
|
465
|
+
const fallbackHeightTwips =
|
|
466
|
+
kind === "header"
|
|
467
|
+
? Math.max(0, layout.marginTop - (layout.headerMargin ?? 720))
|
|
468
|
+
: Math.max(0, layout.marginBottom - (layout.footerMargin ?? 720));
|
|
469
|
+
|
|
470
|
+
const region: PublicPageRegion =
|
|
471
|
+
(kind === "header" ? page.regions.header : page.regions.footer) ?? {
|
|
472
|
+
kind,
|
|
473
|
+
originTwips: fallbackTopTwips,
|
|
474
|
+
widthTwips: fallbackWidthTwips,
|
|
475
|
+
heightTwips: fallbackHeightTwips,
|
|
476
|
+
fragmentCount: 0,
|
|
477
|
+
};
|
|
456
478
|
|
|
457
479
|
const frame: RenderFrameRect = {
|
|
458
480
|
leftPx: layout.marginLeft * zoom.pxPerTwip,
|
|
459
|
-
topPx: pageTopPx +
|
|
460
|
-
widthPx: widthTwips * zoom.pxPerTwip,
|
|
461
|
-
heightPx: heightTwips * zoom.pxPerTwip,
|
|
481
|
+
topPx: pageTopPx + region.originTwips * zoom.pxPerTwip,
|
|
482
|
+
widthPx: region.widthTwips * zoom.pxPerTwip,
|
|
483
|
+
heightPx: region.heightTwips * zoom.pxPerTwip,
|
|
462
484
|
};
|
|
463
485
|
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
fragmentCount: 0,
|
|
474
|
-
},
|
|
475
|
-
frame,
|
|
476
|
-
blocks: [],
|
|
477
|
-
};
|
|
478
|
-
}
|
|
486
|
+
const regionBlocks = facet.getStoryBlocksForRegion(page.pageIndex, kind);
|
|
487
|
+
const blocks = projectRegionBlocks(
|
|
488
|
+
regionBlocks,
|
|
489
|
+
frame,
|
|
490
|
+
zoom.pxPerTwip,
|
|
491
|
+
page.pageId,
|
|
492
|
+
page.pageIndex,
|
|
493
|
+
kind,
|
|
494
|
+
);
|
|
479
495
|
|
|
480
496
|
return {
|
|
481
497
|
storyTarget,
|
|
482
498
|
region,
|
|
483
499
|
frame,
|
|
484
|
-
blocks
|
|
500
|
+
blocks,
|
|
485
501
|
};
|
|
486
502
|
}
|
|
487
503
|
|
|
504
|
+
/**
|
|
505
|
+
* P8.3 — Build one `RenderStoryRegion` per `page.regions.footnotes` entry.
|
|
506
|
+
* The page graph reserves footnote regions at the bottom of the page
|
|
507
|
+
* (above the footer band) when `noteAllocations` produced fragments.
|
|
508
|
+
* Blocks come from `facet.getStoryBlocksForRegion(pageIndex, "footnote-area")`
|
|
509
|
+
* — one entry per allocated note body, stacked vertically.
|
|
510
|
+
*
|
|
511
|
+
* Currently the page graph emits a single footnote region per page
|
|
512
|
+
* covering every allocation, so the returned array has length 0 or 1. The
|
|
513
|
+
* shape allows for future allocation-splitting without changing the
|
|
514
|
+
* render-kernel contract.
|
|
515
|
+
*/
|
|
516
|
+
function buildFootnoteRegions(
|
|
517
|
+
page: PublicPageNode,
|
|
518
|
+
pageTopPx: number,
|
|
519
|
+
zoom: RenderZoom,
|
|
520
|
+
facet: WordReviewEditorLayoutFacet,
|
|
521
|
+
): RenderStoryRegion[] {
|
|
522
|
+
const footnoteRegions = page.regions.footnotes;
|
|
523
|
+
if (!footnoteRegions || footnoteRegions.length === 0) return [];
|
|
524
|
+
|
|
525
|
+
const regionBlocks = facet.getStoryBlocksForRegion(
|
|
526
|
+
page.pageIndex,
|
|
527
|
+
"footnote-area",
|
|
528
|
+
);
|
|
529
|
+
if (regionBlocks.length === 0) return [];
|
|
530
|
+
|
|
531
|
+
const results: RenderStoryRegion[] = [];
|
|
532
|
+
// Today the runtime emits a single footnote-area region per page; if that
|
|
533
|
+
// ever changes we will split `regionBlocks` across each region entry's
|
|
534
|
+
// `fragmentCount`. Preserve the shape so consumers can iterate safely.
|
|
535
|
+
let cursor = 0;
|
|
536
|
+
for (const region of footnoteRegions) {
|
|
537
|
+
const frame: RenderFrameRect = {
|
|
538
|
+
leftPx: page.layout.marginLeft * zoom.pxPerTwip,
|
|
539
|
+
topPx: pageTopPx + region.originTwips * zoom.pxPerTwip,
|
|
540
|
+
widthPx: region.widthTwips * zoom.pxPerTwip,
|
|
541
|
+
heightPx: region.heightTwips * zoom.pxPerTwip,
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const blocksForThisRegion =
|
|
545
|
+
footnoteRegions.length === 1
|
|
546
|
+
? regionBlocks
|
|
547
|
+
: regionBlocks.slice(cursor, cursor + region.fragmentCount);
|
|
548
|
+
cursor += region.fragmentCount;
|
|
549
|
+
|
|
550
|
+
const blocks = projectRegionBlocks(
|
|
551
|
+
blocksForThisRegion,
|
|
552
|
+
frame,
|
|
553
|
+
zoom.pxPerTwip,
|
|
554
|
+
page.pageId,
|
|
555
|
+
page.pageIndex,
|
|
556
|
+
"footnote-area",
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
// Footnote regions don't have a single canonical storyTarget (each
|
|
560
|
+
// block belongs to a different footnote body). Pick the first block's
|
|
561
|
+
// note as the region's primary story when available so chrome surfaces
|
|
562
|
+
// keyed on `storyTarget.kind === "footnote"` can dispatch on the band.
|
|
563
|
+
const firstNote = page.noteAllocations.find(
|
|
564
|
+
(alloc) => alloc.noteKind === "footnote",
|
|
565
|
+
);
|
|
566
|
+
const storyTarget: EditorStoryTarget = firstNote
|
|
567
|
+
? { kind: "footnote", noteId: firstNote.noteId }
|
|
568
|
+
: MAIN_STORY_TARGET;
|
|
569
|
+
|
|
570
|
+
results.push({
|
|
571
|
+
storyTarget,
|
|
572
|
+
region,
|
|
573
|
+
frame,
|
|
574
|
+
blocks,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return results;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* P8.3 — Stack a `PublicRegionBlock[]` into `RenderBlock[]` inside the
|
|
583
|
+
* given region frame. The cursor starts at `regionFrame.topPx` and
|
|
584
|
+
* advances by each block's `heightTwips × pxPerTwip`. Synthesizes a
|
|
585
|
+
* `PublicBlockFragment` shape for each block so chrome surfaces reading
|
|
586
|
+
* `RenderBlock.fragment` see a consistent shape across body / header /
|
|
587
|
+
* footer / footnote-area regions.
|
|
588
|
+
*/
|
|
589
|
+
function projectRegionBlocks(
|
|
590
|
+
regionBlocks: readonly PublicRegionBlock[],
|
|
591
|
+
regionFrame: RenderFrameRect,
|
|
592
|
+
pxPerTwip: number,
|
|
593
|
+
pageId: string,
|
|
594
|
+
pageIndex: number,
|
|
595
|
+
regionKind: PublicPageRegion["kind"],
|
|
596
|
+
): RenderBlock[] {
|
|
597
|
+
const blocks: RenderBlock[] = [];
|
|
598
|
+
let y = regionFrame.topPx;
|
|
599
|
+
for (let i = 0; i < regionBlocks.length; i += 1) {
|
|
600
|
+
const regionBlock = regionBlocks[i]!;
|
|
601
|
+
const blockHeightPx = Math.max(0, regionBlock.heightTwips) * pxPerTwip;
|
|
602
|
+
const blockFrame: RenderFrameRect = {
|
|
603
|
+
leftPx: regionFrame.leftPx,
|
|
604
|
+
topPx: y,
|
|
605
|
+
widthPx: regionFrame.widthPx,
|
|
606
|
+
heightPx: blockHeightPx,
|
|
607
|
+
};
|
|
608
|
+
const fragment: PublicBlockFragment = {
|
|
609
|
+
fragmentId: regionBlock.fragmentId,
|
|
610
|
+
blockId: regionBlock.blockId,
|
|
611
|
+
pageId,
|
|
612
|
+
pageIndex,
|
|
613
|
+
regionKind,
|
|
614
|
+
from: regionBlock.runtimeFromOffset,
|
|
615
|
+
to: regionBlock.runtimeToOffset,
|
|
616
|
+
heightTwips: regionBlock.heightTwips,
|
|
617
|
+
orderInRegion: i,
|
|
618
|
+
};
|
|
619
|
+
blocks.push({
|
|
620
|
+
fragment,
|
|
621
|
+
frame: blockFrame,
|
|
622
|
+
kind: classifyBlockKindFromId(regionBlock.blockId),
|
|
623
|
+
lines: [],
|
|
624
|
+
blockDecorations: [],
|
|
625
|
+
});
|
|
626
|
+
y += blockHeightPx;
|
|
627
|
+
}
|
|
628
|
+
return blocks;
|
|
629
|
+
}
|
|
630
|
+
|
|
488
631
|
// classifyBlockKind moved to `./block-fragment-projection.ts` (P4).
|
|
489
632
|
|
|
490
633
|
function buildAnchorIndex(
|
|
@@ -107,7 +107,7 @@ export function createEditorSurfaceSnapshot(
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
const secondaryStories = createSecondaryStorySurfaces(document);
|
|
110
|
+
const secondaryStories = createSecondaryStorySurfaces(document, numberingPrefixResolver);
|
|
111
111
|
|
|
112
112
|
return {
|
|
113
113
|
storySize: cursor,
|
|
@@ -886,9 +886,13 @@ function appendInlineSegments(
|
|
|
886
886
|
return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
|
|
887
887
|
}
|
|
888
888
|
case "chart_preview":
|
|
889
|
-
return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node)
|
|
889
|
+
return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node), {
|
|
890
|
+
previewMediaId: node.previewMediaId,
|
|
891
|
+
});
|
|
890
892
|
case "smartart_preview":
|
|
891
|
-
return appendComplexPreviewSegment(paragraph, node, start, "SmartArt diagram", createSmartArtDetail(node)
|
|
893
|
+
return appendComplexPreviewSegment(paragraph, node, start, "SmartArt diagram", createSmartArtDetail(node), {
|
|
894
|
+
previewMediaId: node.previewMediaId,
|
|
895
|
+
});
|
|
892
896
|
case "shape":
|
|
893
897
|
if (promoteSecondaryStoryTextBoxes && node.isTextBox && node.text) {
|
|
894
898
|
return appendTextBoxSegment(
|
|
@@ -1023,6 +1027,7 @@ function appendComplexPreviewSegment(
|
|
|
1023
1027
|
start: number,
|
|
1024
1028
|
label: string,
|
|
1025
1029
|
detail: string,
|
|
1030
|
+
extras: { previewMediaId?: string } = {},
|
|
1026
1031
|
): { nextCursor: number; lockedFragmentIds: string[] } {
|
|
1027
1032
|
paragraph.segments.push({
|
|
1028
1033
|
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
@@ -1033,6 +1038,7 @@ function appendComplexPreviewSegment(
|
|
|
1033
1038
|
warningId: `warning:complex-preview:${start}`,
|
|
1034
1039
|
label,
|
|
1035
1040
|
detail,
|
|
1041
|
+
...(extras.previewMediaId ? { previewMediaId: extras.previewMediaId } : {}),
|
|
1036
1042
|
state: "locked-preserve-only",
|
|
1037
1043
|
});
|
|
1038
1044
|
return { nextCursor: start + 1, lockedFragmentIds: [] };
|
|
@@ -1174,6 +1180,7 @@ function createPlainText(
|
|
|
1174
1180
|
|
|
1175
1181
|
function createSecondaryStorySurfaces(
|
|
1176
1182
|
document: CanonicalDocumentEnvelope,
|
|
1183
|
+
numberingPrefixResolver: NumberingPrefixResolver,
|
|
1177
1184
|
): SecondaryStorySurface[] {
|
|
1178
1185
|
const surfaces: SecondaryStorySurface[] = [];
|
|
1179
1186
|
const subParts = document.subParts;
|
|
@@ -1181,8 +1188,6 @@ function createSecondaryStorySurfaces(
|
|
|
1181
1188
|
return surfaces;
|
|
1182
1189
|
}
|
|
1183
1190
|
|
|
1184
|
-
const numberingPrefixResolver = createNumberingPrefixResolver(document.numbering);
|
|
1185
|
-
|
|
1186
1191
|
for (const section of collectSectionContexts(document)) {
|
|
1187
1192
|
const headerVariants = resolveSectionVariants(
|
|
1188
1193
|
"header",
|
|
@@ -22,28 +22,32 @@ import type {
|
|
|
22
22
|
WorkflowCommentMarkup,
|
|
23
23
|
} from "../api/public-types";
|
|
24
24
|
import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
projectSurfaceText,
|
|
27
|
+
searchProjectedSurfaceText,
|
|
28
|
+
} from "../core/search/search-text.ts";
|
|
26
29
|
import { describeOpaqueFragment, isBlockedImportFeatureKey } from "../preservation/store.ts";
|
|
27
30
|
import type { CanonicalDocumentEnvelope } from "../core/state/editor-state.ts";
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Surface-derived markup (highlights + block-level opaque fragments).
|
|
34
|
+
*
|
|
35
|
+
* Pure function of `(surface, preservation)` — extracted from
|
|
36
|
+
* `collectWorkflowMarkupSnapshot` so callers can cache the expensive walk
|
|
37
|
+
* separately from the cheap reference-equal inputs (metadata, comments,
|
|
38
|
+
* revisions, protected ranges).
|
|
39
|
+
*/
|
|
40
|
+
export function collectWorkflowSurfaceMarkup(
|
|
41
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
42
|
+
preservation: CanonicalDocumentEnvelope["preservation"],
|
|
43
|
+
): { highlights: WorkflowHighlightMarkup[]; opaqueFragments: WorkflowOpaqueFragmentMarkup[] } {
|
|
36
44
|
const highlights: WorkflowHighlightMarkup[] = [];
|
|
37
|
-
const metadata = collectWorkflowMetadataMarkup(input.workflowMetadataSnapshot);
|
|
38
|
-
const fields: WorkflowFieldMarkup[] = [];
|
|
39
45
|
const opaqueFragments: WorkflowOpaqueFragmentMarkup[] = [];
|
|
40
|
-
const surface = input.renderSnapshot.surface;
|
|
41
|
-
|
|
42
46
|
if (surface) {
|
|
43
47
|
collectSurfaceMarkup(
|
|
44
48
|
surface.blocks,
|
|
45
49
|
MAIN_STORY_TARGET,
|
|
46
|
-
|
|
50
|
+
preservation,
|
|
47
51
|
highlights,
|
|
48
52
|
opaqueFragments,
|
|
49
53
|
);
|
|
@@ -51,16 +55,55 @@ export function collectWorkflowMarkupSnapshot(input: {
|
|
|
51
55
|
collectSurfaceMarkup(
|
|
52
56
|
story.blocks,
|
|
53
57
|
story.target,
|
|
54
|
-
|
|
58
|
+
preservation,
|
|
55
59
|
highlights,
|
|
56
60
|
opaqueFragments,
|
|
57
61
|
);
|
|
58
62
|
}
|
|
63
|
+
}
|
|
64
|
+
return { highlights, opaqueFragments };
|
|
65
|
+
}
|
|
59
66
|
|
|
67
|
+
export function collectWorkflowMarkupSnapshot(input: {
|
|
68
|
+
renderSnapshot: RuntimeRenderSnapshot;
|
|
69
|
+
fieldSnapshot: FieldSnapshot;
|
|
70
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
71
|
+
preservation: CanonicalDocumentEnvelope["preservation"];
|
|
72
|
+
workflowMetadataSnapshot?: WorkflowMetadataSnapshot;
|
|
73
|
+
surfaceMarkupCache?: {
|
|
74
|
+
highlights: WorkflowHighlightMarkup[];
|
|
75
|
+
opaqueFragments: WorkflowOpaqueFragmentMarkup[];
|
|
76
|
+
};
|
|
77
|
+
perfStage?: (name: string, durationMs: number) => void;
|
|
78
|
+
}): WorkflowMarkupSnapshot {
|
|
79
|
+
const perf = input.perfStage;
|
|
80
|
+
const stageStart = perf ? () => performance.now() : () => 0;
|
|
81
|
+
const stageEnd = perf ? (name: string, t0: number) => perf(name, performance.now() - t0) : () => {};
|
|
82
|
+
|
|
83
|
+
const tMeta = stageStart();
|
|
84
|
+
const metadata = collectWorkflowMetadataMarkup(input.workflowMetadataSnapshot);
|
|
85
|
+
stageEnd("metadata", tMeta);
|
|
86
|
+
|
|
87
|
+
const fields: WorkflowFieldMarkup[] = [];
|
|
88
|
+
const surface = input.renderSnapshot.surface;
|
|
89
|
+
|
|
90
|
+
const tSurface = stageStart();
|
|
91
|
+
const surfaceMarkup = input.surfaceMarkupCache ??
|
|
92
|
+
collectWorkflowSurfaceMarkup(surface, input.preservation);
|
|
93
|
+
const highlights = surfaceMarkup.highlights.slice();
|
|
94
|
+
const opaqueFragments = surfaceMarkup.opaqueFragments.slice();
|
|
95
|
+
stageEnd("surface", tSurface);
|
|
96
|
+
|
|
97
|
+
if (surface) {
|
|
98
|
+
const tFields = stageStart();
|
|
60
99
|
fields.push(...collectFieldMarkup(surface, input.fieldSnapshot));
|
|
100
|
+
stageEnd("fields", tFields);
|
|
61
101
|
}
|
|
102
|
+
const tOpaqueRest = stageStart();
|
|
62
103
|
opaqueFragments.push(...collectOpaqueFragmentMarkup(input.preservation, opaqueFragments));
|
|
104
|
+
stageEnd("opaqueRest", tOpaqueRest);
|
|
63
105
|
|
|
106
|
+
const tCommentsEtc = stageStart();
|
|
64
107
|
const comments = input.renderSnapshot.comments.threads.map((thread): WorkflowCommentMarkup => ({
|
|
65
108
|
markupId: `comment:${thread.commentId}`,
|
|
66
109
|
kind: "comment",
|
|
@@ -113,6 +156,8 @@ export function collectWorkflowMarkupSnapshot(input: {
|
|
|
113
156
|
}),
|
|
114
157
|
);
|
|
115
158
|
|
|
159
|
+
stageEnd("commentsEtc", tCommentsEtc);
|
|
160
|
+
|
|
116
161
|
const items: WorkflowMarkupItem[] = [
|
|
117
162
|
...highlights,
|
|
118
163
|
...metadata,
|
|
@@ -303,11 +348,21 @@ function collectFieldMarkup(
|
|
|
303
348
|
return [];
|
|
304
349
|
}
|
|
305
350
|
|
|
351
|
+
// L7 Phase 1.5: project each story's text once up-front. The prior code
|
|
352
|
+
// called searchSurfaceBlocks per field, which re-projected the entire
|
|
353
|
+
// surface on every invocation. For the CCEP large-tables fixture this
|
|
354
|
+
// was ~220 ms per commit. Hoisting the projection out of the per-field
|
|
355
|
+
// loop collapses that to a single projection per story.
|
|
306
356
|
const stories = [
|
|
307
|
-
{
|
|
357
|
+
{
|
|
358
|
+
blocks: surface.blocks,
|
|
359
|
+
storyTarget: MAIN_STORY_TARGET,
|
|
360
|
+
projection: projectSurfaceText(surface.blocks),
|
|
361
|
+
},
|
|
308
362
|
...surface.secondaryStories.map((story) => ({
|
|
309
363
|
blocks: story.blocks,
|
|
310
364
|
storyTarget: story.target,
|
|
365
|
+
projection: projectSurfaceText(story.blocks),
|
|
311
366
|
})),
|
|
312
367
|
];
|
|
313
368
|
|
|
@@ -318,7 +373,7 @@ function collectFieldMarkup(
|
|
|
318
373
|
}
|
|
319
374
|
|
|
320
375
|
for (const story of stories) {
|
|
321
|
-
const matches =
|
|
376
|
+
const matches = searchProjectedSurfaceText(story.projection, displayText, { limit: 2 });
|
|
322
377
|
if (matches.length === 1) {
|
|
323
378
|
const match = matches[0]!;
|
|
324
379
|
return [
|
|
@@ -938,6 +938,22 @@ export function __createWordReviewEditorRefBridge(
|
|
|
938
938
|
pendingConflicts,
|
|
939
939
|
});
|
|
940
940
|
},
|
|
941
|
+
// Schema 1.2 — EditorStateChannel delegation.
|
|
942
|
+
configureEditorStatePolicy: (policy) => {
|
|
943
|
+
runtime.configureEditorStatePolicy(policy);
|
|
944
|
+
},
|
|
945
|
+
registerEditorStateResolver: (resolver) => {
|
|
946
|
+
runtime.registerEditorStateResolver(resolver);
|
|
947
|
+
},
|
|
948
|
+
registerEditorStatePersister: (persister) => {
|
|
949
|
+
runtime.registerEditorStatePersister(persister);
|
|
950
|
+
},
|
|
951
|
+
getEditorStateKey: (namespace) => {
|
|
952
|
+
return runtime.getEditorStateKey(namespace);
|
|
953
|
+
},
|
|
954
|
+
retryPendingPersist: async (namespace) => {
|
|
955
|
+
await runtime.retryPendingPersist(namespace);
|
|
956
|
+
},
|
|
941
957
|
setHostAnnotationOverlay: (overlay) => {
|
|
942
958
|
runtime.setHostAnnotationOverlay(clonePublicValue(overlay));
|
|
943
959
|
},
|
|
@@ -1961,6 +1977,22 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1961
1977
|
pendingConflicts: metadataConflictPendingRef.current,
|
|
1962
1978
|
});
|
|
1963
1979
|
},
|
|
1980
|
+
// Schema 1.2 — EditorStateChannel delegation.
|
|
1981
|
+
configureEditorStatePolicy: (policy) => {
|
|
1982
|
+
activeRuntime.configureEditorStatePolicy(policy);
|
|
1983
|
+
},
|
|
1984
|
+
registerEditorStateResolver: (resolver) => {
|
|
1985
|
+
activeRuntime.registerEditorStateResolver(resolver);
|
|
1986
|
+
},
|
|
1987
|
+
registerEditorStatePersister: (persister) => {
|
|
1988
|
+
activeRuntime.registerEditorStatePersister(persister);
|
|
1989
|
+
},
|
|
1990
|
+
getEditorStateKey: (namespace) => {
|
|
1991
|
+
return activeRuntime.getEditorStateKey(namespace);
|
|
1992
|
+
},
|
|
1993
|
+
retryPendingPersist: async (namespace) => {
|
|
1994
|
+
await activeRuntime.retryPendingPersist(namespace);
|
|
1995
|
+
},
|
|
1964
1996
|
setHostAnnotationOverlay: (overlay) => {
|
|
1965
1997
|
setHostAnnotationOverlayState(clonePublicValue(overlay));
|
|
1966
1998
|
},
|
|
@@ -2618,6 +2650,19 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2618
2650
|
code: "unsupported_surface",
|
|
2619
2651
|
message,
|
|
2620
2652
|
}]),
|
|
2653
|
+
onPasteApplied: (meta: {
|
|
2654
|
+
segmentCount: number;
|
|
2655
|
+
charCount: number;
|
|
2656
|
+
source: "paste" | "drop";
|
|
2657
|
+
}) => {
|
|
2658
|
+
onEventRef.current?.({
|
|
2659
|
+
type: "paste_applied",
|
|
2660
|
+
documentId: props.documentId,
|
|
2661
|
+
segmentCount: meta.segmentCount,
|
|
2662
|
+
charCount: meta.charCount,
|
|
2663
|
+
source: meta.source,
|
|
2664
|
+
});
|
|
2665
|
+
},
|
|
2621
2666
|
};
|
|
2622
2667
|
|
|
2623
2668
|
const reviewCallbacks = {
|
|
@@ -2763,14 +2808,21 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2763
2808
|
applyRuntimeImageResize(activeRuntime, mediaId, dimensions),
|
|
2764
2809
|
onSetImageFrame: (mediaId, offsets) =>
|
|
2765
2810
|
applyRuntimeImageReposition(activeRuntime, mediaId, offsets),
|
|
2766
|
-
onOpenHeaderStory
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2811
|
+
// P8.11 — `onOpenHeaderStory` / `onOpenFooterStory` retired from the
|
|
2812
|
+
// WordReviewEditor wiring. Per-page header / footer bands rendered
|
|
2813
|
+
// by `TwPageStackChromeLayer` call `onOpenStory(target)` with the
|
|
2814
|
+
// exact `EditorStoryTarget` they represent, so the variant /
|
|
2815
|
+
// relationship resolution happens inside the layout facet instead
|
|
2816
|
+
// of the UI. The deprecated props remain in the workspace type
|
|
2817
|
+
// with a mount-time `console.warn`; hosts that still pass them can
|
|
2818
|
+
// migrate to `onOpenStory` at their leisure.
|
|
2770
2819
|
onOpenHeaderStoryForPage: (pageIndex: number) =>
|
|
2771
2820
|
openStoryForPage(activeRuntime, pageIndex, "header"),
|
|
2772
2821
|
onOpenFooterStoryForPage: (pageIndex: number) =>
|
|
2773
2822
|
openStoryForPage(activeRuntime, pageIndex, "footer"),
|
|
2823
|
+
onOpenStory: (target) => {
|
|
2824
|
+
activeRuntime.openStory(target);
|
|
2825
|
+
},
|
|
2774
2826
|
onDeleteSectionBreak: (sectionIndex) =>
|
|
2775
2827
|
applyRuntimeDeleteSectionBreak(activeRuntime, sectionIndex),
|
|
2776
2828
|
onUpdateSectionLayout: (sectionIndex, patch) =>
|
|
@@ -2901,6 +2953,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2901
2953
|
interactionGuardSnapshot={interactionGuardSnapshot}
|
|
2902
2954
|
chromePreset={effectiveChromePreset}
|
|
2903
2955
|
chromeOptions={chromeOptions}
|
|
2956
|
+
{...(props.collabSession ? { collabSession: props.collabSession } : {})}
|
|
2957
|
+
{...(props.collabTransportStatus
|
|
2958
|
+
? { collabTransportStatus: props.collabTransportStatus }
|
|
2959
|
+
: {})}
|
|
2960
|
+
{...(props.activeCommentId !== undefined
|
|
2961
|
+
? { activeCommentId: props.activeCommentId }
|
|
2962
|
+
: {})}
|
|
2963
|
+
collabActorId={currentUser.userId}
|
|
2964
|
+
{...(props.collabSendBaseline
|
|
2965
|
+
? { collabSendBaseline: props.collabSendBaseline }
|
|
2966
|
+
: {})}
|
|
2904
2967
|
reviewQueue={reviewQueueSnapshot}
|
|
2905
2968
|
documentContextAnalytics={documentContextAnalytics}
|
|
2906
2969
|
selectionContextAnalytics={selectionContextAnalytics}
|
|
@@ -4866,47 +4929,6 @@ function openStoryForPage(
|
|
|
4866
4929
|
runtime.openStory(target);
|
|
4867
4930
|
}
|
|
4868
4931
|
|
|
4869
|
-
function openDefaultStoryVariant(
|
|
4870
|
-
runtime: WordReviewEditorRuntime,
|
|
4871
|
-
pageLayout: PageLayoutSnapshot | undefined,
|
|
4872
|
-
navigation: ReturnType<WordReviewEditorRuntime["getDocumentNavigationSnapshot"]> | undefined,
|
|
4873
|
-
kind: "header" | "footer",
|
|
4874
|
-
): void {
|
|
4875
|
-
const variants =
|
|
4876
|
-
kind === "header"
|
|
4877
|
-
? pageLayout?.headerVariants
|
|
4878
|
-
: pageLayout?.footerVariants;
|
|
4879
|
-
const activePage = navigation?.pages[navigation.activePageIndex];
|
|
4880
|
-
const isFirstPageInSection =
|
|
4881
|
-
activePage !== undefined &&
|
|
4882
|
-
activePage.sectionIndex === pageLayout?.sectionIndex &&
|
|
4883
|
-
activePage.pageInSection === 0;
|
|
4884
|
-
const isEvenDocumentPage = activePage !== undefined && (activePage.pageIndex + 1) % 2 === 0;
|
|
4885
|
-
|
|
4886
|
-
let variant =
|
|
4887
|
-
pageLayout?.differentFirstPage && isFirstPageInSection
|
|
4888
|
-
? variants?.find((entry) => entry.variant === "first")
|
|
4889
|
-
: undefined;
|
|
4890
|
-
|
|
4891
|
-
if (!variant && pageLayout?.differentOddEvenPages && isEvenDocumentPage) {
|
|
4892
|
-
variant = variants?.find((entry) => entry.variant === "even");
|
|
4893
|
-
}
|
|
4894
|
-
|
|
4895
|
-
if (!variant) {
|
|
4896
|
-
variant = variants?.find((entry) => entry.variant === "default") ?? variants?.[0];
|
|
4897
|
-
}
|
|
4898
|
-
|
|
4899
|
-
if (!variant) {
|
|
4900
|
-
return;
|
|
4901
|
-
}
|
|
4902
|
-
runtime.openStory({
|
|
4903
|
-
kind,
|
|
4904
|
-
relationshipId: variant.relationshipId,
|
|
4905
|
-
variant: variant.variant,
|
|
4906
|
-
sectionIndex: pageLayout?.sectionIndex,
|
|
4907
|
-
});
|
|
4908
|
-
}
|
|
4909
|
-
|
|
4910
4932
|
function searchRuntimeDocument(
|
|
4911
4933
|
runtime: WordReviewEditorRuntime,
|
|
4912
4934
|
mountedSurface: TwProseMirrorSurfaceRef | null,
|
|
@@ -2,6 +2,7 @@ import { useMemo, useRef } from "react";
|
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
CommentSidebarThreadSnapshot,
|
|
5
|
+
EditorStoryTarget,
|
|
5
6
|
FormattingAlignment,
|
|
6
7
|
HeaderFooterLinkPatch,
|
|
7
8
|
InsertImageOptions,
|
|
@@ -87,12 +88,25 @@ export interface EditorCommandBag {
|
|
|
87
88
|
onAcceptAllChanges(): void;
|
|
88
89
|
onRejectAllChanges(): void;
|
|
89
90
|
onCloseStory?(): void;
|
|
91
|
+
/**
|
|
92
|
+
* @deprecated P8.11 — see the matching prop on `TwReviewWorkspaceProps`.
|
|
93
|
+
* Kept optional for back-compat; per-page bands use `onOpenStory`.
|
|
94
|
+
*/
|
|
90
95
|
onOpenHeaderStory?(): void;
|
|
96
|
+
/**
|
|
97
|
+
* @deprecated P8.11 — see `onOpenHeaderStory`.
|
|
98
|
+
*/
|
|
91
99
|
onOpenFooterStory?(): void;
|
|
92
100
|
/** Open the header story for a specific page (double-click on its band). */
|
|
93
101
|
onOpenHeaderStoryForPage?(pageIndex: number): void;
|
|
94
102
|
/** Open the footer story for a specific page (double-click on its band). */
|
|
95
103
|
onOpenFooterStoryForPage?(pageIndex: number): void;
|
|
104
|
+
/**
|
|
105
|
+
* P8.11 — per-page header/footer band click handler. Receives the
|
|
106
|
+
* exact `EditorStoryTarget` the band represents; the command bag wires
|
|
107
|
+
* this to `runtime.openStory(target)`.
|
|
108
|
+
*/
|
|
109
|
+
onOpenStory?(target: EditorStoryTarget): void;
|
|
96
110
|
onSetParagraphIndentation?(indentation: {
|
|
97
111
|
left?: number;
|
|
98
112
|
right?: number;
|