@beyondwork/docx-react-component 1.0.96 → 1.0.97
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 +33 -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/model/canonical-document.ts +17 -0
- package/src/runtime/layout/layout-engine-version.ts +8 -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/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/runtime-decoration-plugin.ts +190 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +53 -53
- 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
|
@@ -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,24 +928,11 @@ 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
|
|
|
@@ -940,7 +941,6 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
940
941
|
state,
|
|
941
942
|
nodeViews: { ...tableNodeViews, ...chartNodeViews },
|
|
942
943
|
editable: () => canEdit,
|
|
943
|
-
decorations: () => decorations,
|
|
944
944
|
});
|
|
945
945
|
viewRef.current = view;
|
|
946
946
|
recordPerfSample("pm.mount");
|
|
@@ -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":
|