@beyondwork/docx-react-component 1.0.96 → 1.0.98
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/api/public-types.ts +47 -19
- package/src/api/v3/ui/_types.ts +11 -21
- package/src/api/v3/ui/chrome.ts +8 -9
- package/src/api/v3/ui/debug.ts +15 -77
- package/src/api/v3/ui/overlays-visibility.ts +9 -10
- package/src/api/v3/ui/overlays.ts +8 -75
- package/src/io/ooxml/parse-main-document.ts +30 -0
- package/src/io/ooxml/parse-picture.ts +14 -0
- package/src/io/ooxml/parse-shapes.ts +41 -1
- package/src/io/ooxml/payload-signature.ts +101 -0
- package/src/model/canonical-document.ts +17 -0
- package/src/runtime/layout/layout-engine-version.ts +14 -1
- package/src/runtime/layout/page-story-resolver.ts +1 -0
- package/src/runtime/layout/paginated-layout-engine.ts +26 -10
- package/src/runtime/surface-projection.ts +114 -12
- package/src/session/export/stateful-export-pipeline.ts +18 -2
- package/src/session/export/stateful-export.ts +21 -1
- package/src/ui/WordReviewEditor.tsx +6 -10
- package/src/ui/editor-command-bag.ts +2 -0
- package/src/ui/ui-controller-factory.ts +2 -2
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +11 -25
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +2 -2
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +22 -220
- package/src/ui-tailwind/debug/README.md +12 -50
- package/src/ui-tailwind/debug/tw-debug-overlay.tsx +6 -6
- package/src/ui-tailwind/debug/tw-debug-presentation.tsx +9 -20
- package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +5 -6
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +1 -4
- package/src/ui-tailwind/editor-surface/picture-effects.ts +96 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +89 -62
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +205 -0
- package/src/ui-tailwind/editor-surface/preserve-position.ts +12 -2
- package/src/ui-tailwind/editor-surface/runtime-decoration-plugin.ts +190 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +67 -56
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +83 -20
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +114 -4
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +5 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +3 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +3 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +26 -0
- package/src/ui-tailwind/theme/editor-theme.css +18 -11
- package/src/ui-tailwind/tw-review-workspace.tsx +15 -0
|
@@ -199,6 +199,14 @@ export interface ReplaceStateOptions extends PreservePositionOptions {
|
|
|
199
199
|
* Tests override it to capture the release callback.
|
|
200
200
|
*/
|
|
201
201
|
scheduleMicrotask?: (callback: () => void) => void;
|
|
202
|
+
/**
|
|
203
|
+
* Synchronous work that must happen after `view.updateState(newState)`
|
|
204
|
+
* but before the optional scroll restore. The ProseMirror surface uses
|
|
205
|
+
* this to dispatch runtime decoration inputs into the PM plugin while
|
|
206
|
+
* the same captured scroll anchor still protects the whole replacement
|
|
207
|
+
* cycle.
|
|
208
|
+
*/
|
|
209
|
+
afterUpdateState?: () => void;
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
/**
|
|
@@ -212,8 +220,9 @@ export interface ReplaceStateOptions extends PreservePositionOptions {
|
|
|
212
220
|
* `preserveScrollAnchor: true` and a live `geometryFacet`
|
|
213
221
|
* 2. suppressionRef.current = true
|
|
214
222
|
* 3. view.updateState(newState) ← PM may fire selection events here
|
|
215
|
-
* 4. (
|
|
216
|
-
* 5.
|
|
223
|
+
* 4. options.afterUpdateState?.() ← optional decoration/plugin sync
|
|
224
|
+
* 5. (optional) restore scroll anchor
|
|
225
|
+
* 6. queueMicrotask(() => suppressionRef.current = false)
|
|
217
226
|
*
|
|
218
227
|
* The microtask release guarantees the flag is still `true` for any
|
|
219
228
|
* synchronous selection-change handler that fires during (3), and
|
|
@@ -242,6 +251,7 @@ export function replaceStatePreservingPosition(
|
|
|
242
251
|
: null;
|
|
243
252
|
options.suppressionRef.current = true;
|
|
244
253
|
options.view.updateState(newState);
|
|
254
|
+
options.afterUpdateState?.();
|
|
245
255
|
if (preserved) {
|
|
246
256
|
const restored = restorePosition(preserved, options);
|
|
247
257
|
if (!restored) {
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Refactor/11b Slice B — runtime decoration plugin (Option B fast-path).
|
|
2
|
+
//
|
|
3
|
+
// Before this slice, `tw-prosemirror-surface.tsx` rebuilt the entire
|
|
4
|
+
// `DecorationSet` from scratch on every commit via a React effect:
|
|
5
|
+
//
|
|
6
|
+
// view.setProps({ decorations: () => buildDecorations(...) });
|
|
7
|
+
//
|
|
8
|
+
// `buildDecorations` walks the whole PM doc + every comment thread + every
|
|
9
|
+
// revision + every workflow scope + every candidate + every locked zone.
|
|
10
|
+
// On a 500-page doc toggling "Show tracked changes" this was ~200–300 ms
|
|
11
|
+
// (documented residual in `docs/wiki/performance.md §Context-Analytics Hot
|
|
12
|
+
// Path`). On every keystroke it rebuilt the full set even when only the
|
|
13
|
+
// caret + a few characters moved.
|
|
14
|
+
//
|
|
15
|
+
// This plugin shifts the strategy to the idiomatic PM pattern:
|
|
16
|
+
//
|
|
17
|
+
// 1. Plugin state holds `{ inputs, set }` — the currently-authoritative
|
|
18
|
+
// decoration inputs and the computed DecorationSet.
|
|
19
|
+
// 2. On a transaction with `tr.setMeta(pluginKey, { inputs })`: inputs
|
|
20
|
+
// changed, rebuild the set once.
|
|
21
|
+
// 3. On a transaction without that meta: inputs are stable. Map the
|
|
22
|
+
// existing set through `tr.mapping` (O(decorations), not O(doc)).
|
|
23
|
+
//
|
|
24
|
+
// Runtime-origin doc replacements — where PM state is swapped wholesale
|
|
25
|
+
// via `view.updateState(state)` because the runtime produced a new
|
|
26
|
+
// snapshot — can't be handled by `tr.mapping`, so the consumer dispatches
|
|
27
|
+
// a meta to force a rebuild on those paths. Pure typing transactions
|
|
28
|
+
// (the `onInsertText` fast-text-edit-lane commit that `view.dispatch`es a
|
|
29
|
+
// local step) go through the no-meta branch and pay O(decorations) only.
|
|
30
|
+
//
|
|
31
|
+
// Scope of this slice: wraps the existing `buildDecorations` +
|
|
32
|
+
// `buildPageBreakDecorationsFromProps` unchanged. No restructuring of
|
|
33
|
+
// the 14 decoration sources inside `buildDecorations` — that's a
|
|
34
|
+
// follow-up that would split per-source subsets to let single-source
|
|
35
|
+
// updates skip untouched subsets.
|
|
36
|
+
|
|
37
|
+
import { type EditorState, type Transaction, Plugin, PluginKey } from "prosemirror-state";
|
|
38
|
+
import { type Decoration, type EditorView, DecorationSet } from "prosemirror-view";
|
|
39
|
+
import type { Node as PMNode } from "prosemirror-model";
|
|
40
|
+
|
|
41
|
+
import type {
|
|
42
|
+
EditorStoryTarget,
|
|
43
|
+
WorkflowBlockedCommandReason,
|
|
44
|
+
WorkflowCandidateRange,
|
|
45
|
+
WorkflowLockedZone,
|
|
46
|
+
WorkflowMetadataMarkup,
|
|
47
|
+
WorkflowScope,
|
|
48
|
+
} from "../../api/public-types";
|
|
49
|
+
import type { CommentDecorationModel, MarkupDisplay } from "../../ui/headless/comment-decoration-model";
|
|
50
|
+
import type {
|
|
51
|
+
RevisionDecorationModel,
|
|
52
|
+
RevisionDisplayFlags,
|
|
53
|
+
} from "../../ui/headless/revision-decoration-model";
|
|
54
|
+
import { buildDecorations } from "./pm-decorations";
|
|
55
|
+
import type { PositionMap } from "./pm-position-map";
|
|
56
|
+
|
|
57
|
+
/** Inputs that drive every call to `buildDecorations`, plus an opaque
|
|
58
|
+
* callback that supplies any additional decorations layered on top
|
|
59
|
+
* (today: page-break chrome widgets). Keeping page-break inputs inside
|
|
60
|
+
* the callback keeps this module decoupled from its current dependency
|
|
61
|
+
* graph — the caller captures whatever it needs in the closure. */
|
|
62
|
+
export interface RuntimeDecorationInputs {
|
|
63
|
+
positionMap: PositionMap;
|
|
64
|
+
commentModel: CommentDecorationModel | undefined;
|
|
65
|
+
revisionModel: RevisionDecorationModel | undefined;
|
|
66
|
+
markupDisplay: MarkupDisplay;
|
|
67
|
+
showTrackedChanges: boolean;
|
|
68
|
+
suggestionsEnabled: boolean;
|
|
69
|
+
workflowScopes: readonly WorkflowScope[] | undefined;
|
|
70
|
+
activeStory: EditorStoryTarget;
|
|
71
|
+
workflowCandidates: readonly WorkflowCandidateRange[] | undefined;
|
|
72
|
+
workflowBlockedReasons: readonly WorkflowBlockedCommandReason[] | undefined;
|
|
73
|
+
workflowLockedZones: readonly WorkflowLockedZone[] | undefined;
|
|
74
|
+
activeWorkflowWorkItemId: string | null | undefined;
|
|
75
|
+
activeWorkflowScopeIds: readonly string[] | undefined;
|
|
76
|
+
workflowMetadata: readonly WorkflowMetadataMarkup[] | undefined;
|
|
77
|
+
revisionDisplayByOffset: ReadonlyMap<number, RevisionDisplayFlags> | undefined;
|
|
78
|
+
/** Extra decorations layered on top of the base `buildDecorations`
|
|
79
|
+
* result. Today the page-break chrome widgets flow through here —
|
|
80
|
+
* their inputs + helper live in `tw-prosemirror-surface.tsx` so the
|
|
81
|
+
* consumer captures them in the closure and returns built
|
|
82
|
+
* decorations. `null`/empty returns are fine. */
|
|
83
|
+
extraDecorations?: (doc: PMNode) => readonly Decoration[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface DecorationState {
|
|
87
|
+
inputs: RuntimeDecorationInputs | null;
|
|
88
|
+
set: DecorationSet;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Key for the plugin. Consumers dispatch via
|
|
92
|
+
* `view.state.tr.setMeta(runtimeDecorationPluginKey, { inputs })`. */
|
|
93
|
+
export const runtimeDecorationPluginKey = new PluginKey<DecorationState>(
|
|
94
|
+
"wre-runtime-decorations",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/** Module-level counters — mirror the style used by the L1 identity cache.
|
|
98
|
+
* Read via `getRuntimeDecorationCounters()`. Useful for perf benches +
|
|
99
|
+
* tests that assert the fast-path actually runs. */
|
|
100
|
+
let fullRebuilds = 0;
|
|
101
|
+
let mappedTransactions = 0;
|
|
102
|
+
let noOpTransactions = 0;
|
|
103
|
+
|
|
104
|
+
export function getRuntimeDecorationCounters(): {
|
|
105
|
+
fullRebuilds: number;
|
|
106
|
+
mappedTransactions: number;
|
|
107
|
+
noOpTransactions: number;
|
|
108
|
+
} {
|
|
109
|
+
return { fullRebuilds, mappedTransactions, noOpTransactions };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function __resetRuntimeDecorationCountersForTests(): void {
|
|
113
|
+
fullRebuilds = 0;
|
|
114
|
+
mappedTransactions = 0;
|
|
115
|
+
noOpTransactions = 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function rebuildSet(doc: PMNode, inputs: RuntimeDecorationInputs): DecorationSet {
|
|
119
|
+
const base = buildDecorations(
|
|
120
|
+
doc,
|
|
121
|
+
inputs.positionMap,
|
|
122
|
+
inputs.commentModel,
|
|
123
|
+
inputs.revisionModel,
|
|
124
|
+
inputs.markupDisplay,
|
|
125
|
+
inputs.showTrackedChanges,
|
|
126
|
+
inputs.suggestionsEnabled,
|
|
127
|
+
inputs.workflowScopes,
|
|
128
|
+
inputs.activeStory,
|
|
129
|
+
inputs.workflowCandidates,
|
|
130
|
+
inputs.workflowBlockedReasons,
|
|
131
|
+
inputs.workflowLockedZones,
|
|
132
|
+
inputs.activeWorkflowWorkItemId,
|
|
133
|
+
inputs.activeWorkflowScopeIds,
|
|
134
|
+
inputs.workflowMetadata,
|
|
135
|
+
inputs.revisionDisplayByOffset,
|
|
136
|
+
);
|
|
137
|
+
const extras = inputs.extraDecorations ? inputs.extraDecorations(doc) : [];
|
|
138
|
+
if (extras.length === 0) return base;
|
|
139
|
+
// Layer extras on top of the base set. `DecorationSet.add` is the
|
|
140
|
+
// supported join that keeps the base set's structure and adds new
|
|
141
|
+
// decorations to it.
|
|
142
|
+
return base.add(doc, [...extras]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createRuntimeDecorationPlugin(): Plugin<DecorationState> {
|
|
146
|
+
return new Plugin<DecorationState>({
|
|
147
|
+
key: runtimeDecorationPluginKey,
|
|
148
|
+
state: {
|
|
149
|
+
init: () => ({ inputs: null, set: DecorationSet.empty }),
|
|
150
|
+
apply(tr, prev, _oldState, newState): DecorationState {
|
|
151
|
+
const meta = tr.getMeta(runtimeDecorationPluginKey) as
|
|
152
|
+
| { inputs: RuntimeDecorationInputs }
|
|
153
|
+
| null
|
|
154
|
+
| undefined;
|
|
155
|
+
if (meta) {
|
|
156
|
+
fullRebuilds += 1;
|
|
157
|
+
return { inputs: meta.inputs, set: rebuildSet(newState.doc, meta.inputs) };
|
|
158
|
+
}
|
|
159
|
+
if (tr.docChanged) {
|
|
160
|
+
mappedTransactions += 1;
|
|
161
|
+
return {
|
|
162
|
+
inputs: prev.inputs,
|
|
163
|
+
set: prev.set.map(tr.mapping, newState.doc),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
noOpTransactions += 1;
|
|
167
|
+
return prev;
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
props: {
|
|
171
|
+
decorations(state: EditorState): DecorationSet {
|
|
172
|
+
return runtimeDecorationPluginKey.getState(state)?.set ?? DecorationSet.empty;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Convenience for consumers — dispatches a meta transaction that
|
|
179
|
+
* triggers a full rebuild. Call whenever any `RuntimeDecorationInputs`
|
|
180
|
+
* field changes identity or when the underlying doc was replaced
|
|
181
|
+
* wholesale (runtime-origin `view.updateState(state)` path). */
|
|
182
|
+
export function applyRuntimeDecorationInputs(
|
|
183
|
+
view: EditorView,
|
|
184
|
+
inputs: RuntimeDecorationInputs,
|
|
185
|
+
): void {
|
|
186
|
+
const tr: Transaction = view.state.tr.setMeta(runtimeDecorationPluginKey, {
|
|
187
|
+
inputs,
|
|
188
|
+
});
|
|
189
|
+
view.dispatch(tr);
|
|
190
|
+
}
|
|
@@ -53,6 +53,11 @@ import { buildDecorations } from "./pm-decorations";
|
|
|
53
53
|
import { buildPageBreakDecorations } from "./pm-page-break-decorations";
|
|
54
54
|
import { findBlockIndexRangeForPage } from "./page-slice-util.ts";
|
|
55
55
|
import { DecorationSet } from "prosemirror-view";
|
|
56
|
+
import {
|
|
57
|
+
applyRuntimeDecorationInputs,
|
|
58
|
+
createRuntimeDecorationPlugin,
|
|
59
|
+
type RuntimeDecorationInputs,
|
|
60
|
+
} from "./runtime-decoration-plugin.ts";
|
|
56
61
|
import type { WordReviewEditorLayoutFacet } from "../../api/public-types.ts";
|
|
57
62
|
import { buildPagePreviewMaps } from "../../api/public-types";
|
|
58
63
|
import { createContextualInteractionPlugin } from "./pm-contextual-ui";
|
|
@@ -670,52 +675,61 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
670
675
|
onRevisionHovered: (revisionId) => props.onRevisionHovered?.(revisionId),
|
|
671
676
|
}),
|
|
672
677
|
createSearchPlugin(),
|
|
678
|
+
// Refactor/11b Slice B — runtime decorations as PM plugin state.
|
|
679
|
+
// Non-meta transactions map the existing set through `tr.mapping`
|
|
680
|
+
// in O(decorations); meta-tagged transactions rebuild from inputs.
|
|
681
|
+
// Registered LAST so its `props.decorations(state)` is the
|
|
682
|
+
// runtime-authoritative set the view renders. Search-plugin
|
|
683
|
+
// decorations compose at the view level — PM merges `decorations`
|
|
684
|
+
// returned by each plugin's `props.decorations`.
|
|
685
|
+
createRuntimeDecorationPlugin(),
|
|
673
686
|
];
|
|
674
687
|
}, [props.awareness, props.onCommentActivated, props.onRevisionActivated, props.onRevisionHovered]);
|
|
675
688
|
|
|
676
689
|
const applyDecorationProps = useCallback(
|
|
677
690
|
(view: EditorView, positionMap: PositionMap): void => {
|
|
678
|
-
|
|
679
|
-
|
|
691
|
+
// Refactor/11b Slice B — dispatch inputs into the runtime decoration
|
|
692
|
+
// plugin instead of imperatively calling `view.setProps({decorations})`
|
|
693
|
+
// on every effect fire. The plugin holds the computed DecorationSet
|
|
694
|
+
// in its state; non-meta transactions (typing, selection) map the
|
|
695
|
+
// existing set through `tr.mapping` in O(decorations) rather than
|
|
696
|
+
// re-walking the doc + every comment thread / revision / workflow
|
|
697
|
+
// scope / candidate / locked zone from scratch.
|
|
698
|
+
const inputs: RuntimeDecorationInputs = {
|
|
680
699
|
positionMap,
|
|
681
700
|
commentModel,
|
|
682
701
|
revisionModel,
|
|
683
702
|
markupDisplay,
|
|
684
703
|
showTrackedChanges,
|
|
685
704
|
suggestionsEnabled,
|
|
686
|
-
props.workflowScopes,
|
|
687
|
-
snapshot.activeStory,
|
|
688
|
-
props.workflowCandidates,
|
|
689
|
-
props.workflowBlockedReasons,
|
|
690
|
-
props.workflowLockedZones,
|
|
691
|
-
props.activeWorkflowWorkItemId,
|
|
692
|
-
props.activeWorkflowScopeIds,
|
|
693
|
-
props.workflowMetadata,
|
|
705
|
+
workflowScopes: props.workflowScopes,
|
|
706
|
+
activeStory: snapshot.activeStory,
|
|
707
|
+
workflowCandidates: props.workflowCandidates,
|
|
708
|
+
workflowBlockedReasons: props.workflowBlockedReasons,
|
|
709
|
+
workflowLockedZones: props.workflowLockedZones,
|
|
710
|
+
activeWorkflowWorkItemId: props.activeWorkflowWorkItemId ?? null,
|
|
711
|
+
activeWorkflowScopeIds: props.activeWorkflowScopeIds,
|
|
712
|
+
workflowMetadata: props.workflowMetadata,
|
|
694
713
|
revisionDisplayByOffset,
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
: baseDecorations;
|
|
715
|
-
view.setProps({
|
|
716
|
-
editable: () => canEdit,
|
|
717
|
-
decorations: () => decorations,
|
|
718
|
-
});
|
|
714
|
+
extraDecorations: (_doc) =>
|
|
715
|
+
buildPageBreakDecorationsFromProps(
|
|
716
|
+
props.geometryFacet,
|
|
717
|
+
snapshot.activeStory.kind === "main",
|
|
718
|
+
positionMap,
|
|
719
|
+
props.isPageWorkspace ? "page" : "canvas",
|
|
720
|
+
props.canonicalDocument,
|
|
721
|
+
{
|
|
722
|
+
headerBandPx: props.pageChromeHeaderBandPx,
|
|
723
|
+
footerBandPx: props.pageChromeFooterBandPx,
|
|
724
|
+
interGapPx: props.pageChromeInterGapPx,
|
|
725
|
+
},
|
|
726
|
+
snapshot.surface?.blocks,
|
|
727
|
+
),
|
|
728
|
+
};
|
|
729
|
+
applyRuntimeDecorationInputs(view, inputs);
|
|
730
|
+
// `editable` is not decoration-shaped; keep the existing setProps
|
|
731
|
+
// path for it so the plugin-state swap stays scoped to decorations.
|
|
732
|
+
view.setProps({ editable: () => canEdit });
|
|
719
733
|
decorationBuildKeyRef.current = decorationBuildKey;
|
|
720
734
|
recordPerfSample("pm.decorations");
|
|
721
735
|
incrementInvalidationCounter("pm.laneB.decorationUpdates");
|
|
@@ -914,33 +928,20 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
914
928
|
props.isPageWorkspace,
|
|
915
929
|
);
|
|
916
930
|
positionMapRef.current = positionMap;
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
markupDisplay,
|
|
923
|
-
showTrackedChanges,
|
|
924
|
-
suggestionsEnabled,
|
|
925
|
-
props.workflowScopes,
|
|
926
|
-
snapshot.activeStory,
|
|
927
|
-
props.workflowCandidates,
|
|
928
|
-
props.workflowBlockedReasons,
|
|
929
|
-
props.workflowLockedZones,
|
|
930
|
-
props.activeWorkflowWorkItemId,
|
|
931
|
-
props.activeWorkflowScopeIds,
|
|
932
|
-
props.workflowMetadata,
|
|
933
|
-
revisionDisplayByOffset,
|
|
934
|
-
);
|
|
931
|
+
// Slice B — the runtime decoration plugin (registered in the plugin
|
|
932
|
+
// list) now owns the decoration set. The fresh-mount path dispatches
|
|
933
|
+
// inputs into the plugin via `applyDecorationProps` below; PM reads
|
|
934
|
+
// the set via the plugin's `props.decorations(state)` hook. No
|
|
935
|
+
// pre-mount `buildDecorations` call is needed.
|
|
935
936
|
recordPerfSample("pm.rebuild");
|
|
936
937
|
incrementInvalidationCounter("pm.laneA.rebuilds");
|
|
937
938
|
|
|
939
|
+
let appliedDecorationProps = false;
|
|
938
940
|
if (!viewRef.current) {
|
|
939
941
|
const view = new EditorView(mountRef.current, {
|
|
940
942
|
state,
|
|
941
943
|
nodeViews: { ...tableNodeViews, ...chartNodeViews },
|
|
942
944
|
editable: () => canEdit,
|
|
943
|
-
decorations: () => decorations,
|
|
944
945
|
});
|
|
945
946
|
viewRef.current = view;
|
|
946
947
|
recordPerfSample("pm.mount");
|
|
@@ -960,32 +961,42 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
960
961
|
// would move by more than the small local-edit budget, the helper
|
|
961
962
|
// refuses that exact target but restores the captured scrollTop so
|
|
962
963
|
// a PM/browser-origin top jump is not accepted as the final state.
|
|
964
|
+
// The runtime-decoration plugin is also updated inside the funnel:
|
|
965
|
+
// its meta transaction can rebuild page-break/widget decorations
|
|
966
|
+
// synchronously, so it must be included before the final restore.
|
|
963
967
|
//
|
|
964
968
|
// Ordering invariant is regression-guarded by
|
|
965
969
|
// `test/ui-tailwind/editor-surface/preserve-position-ordering.test.ts`.
|
|
970
|
+
const view = viewRef.current;
|
|
966
971
|
const scrollAnchorPolicy = pendingRebuildScrollAnchorRef.current;
|
|
967
972
|
pendingRebuildScrollAnchorRef.current = null;
|
|
968
973
|
const preserveScrollAnchor = shouldPreserveScrollAnchorForRebuild({
|
|
969
974
|
policy: scrollAnchorPolicy,
|
|
970
|
-
view
|
|
975
|
+
view,
|
|
971
976
|
geometryFacet: props.geometryFacet,
|
|
972
977
|
previousStory: lastBuiltStoryRef.current,
|
|
973
978
|
nextStory: snapshot.activeStory,
|
|
974
979
|
});
|
|
975
980
|
replaceStatePreservingPosition(
|
|
976
981
|
{
|
|
977
|
-
view
|
|
982
|
+
view,
|
|
978
983
|
geometryFacet: props.geometryFacet,
|
|
979
984
|
suppressionRef: suppressSelectionEchoRef,
|
|
980
985
|
preserveScrollAnchor,
|
|
981
986
|
maxScrollDeltaPx: BOUNDED_TYPING_SCROLL_RESTORE_MAX_DELTA_PX,
|
|
987
|
+
afterUpdateState: () => {
|
|
988
|
+
applyDecorationProps(view, positionMap);
|
|
989
|
+
appliedDecorationProps = true;
|
|
990
|
+
},
|
|
982
991
|
},
|
|
983
992
|
state,
|
|
984
993
|
);
|
|
985
994
|
}
|
|
986
995
|
documentBuildKeyRef.current = documentBuildKey;
|
|
987
996
|
lastBuiltStoryRef.current = snapshot.activeStory;
|
|
988
|
-
|
|
997
|
+
if (!appliedDecorationProps) {
|
|
998
|
+
applyDecorationProps(viewRef.current, positionMap);
|
|
999
|
+
}
|
|
989
1000
|
|
|
990
1001
|
if (activeSearchRef.current) {
|
|
991
1002
|
applySearch(
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
SurfaceBlockSnapshot,
|
|
6
6
|
SurfaceDrawingAnchor,
|
|
7
7
|
SurfaceInlineSegment,
|
|
8
|
+
SurfacePictureEffects,
|
|
8
9
|
} from "../../api/public-types.ts";
|
|
9
10
|
import type { WordReviewEditorLayoutFacet } from "../../api/public-types.ts";
|
|
10
11
|
import {
|
|
@@ -44,22 +45,31 @@ export interface FloatingImageOverlayItem {
|
|
|
44
45
|
leftPx: number;
|
|
45
46
|
widthPx: number;
|
|
46
47
|
heightPx: number;
|
|
48
|
+
zIndex?: number;
|
|
47
49
|
behindDoc: boolean;
|
|
48
50
|
src: string | null;
|
|
49
51
|
altText: string | null;
|
|
50
52
|
detail: string | null;
|
|
53
|
+
pictureEffects?: SurfacePictureEffects;
|
|
54
|
+
shape?: {
|
|
55
|
+
label: string;
|
|
56
|
+
text: string;
|
|
57
|
+
fill?: Extract<Extract<SurfaceInlineSegment, { kind: "shape" }>["fill"], object>;
|
|
58
|
+
line?: Extract<SurfaceInlineSegment, { kind: "shape" }>["line"];
|
|
59
|
+
body?: Extract<SurfaceInlineSegment, { kind: "shape" }>["textBoxBody"];
|
|
60
|
+
marks?: Extract<SurfaceInlineSegment, { kind: "shape" }>["txbxMarks"];
|
|
61
|
+
markAttrs?: Extract<SurfaceInlineSegment, { kind: "shape" }>["txbxMarkAttrs"];
|
|
62
|
+
};
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
const SUPPORTED_HORIZONTAL_RELATIVE_FROM = new Set(["page", "margin"]);
|
|
54
|
-
const SUPPORTED_VERTICAL_RELATIVE_FROM = new Set(["page", "margin"]);
|
|
66
|
+
const SUPPORTED_VERTICAL_RELATIVE_FROM = new Set(["page", "margin", "paragraph"]);
|
|
67
|
+
const SUPPORTED_FLOAT_WRAP_MODES = new Set(["none", "square", "topAndBottom"]);
|
|
55
68
|
|
|
56
69
|
export function shouldRenderAbsoluteFloatingImageInPageOverlay(
|
|
57
70
|
anchor: SurfaceDrawingAnchor | undefined,
|
|
58
71
|
): boolean {
|
|
59
|
-
if (!anchor || anchor.display !== "floating" || anchor.wrapMode
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
if (anchor.layoutInCell) {
|
|
72
|
+
if (!anchor || anchor.display !== "floating" || !SUPPORTED_FLOAT_WRAP_MODES.has(anchor.wrapMode)) {
|
|
63
73
|
return false;
|
|
64
74
|
}
|
|
65
75
|
if (!anchor.positionH || !anchor.positionV) {
|
|
@@ -106,10 +116,13 @@ export function collectFloatingImageOverlayItems(input: {
|
|
|
106
116
|
storyTarget: EditorStoryTarget,
|
|
107
117
|
) => {
|
|
108
118
|
walkSurfaceBlocks(blocks, (segment) => {
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
) {
|
|
119
|
+
if (!isOverlaySegment(segment)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!shouldRenderAbsoluteFloatingImageInPageOverlay(segment.anchor)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (isMicrosoftSensitivityLabelOverlay(segment)) {
|
|
113
126
|
return;
|
|
114
127
|
}
|
|
115
128
|
|
|
@@ -119,14 +132,14 @@ export function collectFloatingImageOverlayItems(input: {
|
|
|
119
132
|
if (!pageRect) {
|
|
120
133
|
continue;
|
|
121
134
|
}
|
|
122
|
-
const localRect =
|
|
135
|
+
const localRect = resolveFloatingDrawingLocalRect(page, storyTarget, segment.anchor, pxPerTwip);
|
|
123
136
|
if (!localRect) {
|
|
124
137
|
continue;
|
|
125
138
|
}
|
|
126
|
-
const preview = input.mediaPreviews?.[segment.mediaId];
|
|
139
|
+
const preview = segment.kind === "image" ? input.mediaPreviews?.[segment.mediaId] : undefined;
|
|
127
140
|
items.push({
|
|
128
141
|
key: `${segment.segmentId}:${page.pageId}`,
|
|
129
|
-
mediaId: segment.mediaId
|
|
142
|
+
mediaId: segment.kind === "image" ? segment.mediaId : `shape:${segment.segmentId}`,
|
|
130
143
|
from: segment.from,
|
|
131
144
|
to: segment.to,
|
|
132
145
|
pageId: page.pageId,
|
|
@@ -135,10 +148,29 @@ export function collectFloatingImageOverlayItems(input: {
|
|
|
135
148
|
leftPx: localRect.leftPx,
|
|
136
149
|
widthPx: localRect.widthPx,
|
|
137
150
|
heightPx: localRect.heightPx,
|
|
151
|
+
...(segment.anchor?.relativeHeight !== undefined
|
|
152
|
+
? { zIndex: segment.anchor.relativeHeight }
|
|
153
|
+
: {}),
|
|
138
154
|
behindDoc: Boolean(segment.anchor?.behindDoc),
|
|
139
155
|
src: preview?.src ?? null,
|
|
140
|
-
altText: segment.altText ?? null,
|
|
156
|
+
altText: segment.kind === "image" ? segment.altText ?? null : null,
|
|
141
157
|
detail: segment.detail ?? null,
|
|
158
|
+
...(segment.kind === "image" && segment.pictureEffects
|
|
159
|
+
? { pictureEffects: segment.pictureEffects }
|
|
160
|
+
: {}),
|
|
161
|
+
...(segment.kind === "shape"
|
|
162
|
+
? {
|
|
163
|
+
shape: {
|
|
164
|
+
label: segment.label,
|
|
165
|
+
text: segment.txbxText ?? segment.label,
|
|
166
|
+
...(segment.fill ? { fill: segment.fill } : {}),
|
|
167
|
+
...(segment.line ? { line: segment.line } : {}),
|
|
168
|
+
...(segment.textBoxBody ? { body: segment.textBoxBody } : {}),
|
|
169
|
+
...(segment.txbxMarks ? { marks: segment.txbxMarks } : {}),
|
|
170
|
+
...(segment.txbxMarkAttrs ? { markAttrs: segment.txbxMarkAttrs } : {}),
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
: {}),
|
|
142
174
|
});
|
|
143
175
|
}
|
|
144
176
|
});
|
|
@@ -157,7 +189,36 @@ export function collectFloatingImageOverlayItems(input: {
|
|
|
157
189
|
collectFromStory(secondary.blocks, secondary.target);
|
|
158
190
|
}
|
|
159
191
|
|
|
160
|
-
return items;
|
|
192
|
+
return items.sort(compareOverlayItems);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isMicrosoftSensitivityLabelOverlay(
|
|
196
|
+
segment: Extract<SurfaceInlineSegment, { kind: "image" | "shape" }>,
|
|
197
|
+
): boolean {
|
|
198
|
+
if (segment.kind !== "shape") return false;
|
|
199
|
+
const name = segment.anchor?.docPr?.name ?? "";
|
|
200
|
+
const descr = segment.anchor?.docPr?.descr ?? "";
|
|
201
|
+
if (/^MSIPCM/i.test(name)) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return /"Placement"\s*:\s*"Footer"/.test(descr) &&
|
|
205
|
+
/"Index"\s*:\s*"FirstPage"/.test(descr) &&
|
|
206
|
+
/classification/i.test(segment.txbxText ?? "");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function compareOverlayItems(a: FloatingImageOverlayItem, b: FloatingImageOverlayItem): number {
|
|
210
|
+
if (a.pageIndex !== b.pageIndex) return a.pageIndex - b.pageIndex;
|
|
211
|
+
const aZ = a.zIndex ?? 0;
|
|
212
|
+
const bZ = b.zIndex ?? 0;
|
|
213
|
+
if (aZ !== bZ) return aZ - bZ;
|
|
214
|
+
return a.key.localeCompare(b.key);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isOverlaySegment(
|
|
218
|
+
segment: SurfaceInlineSegment,
|
|
219
|
+
): segment is Extract<SurfaceInlineSegment, { kind: "image" | "shape" }> {
|
|
220
|
+
if (segment.kind === "image") return true;
|
|
221
|
+
return segment.kind === "shape" && Boolean(segment.isTextBox && segment.anchor);
|
|
161
222
|
}
|
|
162
223
|
|
|
163
224
|
function walkSurfaceBlocks(
|
|
@@ -205,10 +266,10 @@ function resolveTargetPages(
|
|
|
205
266
|
});
|
|
206
267
|
}
|
|
207
268
|
|
|
208
|
-
function
|
|
269
|
+
function resolveFloatingDrawingLocalRect(
|
|
209
270
|
page: PublicPageNode,
|
|
210
271
|
activeStory: EditorStoryTarget,
|
|
211
|
-
|
|
272
|
+
anchor: SurfaceDrawingAnchor | undefined,
|
|
212
273
|
pxPerTwip: number,
|
|
213
274
|
): {
|
|
214
275
|
topPx: number;
|
|
@@ -216,7 +277,6 @@ function resolveFloatingImageLocalRect(
|
|
|
216
277
|
widthPx: number;
|
|
217
278
|
heightPx: number;
|
|
218
279
|
} | null {
|
|
219
|
-
const anchor = segment.anchor;
|
|
220
280
|
if (!anchor) {
|
|
221
281
|
return null;
|
|
222
282
|
}
|
|
@@ -245,12 +305,13 @@ function resolveHorizontalSpace(
|
|
|
245
305
|
pxPerTwip: number,
|
|
246
306
|
): { startPx: number; sizePx: number } | null {
|
|
247
307
|
const pageWidthPx = twipsToPx(page.layout.pageWidth, pxPerTwip);
|
|
308
|
+
const pageMarginLeftPx = twipsToPx(page.layout.marginLeft, pxPerTwip);
|
|
248
309
|
const storyHost = resolveStoryHostSpace(page, activeStory, pxPerTwip);
|
|
249
310
|
switch (anchor.positionH?.relativeFrom) {
|
|
250
311
|
case "page":
|
|
251
|
-
return { startPx:
|
|
312
|
+
return { startPx: -pageMarginLeftPx, sizePx: pageWidthPx };
|
|
252
313
|
case "margin":
|
|
253
|
-
return storyHost ? { startPx:
|
|
314
|
+
return storyHost ? { startPx: 0, sizePx: storyHost.widthPx } : null;
|
|
254
315
|
default:
|
|
255
316
|
return null;
|
|
256
317
|
}
|
|
@@ -269,6 +330,8 @@ function resolveVerticalSpace(
|
|
|
269
330
|
return { startPx: 0, sizePx: pageHeightPx };
|
|
270
331
|
case "margin":
|
|
271
332
|
return storyHost ? { startPx: storyHost.topPx, sizePx: storyHost.heightPx } : null;
|
|
333
|
+
case "paragraph":
|
|
334
|
+
return storyHost ? { startPx: storyHost.topPx, sizePx: storyHost.heightPx } : null;
|
|
272
335
|
default:
|
|
273
336
|
return null;
|
|
274
337
|
}
|
|
@@ -339,7 +402,7 @@ function alignAxisPosition(
|
|
|
339
402
|
page: PublicPageNode,
|
|
340
403
|
orientation: "horizontal" | "vertical",
|
|
341
404
|
): number {
|
|
342
|
-
const remainder =
|
|
405
|
+
const remainder = space.sizePx - objectSizePx;
|
|
343
406
|
switch (align) {
|
|
344
407
|
case "left":
|
|
345
408
|
case "top":
|