@beyondwork/docx-react-component 1.0.81 → 1.0.83
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 +2 -1
- package/src/api/v3/_runtime-handle.ts +4 -0
- package/src/api/v3/runtime/document.ts +61 -3
- package/src/api/v3/runtime/review.ts +55 -2
- package/src/io/normalize/normalize-text.ts +4 -1
- package/src/io/ooxml/parse-drawing.ts +4 -0
- package/src/model/canonical-document.ts +2 -0
- package/src/ui/WordReviewEditor.tsx +243 -2
- package/src/ui-tailwind/chrome/editor-action-registry.ts +220 -0
- package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +59 -35
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +256 -37
- package/src/ui-tailwind/editor-surface/pm-schema.ts +54 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +31 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +15 -0
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +24 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +35 -6
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +333 -43
- package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +32 -6
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +273 -24
- package/src/ui-tailwind/theme/editor-theme.css +3 -5
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +21 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +4 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.83",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
package/src/api/public-types.ts
CHANGED
|
@@ -5614,7 +5614,8 @@ export interface WordReviewEditorProps {
|
|
|
5614
5614
|
* Optional host-callback extension bag for workspace command chrome.
|
|
5615
5615
|
* The default `<WordReviewEditor />` path now mounts
|
|
5616
5616
|
* `TwWorkspaceChromeHost` with product-backed commands for formatting,
|
|
5617
|
-
* paragraph/list actions,
|
|
5617
|
+
* paragraph/list/style/font/color actions, search/navigation host
|
|
5618
|
+
* delegation, comments, and table insertion/structure.
|
|
5618
5619
|
* Supplying this bag overrides or extends those defaults for host-owned
|
|
5619
5620
|
* actions such as custom table properties, hyperlink handling, or
|
|
5620
5621
|
* object metadata. Actions without a wired callback are hidden from
|
|
@@ -43,6 +43,7 @@ export type RuntimeApiHandle = Pick<
|
|
|
43
43
|
DocumentRuntime,
|
|
44
44
|
// Session + export (runtime.document family)
|
|
45
45
|
| "getSessionState"
|
|
46
|
+
| "setDocumentMode"
|
|
46
47
|
| "exportDocx"
|
|
47
48
|
| "getCompatibilityReport"
|
|
48
49
|
| "getWarnings"
|
|
@@ -54,6 +55,7 @@ export type RuntimeApiHandle = Pick<
|
|
|
54
55
|
| "findAllText"
|
|
55
56
|
// Review (runtime.review family)
|
|
56
57
|
| "getReviewWorkSnapshot"
|
|
58
|
+
| "getSuggestionsSnapshot"
|
|
57
59
|
| "acceptChange"
|
|
58
60
|
| "rejectChange"
|
|
59
61
|
| "resolveComment"
|
|
@@ -136,6 +138,7 @@ export type RuntimeApiHandle = Pick<
|
|
|
136
138
|
*/
|
|
137
139
|
export const RUNTIME_API_HANDLE_SHAPE_CHECK: Record<keyof RuntimeApiHandle, true> = {
|
|
138
140
|
getSessionState: true,
|
|
141
|
+
setDocumentMode: true,
|
|
139
142
|
exportDocx: true,
|
|
140
143
|
getCompatibilityReport: true,
|
|
141
144
|
getWarnings: true,
|
|
@@ -143,6 +146,7 @@ export const RUNTIME_API_HANDLE_SHAPE_CHECK: Record<keyof RuntimeApiHandle, true
|
|
|
143
146
|
getCanonicalDocument: true,
|
|
144
147
|
findAllText: true,
|
|
145
148
|
getReviewWorkSnapshot: true,
|
|
149
|
+
getSuggestionsSnapshot: true,
|
|
146
150
|
acceptChange: true,
|
|
147
151
|
rejectChange: true,
|
|
148
152
|
resolveComment: true,
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @endStateApi v3 — `runtime.document` family.
|
|
3
3
|
*
|
|
4
|
-
* See docs/reference/public-api.md § runtime.document.
|
|
4
|
+
* See docs/reference/public-api.md § runtime.document.
|
|
5
5
|
* `load` (partial — runtime pre-load is the caller's responsibility; v3
|
|
6
|
-
* exposes a re-mount semantic later), `
|
|
7
|
-
*
|
|
6
|
+
* exposes a re-mount semantic later), `getMode` / `setMode` (live;
|
|
7
|
+
* delegates to the runtime view-state posture), `export` (live; delegates
|
|
8
|
+
* to `runtime.exportDocx`), `validate` (partial; read live, write mock).
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
11
12
|
import type {
|
|
13
|
+
DocumentMode,
|
|
12
14
|
EditorError,
|
|
13
15
|
ExportDocxOptions,
|
|
14
16
|
ExportResult,
|
|
@@ -88,6 +90,42 @@ export const loadMetadata: ApiV3FnMetadata = {
|
|
|
88
90
|
"§Runtime API § runtime.document.load. Graduation (2026-04-22, post-eb7d14fa): `live` via direct delegation to `loadDocxSessionAsync` (src/session/import/loader.ts). Returns a PersistedEditorSnapshot the caller can pass to DocxSession.reopenFromSnapshot or persist for later rehydrate. Note per arch §R8 Option B: v3 does NOT construct the receiving DocumentRuntime — that's the caller's job via createDocumentRuntime(initialSessionState).",
|
|
89
91
|
};
|
|
90
92
|
|
|
93
|
+
/* ================================================================== */
|
|
94
|
+
/* mode */
|
|
95
|
+
/* ================================================================== */
|
|
96
|
+
|
|
97
|
+
export const getModeMetadata: ApiV3FnMetadata = {
|
|
98
|
+
name: "runtime.document.getMode",
|
|
99
|
+
status: "live",
|
|
100
|
+
sourceLayer: "runtime-core",
|
|
101
|
+
liveEvidence: {
|
|
102
|
+
runnerTest: "test/api/v3/create-accepts-handle.test.ts",
|
|
103
|
+
commit: "refactor-03-tracked-changes-v1-api-adapter",
|
|
104
|
+
},
|
|
105
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
106
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "document", auditCategory: "document-mode" },
|
|
107
|
+
stateClass: "C-local",
|
|
108
|
+
persistsTo: "none",
|
|
109
|
+
rwdReference:
|
|
110
|
+
"§Runtime API § runtime.document.getMode. Live adapter over the runtime render snapshot's DocumentMode; suggesting remains the tracked-change authoring posture.",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const setModeMetadata: ApiV3FnMetadata = {
|
|
114
|
+
name: "runtime.document.setMode",
|
|
115
|
+
status: "live",
|
|
116
|
+
sourceLayer: "runtime-core",
|
|
117
|
+
liveEvidence: {
|
|
118
|
+
runnerTest: "test/api/v3/create-accepts-handle.test.ts",
|
|
119
|
+
commit: "refactor-03-tracked-changes-v1-api-adapter",
|
|
120
|
+
},
|
|
121
|
+
uxIntent: { uiVisible: true, expectsUxResponse: "surface-refresh", expectedDelta: "document mode changes" },
|
|
122
|
+
agentMetadata: { readOrMutate: "mutate", boundedScope: "document", auditCategory: "document-mode" },
|
|
123
|
+
stateClass: "C-local",
|
|
124
|
+
persistsTo: "none",
|
|
125
|
+
rwdReference:
|
|
126
|
+
"§Runtime API § runtime.document.setMode. Live adapter over runtime.setDocumentMode(); mode 'suggesting' is the v3 entry to tracked-change authoring.",
|
|
127
|
+
};
|
|
128
|
+
|
|
91
129
|
/* ================================================================== */
|
|
92
130
|
/* export */
|
|
93
131
|
/* ================================================================== */
|
|
@@ -196,6 +234,26 @@ export function createDocumentFamily(runtime: RuntimeApiHandle) {
|
|
|
196
234
|
return result;
|
|
197
235
|
},
|
|
198
236
|
|
|
237
|
+
getMode(): DocumentMode {
|
|
238
|
+
// @endStateApi — live. Reads the runtime view-state posture that
|
|
239
|
+
// render snapshots already expose.
|
|
240
|
+
return runtime.getRenderSnapshot().documentMode;
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
setMode(mode: DocumentMode): void {
|
|
244
|
+
// @endStateApi — live. Delegates to the runtime's document-mode
|
|
245
|
+
// setter; `suggesting` is the tracked-change authoring posture.
|
|
246
|
+
runtime.setDocumentMode(mode);
|
|
247
|
+
emitUxResponse(runtime, {
|
|
248
|
+
apiFn: setModeMetadata.name,
|
|
249
|
+
intent: setModeMetadata.uxIntent.expectedDelta ?? "",
|
|
250
|
+
mockOrLive: "live",
|
|
251
|
+
uiVisible: true,
|
|
252
|
+
expectedDelta: setModeMetadata.uxIntent.expectedDelta,
|
|
253
|
+
actualDelta: { kind: "surface-refresh", payload: { mode } },
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
|
|
199
257
|
async export(options?: ExportDocxOptions): Promise<ExportResult> {
|
|
200
258
|
// @endStateApi — live. Delegates to the shipped runtime export path.
|
|
201
259
|
const result = await runtime.exportDocx(options);
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @endStateApi v3 — `runtime.review` family.
|
|
3
3
|
*
|
|
4
|
-
* getComments (live) / getChanges (live) /
|
|
5
|
-
* resolveComment (live).
|
|
4
|
+
* getComments (live) / getChanges (live) / getSuggestions (live) /
|
|
5
|
+
* acceptChange (live) / rejectChange (live) / resolveComment (live).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { RuntimeApiHandle } from "../_runtime-handle.ts";
|
|
9
9
|
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
10
10
|
import type {
|
|
11
11
|
CommentSidebarThreadSnapshot,
|
|
12
|
+
SuggestionsSnapshot,
|
|
12
13
|
TrackedChangeEntrySnapshot,
|
|
13
14
|
} from "../../public-types.ts";
|
|
14
15
|
import { emitUxResponse } from "../_ux-response.ts";
|
|
@@ -51,6 +52,22 @@ export const getChangesMetadata: ApiV3FnMetadata = {
|
|
|
51
52
|
rwdReference: "§Runtime API § runtime.review.getChanges",
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
export const getSuggestionsMetadata: ApiV3FnMetadata = {
|
|
56
|
+
name: "runtime.review.getSuggestions",
|
|
57
|
+
status: "live",
|
|
58
|
+
sourceLayer: "workflow-review",
|
|
59
|
+
liveEvidence: {
|
|
60
|
+
runnerTest: "test/api/v3/create-accepts-handle.test.ts",
|
|
61
|
+
commit: "refactor-03-tracked-changes-v1-api-adapter",
|
|
62
|
+
},
|
|
63
|
+
uxIntent: { uiVisible: false, expectsUxResponse: "none" },
|
|
64
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "document", auditCategory: "review-read" },
|
|
65
|
+
stateClass: "A-canonical",
|
|
66
|
+
persistsTo: "canonical",
|
|
67
|
+
rwdReference:
|
|
68
|
+
"§Runtime API § runtime.review.getSuggestions. Live adapter over runtime.getSuggestionsSnapshot(); semantic suggestion readback is grouped by the runtime, not by v3.",
|
|
69
|
+
};
|
|
70
|
+
|
|
54
71
|
export const acceptChangeMetadata: ApiV3FnMetadata = {
|
|
55
72
|
name: "runtime.review.acceptChange",
|
|
56
73
|
status: "live",
|
|
@@ -70,6 +87,23 @@ export const acceptChangeMetadata: ApiV3FnMetadata = {
|
|
|
70
87
|
rwdReference: "§Runtime API § runtime.review.acceptChange",
|
|
71
88
|
};
|
|
72
89
|
|
|
90
|
+
export const rejectChangeMetadata: ApiV3FnMetadata = {
|
|
91
|
+
name: "runtime.review.rejectChange",
|
|
92
|
+
status: "live",
|
|
93
|
+
sourceLayer: "workflow-review",
|
|
94
|
+
liveEvidence: {
|
|
95
|
+
runnerTest: "test/api/v3/create-accepts-handle.test.ts",
|
|
96
|
+
commit: "refactor-03-tracked-changes-v1-api-adapter",
|
|
97
|
+
},
|
|
98
|
+
uxIntent: { uiVisible: true, expectsUxResponse: "inline-change", expectedDelta: "change mark disappears and text restores" },
|
|
99
|
+
agentMetadata: { readOrMutate: "mutate", boundedScope: "scope", auditCategory: "change-reject" },
|
|
100
|
+
stateClass: "A-canonical",
|
|
101
|
+
persistsTo: "canonical",
|
|
102
|
+
broadcastsVia: "crdt",
|
|
103
|
+
rwdReference:
|
|
104
|
+
"§Runtime API § runtime.review.rejectChange. Live adapter over runtime.rejectChange; mirrors acceptChange for individual tracked-change review.",
|
|
105
|
+
};
|
|
106
|
+
|
|
73
107
|
export const resolveCommentMetadata: ApiV3FnMetadata = {
|
|
74
108
|
name: "runtime.review.resolveComment",
|
|
75
109
|
status: "live",
|
|
@@ -100,6 +134,12 @@ export function createReviewFamily(runtime: RuntimeApiHandle) {
|
|
|
100
134
|
return runtime.getRenderSnapshot().trackedChanges.revisions;
|
|
101
135
|
},
|
|
102
136
|
|
|
137
|
+
getSuggestions(): SuggestionsSnapshot {
|
|
138
|
+
// @endStateApi — live. Delegates to the runtime's semantic
|
|
139
|
+
// suggestion grouping rather than regrouping raw revisions here.
|
|
140
|
+
return runtime.getSuggestionsSnapshot();
|
|
141
|
+
},
|
|
142
|
+
|
|
103
143
|
acceptChange(changeId: string): void {
|
|
104
144
|
// @endStateApi — live. Delegates.
|
|
105
145
|
runtime.acceptChange(changeId);
|
|
@@ -113,6 +153,19 @@ export function createReviewFamily(runtime: RuntimeApiHandle) {
|
|
|
113
153
|
});
|
|
114
154
|
},
|
|
115
155
|
|
|
156
|
+
rejectChange(changeId: string): void {
|
|
157
|
+
// @endStateApi — live. Delegates.
|
|
158
|
+
runtime.rejectChange(changeId);
|
|
159
|
+
emitUxResponse(runtime, {
|
|
160
|
+
apiFn: rejectChangeMetadata.name,
|
|
161
|
+
intent: rejectChangeMetadata.uxIntent.expectedDelta ?? "",
|
|
162
|
+
mockOrLive: "live",
|
|
163
|
+
uiVisible: true,
|
|
164
|
+
expectedDelta: rejectChangeMetadata.uxIntent.expectedDelta,
|
|
165
|
+
actualDelta: { kind: "inline-change", payload: { changeId } },
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
|
|
116
169
|
resolveComment(commentId: string): void {
|
|
117
170
|
// @endStateApi — live.
|
|
118
171
|
runtime.resolveComment(commentId);
|
|
@@ -671,7 +671,10 @@ function normalizeDrawingFrameNode(
|
|
|
671
671
|
const filename = packagePartName.slice(packagePartName.lastIndexOf("/") + 1) || "image.bin";
|
|
672
672
|
state.media.items[node.content.mediaId] = {
|
|
673
673
|
mediaId: node.content.mediaId,
|
|
674
|
-
contentType:
|
|
674
|
+
contentType:
|
|
675
|
+
node.content.contentType ??
|
|
676
|
+
existingMediaItem?.contentType ??
|
|
677
|
+
"application/octet-stream",
|
|
675
678
|
filename,
|
|
676
679
|
packagePartName,
|
|
677
680
|
relationshipId: node.content.blipRef,
|
|
@@ -188,8 +188,12 @@ function resolveContent(
|
|
|
188
188
|
const partPath = normalizePartPath(
|
|
189
189
|
resolveRelationshipTarget(opts.sourcePartPath ?? "/word/document.xml", rel),
|
|
190
190
|
);
|
|
191
|
+
const mediaPart = opts.mediaParts?.get(partPath);
|
|
191
192
|
pic.packagePartName = partPath;
|
|
192
193
|
pic.mediaId = `media:${partPath.slice(1)}`;
|
|
194
|
+
if (mediaPart?.contentType) {
|
|
195
|
+
pic.contentType = mediaPart.contentType;
|
|
196
|
+
}
|
|
193
197
|
}
|
|
194
198
|
// F4.1 — preserve outer drawing XML for lossless round-trip serialization
|
|
195
199
|
pic.rawXml = rawXml;
|
|
@@ -1922,6 +1922,8 @@ export interface PictureContent {
|
|
|
1922
1922
|
mediaId?: string;
|
|
1923
1923
|
/** Absolute package path for media catalog lookup. */
|
|
1924
1924
|
packagePartName?: string;
|
|
1925
|
+
/** MIME resolved from the OPC media part, when known. */
|
|
1926
|
+
contentType?: string;
|
|
1925
1927
|
srcRect?: { top: number; bottom: number; left: number; right: number };
|
|
1926
1928
|
stretch?: boolean;
|
|
1927
1929
|
/**
|
|
@@ -2414,9 +2414,10 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2414
2414
|
function addReviewComment(): string | null {
|
|
2415
2415
|
try {
|
|
2416
2416
|
const { commentId } = activeRuntime.addComment({
|
|
2417
|
-
anchor: snapshot
|
|
2417
|
+
anchor: resolveCommentCommandAnchor(snapshot),
|
|
2418
2418
|
body: "",
|
|
2419
2419
|
authorId: currentUser.userId,
|
|
2420
|
+
snapToSafeBoundary: true,
|
|
2420
2421
|
});
|
|
2421
2422
|
activeRuntime.openComment(commentId);
|
|
2422
2423
|
setActiveRailTab("comments");
|
|
@@ -3341,13 +3342,40 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3341
3342
|
onToggleItalic: commands.onToggleItalic,
|
|
3342
3343
|
onToggleUnderline: commands.onToggleUnderline,
|
|
3343
3344
|
onToggleStrikethrough: commands.onToggleStrikethrough,
|
|
3345
|
+
onSetParagraphStyle: (styleId) => {
|
|
3346
|
+
const resolvedStyleId = resolveProductParagraphStyleId(styleCatalog, styleId);
|
|
3347
|
+
if (!resolvedStyleId) {
|
|
3348
|
+
activeRuntime.emitBlockedCommand("setParagraphStyle", [{
|
|
3349
|
+
code: "unsupported_surface",
|
|
3350
|
+
message: `${styleId} is not available in this document's style catalog.`,
|
|
3351
|
+
}]);
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
commands.onSetParagraphStyle?.(resolvedStyleId);
|
|
3355
|
+
},
|
|
3356
|
+
onSetFontFamily: commands.onSetFontFamily,
|
|
3357
|
+
onSetFontSize: commands.onSetFontSize,
|
|
3358
|
+
onSetTextColor: commands.onSetTextColor,
|
|
3359
|
+
onSetHighlightColor: commands.onSetHighlightColor,
|
|
3344
3360
|
onToggleBulletedList: commands.onToggleBulletedList,
|
|
3345
3361
|
onToggleNumberedList: commands.onToggleNumberedList,
|
|
3346
3362
|
onOutdent: commands.onOutdent,
|
|
3347
3363
|
onIndent: commands.onIndent,
|
|
3348
3364
|
onSetAlignment: (alignment) => commands.onSetAlignment?.(alignment),
|
|
3365
|
+
onInsertPageBreak: commands.onInsertPageBreak,
|
|
3366
|
+
onInsertSectionBreak: (type) => commands.onInsertSectionBreak?.(type),
|
|
3349
3367
|
onInsertTable: commands.onInsertTable,
|
|
3350
3368
|
onAddComment: commands.onAddComment,
|
|
3369
|
+
onFindRequested: onFindRequested
|
|
3370
|
+
? () => onFindRequested({ selectionText: "", selectionRange: snapshot.selection })
|
|
3371
|
+
: undefined,
|
|
3372
|
+
onReplaceRequested: onReplaceRequested
|
|
3373
|
+
? () => onReplaceRequested({ selectionText: "", selectionRange: snapshot.selection })
|
|
3374
|
+
: undefined,
|
|
3375
|
+
onPrintRequested,
|
|
3376
|
+
onGoToRequested: onGoToRequested
|
|
3377
|
+
? () => onGoToRequested({ selectionText: "", selectionRange: snapshot.selection })
|
|
3378
|
+
: undefined,
|
|
3351
3379
|
onInsertRowAbove: commands.onAddRowBefore,
|
|
3352
3380
|
onInsertRowBelow: commands.onAddRowAfter,
|
|
3353
3381
|
onInsertColumnBefore: commands.onAddColumnBefore,
|
|
@@ -3362,7 +3390,17 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
3362
3390
|
return editorActionHost
|
|
3363
3391
|
? { ...defaultHost, ...editorActionHost }
|
|
3364
3392
|
: defaultHost;
|
|
3365
|
-
}, [
|
|
3393
|
+
}, [
|
|
3394
|
+
activeRuntime,
|
|
3395
|
+
commands,
|
|
3396
|
+
editorActionHost,
|
|
3397
|
+
onFindRequested,
|
|
3398
|
+
onGoToRequested,
|
|
3399
|
+
onPrintRequested,
|
|
3400
|
+
onReplaceRequested,
|
|
3401
|
+
snapshot.selection,
|
|
3402
|
+
styleCatalog,
|
|
3403
|
+
]);
|
|
3366
3404
|
|
|
3367
3405
|
const harnessShowUnsupportedPreviews = readHarnessDebugPortsFlag(__harnessDebugPorts, "unsupportedObjectPreviews");
|
|
3368
3406
|
const effectiveShowUnsupportedPreviews = computeEffectiveShowUnsupportedPreviews({
|
|
@@ -4155,6 +4193,176 @@ function summarizeSelectionPreview(snapshot: RuntimeRenderSnapshot): string | nu
|
|
|
4155
4193
|
return preview.length > 48 ? `${preview.slice(0, 45)}...` : preview;
|
|
4156
4194
|
}
|
|
4157
4195
|
|
|
4196
|
+
function resolveCommentCommandAnchor(
|
|
4197
|
+
snapshot: RuntimeRenderSnapshot,
|
|
4198
|
+
): PublicSelectionSnapshot["activeRange"] {
|
|
4199
|
+
const selection = snapshot.selection;
|
|
4200
|
+
if (!selection.isCollapsed && selection.activeRange.kind === "range") {
|
|
4201
|
+
return selection.activeRange;
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
const collapsedRange = resolveCollapsedCommentRange(snapshot.surface, selection);
|
|
4205
|
+
return collapsedRange
|
|
4206
|
+
? {
|
|
4207
|
+
kind: "range",
|
|
4208
|
+
from: collapsedRange.from,
|
|
4209
|
+
to: collapsedRange.to,
|
|
4210
|
+
assoc: { start: -1, end: 1 },
|
|
4211
|
+
}
|
|
4212
|
+
: selection.activeRange;
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
function resolveCollapsedCommentRange(
|
|
4216
|
+
surface: RuntimeRenderSnapshot["surface"],
|
|
4217
|
+
selection: RuntimeRenderSnapshot["selection"],
|
|
4218
|
+
): { from: number; to: number } | null {
|
|
4219
|
+
if (!surface) {
|
|
4220
|
+
return null;
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
const position = selection.activeRange.kind === "node"
|
|
4224
|
+
? selection.activeRange.at
|
|
4225
|
+
: selection.head;
|
|
4226
|
+
const paragraph =
|
|
4227
|
+
findParagraphRangeAtPosition(surface.blocks, position) ??
|
|
4228
|
+
findFirstNonEmptyParagraphRange(surface.blocks);
|
|
4229
|
+
return paragraph
|
|
4230
|
+
? resolveWordRangeInsideParagraph(surface.plainText, paragraph, position)
|
|
4231
|
+
: null;
|
|
4232
|
+
}
|
|
4233
|
+
|
|
4234
|
+
function findParagraphRangeAtPosition(
|
|
4235
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
4236
|
+
position: number,
|
|
4237
|
+
): { from: number; to: number } | null {
|
|
4238
|
+
for (const block of blocks) {
|
|
4239
|
+
if (
|
|
4240
|
+
block.kind === "paragraph" &&
|
|
4241
|
+
block.to > block.from &&
|
|
4242
|
+
position >= block.from &&
|
|
4243
|
+
position <= block.to
|
|
4244
|
+
) {
|
|
4245
|
+
return { from: block.from, to: block.to };
|
|
4246
|
+
}
|
|
4247
|
+
const nested = findParagraphRangeInNestedBlocks(block, position);
|
|
4248
|
+
if (nested) {
|
|
4249
|
+
return nested;
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
return null;
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
function findFirstNonEmptyParagraphRange(
|
|
4256
|
+
blocks: readonly SurfaceBlockSnapshot[],
|
|
4257
|
+
): { from: number; to: number } | null {
|
|
4258
|
+
for (const block of blocks) {
|
|
4259
|
+
if (block.kind === "paragraph" && block.to > block.from) {
|
|
4260
|
+
return { from: block.from, to: block.to };
|
|
4261
|
+
}
|
|
4262
|
+
const nested = findFirstNonEmptyParagraphInNestedBlocks(block);
|
|
4263
|
+
if (nested) {
|
|
4264
|
+
return nested;
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
return null;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
function findParagraphRangeInNestedBlocks(
|
|
4271
|
+
block: SurfaceBlockSnapshot,
|
|
4272
|
+
position: number,
|
|
4273
|
+
): { from: number; to: number } | null {
|
|
4274
|
+
if (block.kind === "sdt_block") {
|
|
4275
|
+
return findParagraphRangeAtPosition(block.children, position);
|
|
4276
|
+
}
|
|
4277
|
+
if (block.kind === "table") {
|
|
4278
|
+
for (const row of block.rows) {
|
|
4279
|
+
for (const cell of row.cells) {
|
|
4280
|
+
const nested = findParagraphRangeAtPosition(cell.content, position);
|
|
4281
|
+
if (nested) {
|
|
4282
|
+
return nested;
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
return null;
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
function findFirstNonEmptyParagraphInNestedBlocks(
|
|
4291
|
+
block: SurfaceBlockSnapshot,
|
|
4292
|
+
): { from: number; to: number } | null {
|
|
4293
|
+
if (block.kind === "sdt_block") {
|
|
4294
|
+
return findFirstNonEmptyParagraphRange(block.children);
|
|
4295
|
+
}
|
|
4296
|
+
if (block.kind === "table") {
|
|
4297
|
+
for (const row of block.rows) {
|
|
4298
|
+
for (const cell of row.cells) {
|
|
4299
|
+
const nested = findFirstNonEmptyParagraphRange(cell.content);
|
|
4300
|
+
if (nested) {
|
|
4301
|
+
return nested;
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
return null;
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
function resolveWordRangeInsideParagraph(
|
|
4310
|
+
plainText: string,
|
|
4311
|
+
paragraph: { from: number; to: number },
|
|
4312
|
+
position: number,
|
|
4313
|
+
): { from: number; to: number } {
|
|
4314
|
+
const paragraphFrom = paragraph.from;
|
|
4315
|
+
const paragraphTo = paragraph.to;
|
|
4316
|
+
const fallback = { from: paragraphFrom, to: paragraphTo };
|
|
4317
|
+
if (paragraphTo <= paragraphFrom) {
|
|
4318
|
+
return fallback;
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
let cursor = Math.max(paragraphFrom, Math.min(position, paragraphTo - 1));
|
|
4322
|
+
if (isCommentWordBoundary(plainText.charAt(cursor))) {
|
|
4323
|
+
const next = findNearestCommentTextOffset(plainText, cursor, paragraphFrom, paragraphTo);
|
|
4324
|
+
if (next === null) {
|
|
4325
|
+
return fallback;
|
|
4326
|
+
}
|
|
4327
|
+
cursor = next;
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
let from = cursor;
|
|
4331
|
+
while (from > paragraphFrom && !isCommentWordBoundary(plainText.charAt(from - 1))) {
|
|
4332
|
+
from -= 1;
|
|
4333
|
+
}
|
|
4334
|
+
|
|
4335
|
+
let to = cursor + 1;
|
|
4336
|
+
while (to < paragraphTo && !isCommentWordBoundary(plainText.charAt(to))) {
|
|
4337
|
+
to += 1;
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
return to > from ? { from, to } : fallback;
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
function findNearestCommentTextOffset(
|
|
4344
|
+
plainText: string,
|
|
4345
|
+
cursor: number,
|
|
4346
|
+
from: number,
|
|
4347
|
+
to: number,
|
|
4348
|
+
): number | null {
|
|
4349
|
+
for (let distance = 1; distance < to - from; distance += 1) {
|
|
4350
|
+
const right = cursor + distance;
|
|
4351
|
+
if (right < to && !isCommentWordBoundary(plainText.charAt(right))) {
|
|
4352
|
+
return right;
|
|
4353
|
+
}
|
|
4354
|
+
const left = cursor - distance;
|
|
4355
|
+
if (left >= from && !isCommentWordBoundary(plainText.charAt(left))) {
|
|
4356
|
+
return left;
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
return null;
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
function isCommentWordBoundary(value: string): boolean {
|
|
4363
|
+
return value.length === 0 || /\s/.test(value);
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4158
4366
|
function selectionToolbarAnchorsEqual(
|
|
4159
4367
|
left: SelectionToolbarAnchor | null,
|
|
4160
4368
|
right: SelectionToolbarAnchor | null,
|
|
@@ -4574,6 +4782,39 @@ function createSelectionToolbarStyleBadge(
|
|
|
4574
4782
|
return { label: styleEntry.displayName };
|
|
4575
4783
|
}
|
|
4576
4784
|
|
|
4785
|
+
function resolveProductParagraphStyleId(
|
|
4786
|
+
styleCatalog: StyleCatalogSnapshot,
|
|
4787
|
+
requestedStyleId: string,
|
|
4788
|
+
): string | null {
|
|
4789
|
+
switch (requestedStyleId) {
|
|
4790
|
+
case "Heading1":
|
|
4791
|
+
return resolveHeadingShortcutStyleId(styleCatalog, 1);
|
|
4792
|
+
case "Heading2":
|
|
4793
|
+
return resolveHeadingShortcutStyleId(styleCatalog, 2);
|
|
4794
|
+
case "Heading3":
|
|
4795
|
+
return resolveHeadingShortcutStyleId(styleCatalog, 3);
|
|
4796
|
+
case "Normal": {
|
|
4797
|
+
const defaultStyle = styleCatalog.paragraphs.find((entry) => entry.isDefault);
|
|
4798
|
+
if (defaultStyle) return defaultStyle.styleId;
|
|
4799
|
+
break;
|
|
4800
|
+
}
|
|
4801
|
+
default:
|
|
4802
|
+
break;
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
const requestedToken = normalizeProductStyleToken(requestedStyleId);
|
|
4806
|
+
const match = styleCatalog.paragraphs.find(
|
|
4807
|
+
(entry) =>
|
|
4808
|
+
normalizeProductStyleToken(entry.styleId) === requestedToken ||
|
|
4809
|
+
normalizeProductStyleToken(entry.displayName) === requestedToken,
|
|
4810
|
+
);
|
|
4811
|
+
return match?.styleId ?? null;
|
|
4812
|
+
}
|
|
4813
|
+
|
|
4814
|
+
function normalizeProductStyleToken(value: string): string {
|
|
4815
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4577
4818
|
function createSelectionToolbarListBadge(
|
|
4578
4819
|
viewState: ReturnType<WordReviewEditorRuntime["getViewState"]>,
|
|
4579
4820
|
): SelectionToolbarModel["badges"][number] | null {
|