@beyondwork/docx-react-component 1.0.18 → 1.0.19
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/README.md +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +374 -4
- package/src/api/session-state.ts +58 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +5 -1
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +329 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +1 -1
- package/src/index.ts +30 -0
- package/src/io/docx-session.ts +260 -39
- package/src/io/export/serialize-main-document.ts +202 -5
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/normalize/normalize-text.ts +63 -25
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-footnotes.ts +212 -20
- package/src/io/ooxml/parse-headers-footers.ts +229 -25
- package/src/io/ooxml/parse-inline-media.ts +16 -0
- package/src/io/ooxml/parse-main-document.ts +411 -6
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/model/canonical-document.ts +133 -3
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +2 -1
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +564 -0
- package/src/runtime/document-runtime.ts +265 -35
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
- package/src/runtime/session-capabilities.ts +2 -0
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +239 -12
- package/src/runtime/table-schema.ts +87 -5
- package/src/runtime/view-state.ts +459 -0
- package/src/ui/WordReviewEditor.tsx +1902 -312
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
- package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
- package/src/ui-tailwind/theme/editor-theme.css +123 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
- package/src/validation/compatibility-engine.ts +92 -20
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +487 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, {
|
|
2
2
|
forwardRef,
|
|
3
3
|
type FocusEventHandler,
|
|
4
|
+
useCallback,
|
|
4
5
|
useEffect,
|
|
5
6
|
useImperativeHandle,
|
|
6
7
|
useMemo,
|
|
@@ -9,12 +10,15 @@ import React, {
|
|
|
9
10
|
import { EditorView } from "prosemirror-view";
|
|
10
11
|
|
|
11
12
|
import type {
|
|
13
|
+
DocumentNavigationSnapshot,
|
|
12
14
|
EditorUser,
|
|
13
15
|
RuntimeRenderSnapshot,
|
|
14
16
|
SearchOptions,
|
|
15
17
|
SearchResultSnapshot,
|
|
16
18
|
SelectionSnapshot,
|
|
17
19
|
} from "../../api/public-types";
|
|
20
|
+
import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
|
|
21
|
+
import { searchDocument } from "../../runtime/document-search.ts";
|
|
18
22
|
import {
|
|
19
23
|
getTableSelectionDescriptor,
|
|
20
24
|
type TableSelectionDescriptor,
|
|
@@ -24,22 +28,27 @@ import {
|
|
|
24
28
|
type MarkupDisplay,
|
|
25
29
|
} from "../../ui/headless/comment-decoration-model";
|
|
26
30
|
import { createRevisionDecorationModel } from "../../ui/headless/revision-decoration-model";
|
|
27
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
createPMSelectionFromSnapshot,
|
|
33
|
+
createPMStateFromSnapshot,
|
|
34
|
+
} from "./pm-state-from-snapshot";
|
|
28
35
|
import {
|
|
29
36
|
createCommandBridgePlugins,
|
|
30
37
|
type CommandBridgeCallbacks,
|
|
31
38
|
} from "./pm-command-bridge";
|
|
32
39
|
import { buildDecorations } from "./pm-decorations";
|
|
40
|
+
import { createContextualInteractionPlugin } from "./pm-contextual-ui";
|
|
41
|
+
import { finishPerfProbe, startPerfProbe } from "./perf-probe";
|
|
33
42
|
import type { PositionMap } from "./pm-position-map";
|
|
34
43
|
import {
|
|
35
44
|
clearSearch as clearSearchPlugin,
|
|
36
|
-
createSearchExcerpt,
|
|
37
45
|
createSearchPlugin,
|
|
38
46
|
DEFAULT_SEARCH_HIGHLIGHT_COLOR,
|
|
39
47
|
performSearch,
|
|
40
48
|
searchPluginKey,
|
|
41
49
|
} from "./search-plugin";
|
|
42
50
|
import { tableNodeViews } from "./tw-table-node-view";
|
|
51
|
+
import type { SelectionToolbarAnchor } from "../../ui/headless/selection-toolbar-model";
|
|
43
52
|
|
|
44
53
|
/**
|
|
45
54
|
* Same props interface as the legacy TwEditorSurface — drop-in replacement.
|
|
@@ -47,10 +56,14 @@ import { tableNodeViews } from "./tw-table-node-view";
|
|
|
47
56
|
export interface TwProseMirrorSurfaceProps {
|
|
48
57
|
currentUser: EditorUser;
|
|
49
58
|
snapshot: RuntimeRenderSnapshot;
|
|
59
|
+
canonicalDocument: CanonicalDocumentEnvelope;
|
|
60
|
+
documentNavigation: DocumentNavigationSnapshot;
|
|
50
61
|
reviewMode: "editing" | "review";
|
|
51
62
|
markupDisplay: MarkupDisplay;
|
|
52
63
|
activeRevisionId?: string;
|
|
53
64
|
showTrackedChanges?: boolean;
|
|
65
|
+
/** When true, the surface renders inside the page workspace (vs canvas). */
|
|
66
|
+
isPageWorkspace?: boolean;
|
|
54
67
|
onFocus: FocusEventHandler<HTMLDivElement>;
|
|
55
68
|
onBlur: FocusEventHandler<HTMLDivElement>;
|
|
56
69
|
onSelectionChange?: (selection: SelectionSnapshot) => void;
|
|
@@ -58,10 +71,12 @@ export interface TwProseMirrorSurfaceProps {
|
|
|
58
71
|
onDeleteBackward?: () => void;
|
|
59
72
|
onDeleteForward?: () => void;
|
|
60
73
|
onInsertTab?: () => void;
|
|
74
|
+
onOutdentTab?: () => void;
|
|
61
75
|
onInsertHardBreak?: () => void;
|
|
62
76
|
onSplitParagraph?: () => void;
|
|
63
77
|
onCommentActivated?: (commentId: string) => void;
|
|
64
78
|
onRevisionActivated?: (revisionId: string) => void;
|
|
79
|
+
onSelectionToolbarAnchorChange?: (anchor: SelectionToolbarAnchor | null) => void;
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
export interface TwProseMirrorSurfaceRef {
|
|
@@ -92,19 +107,43 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
92
107
|
const positionMapRef = useRef<PositionMap | null>(null);
|
|
93
108
|
const callbacksRef = useRef<CommandBridgeCallbacks | null>(null);
|
|
94
109
|
const activeSearchRef = useRef<{ query: string; options: SearchOptions } | null>(null);
|
|
110
|
+
const pendingTypingProbeRef = useRef<string | null>(null);
|
|
111
|
+
const pendingSelectionProbeRef = useRef<string | null>(null);
|
|
112
|
+
const surfaceBuildKeyRef = useRef<string | null>(null);
|
|
113
|
+
const suppressSelectionEchoRef = useRef(false);
|
|
114
|
+
const selectionToolbarFrameRef = useRef<number | null>(null);
|
|
115
|
+
const lastSelectionToolbarMeasurementRef = useRef<{
|
|
116
|
+
key: string | null;
|
|
117
|
+
anchor: SelectionToolbarAnchor | null;
|
|
118
|
+
}>({
|
|
119
|
+
key: null,
|
|
120
|
+
anchor: null,
|
|
121
|
+
});
|
|
95
122
|
|
|
96
123
|
// Keep callbacks ref up to date (avoids stale closures in PM plugins)
|
|
97
124
|
callbacksRef.current = {
|
|
98
|
-
onInsertText: (text) =>
|
|
125
|
+
onInsertText: (text) => {
|
|
126
|
+
pendingTypingProbeRef.current = startPerfProbe("typing");
|
|
127
|
+
props.onInsertText?.(text);
|
|
128
|
+
},
|
|
99
129
|
onDeleteBackward: () => props.onDeleteBackward?.(),
|
|
100
130
|
onDeleteForward: () => props.onDeleteForward?.(),
|
|
101
131
|
onSplitParagraph: () => props.onSplitParagraph?.(),
|
|
102
132
|
onInsertHardBreak: () => props.onInsertHardBreak?.(),
|
|
103
133
|
onInsertTab: () => props.onInsertTab?.(),
|
|
134
|
+
onOutdentTab: () => props.onOutdentTab?.(),
|
|
104
135
|
onUndo: () => {}, // Handled by toolbar, not PM
|
|
105
136
|
onRedo: () => {}, // Handled by toolbar, not PM
|
|
106
|
-
onSelectionChange: (sel) =>
|
|
137
|
+
onSelectionChange: (sel) => {
|
|
138
|
+
pendingSelectionProbeRef.current = startPerfProbe("selection");
|
|
139
|
+
props.onSelectionChange?.(
|
|
140
|
+
snapshot.activeStory.kind === "main"
|
|
141
|
+
? sel
|
|
142
|
+
: { ...sel, storyTarget: snapshot.activeStory },
|
|
143
|
+
);
|
|
144
|
+
},
|
|
107
145
|
getPositionMap: () => positionMapRef.current,
|
|
146
|
+
isSelectionSyncSuppressed: () => suppressSelectionEchoRef.current,
|
|
108
147
|
};
|
|
109
148
|
|
|
110
149
|
// Comment/revision decoration models
|
|
@@ -130,14 +169,19 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
130
169
|
onSplitParagraph: () => callbacksRef.current?.onSplitParagraph(),
|
|
131
170
|
onInsertHardBreak: () => callbacksRef.current?.onInsertHardBreak(),
|
|
132
171
|
onInsertTab: () => callbacksRef.current?.onInsertTab(),
|
|
172
|
+
onOutdentTab: () => callbacksRef.current?.onOutdentTab?.(),
|
|
133
173
|
onUndo: () => callbacksRef.current?.onUndo(),
|
|
134
174
|
onRedo: () => callbacksRef.current?.onRedo(),
|
|
135
175
|
onSelectionChange: (sel) => callbacksRef.current?.onSelectionChange(sel),
|
|
136
176
|
getPositionMap: () => callbacksRef.current?.getPositionMap() ?? null,
|
|
137
177
|
}),
|
|
178
|
+
createContextualInteractionPlugin({
|
|
179
|
+
onCommentActivated: (commentId) => props.onCommentActivated?.(commentId),
|
|
180
|
+
onRevisionActivated: (revisionId) => props.onRevisionActivated?.(revisionId),
|
|
181
|
+
}),
|
|
138
182
|
createSearchPlugin(),
|
|
139
183
|
];
|
|
140
|
-
}, []);
|
|
184
|
+
}, [props.onCommentActivated, props.onRevisionActivated]);
|
|
141
185
|
|
|
142
186
|
// Create or update PM view whenever surface becomes available or changes.
|
|
143
187
|
// The view is created lazily — if surface is null on first render (loading),
|
|
@@ -145,6 +189,34 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
145
189
|
useEffect(() => {
|
|
146
190
|
if (!mountRef.current || !surface) return;
|
|
147
191
|
|
|
192
|
+
const surfaceBuildKey = JSON.stringify({
|
|
193
|
+
revisionToken: snapshot.revisionToken,
|
|
194
|
+
activeStory: snapshot.activeStory,
|
|
195
|
+
markupDisplay,
|
|
196
|
+
canEdit,
|
|
197
|
+
showTrackedChanges,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (viewRef.current && surfaceBuildKeyRef.current === surfaceBuildKey) {
|
|
201
|
+
const positionMap = positionMapRef.current;
|
|
202
|
+
if (!positionMap) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const decorations = buildDecorations(
|
|
206
|
+
viewRef.current.state.doc,
|
|
207
|
+
positionMap,
|
|
208
|
+
commentModel,
|
|
209
|
+
revisionModel,
|
|
210
|
+
markupDisplay,
|
|
211
|
+
showTrackedChanges,
|
|
212
|
+
);
|
|
213
|
+
viewRef.current.setProps({
|
|
214
|
+
editable: () => canEdit,
|
|
215
|
+
decorations: () => decorations,
|
|
216
|
+
});
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
148
220
|
const { state, positionMap } = createPMStateFromSnapshot(
|
|
149
221
|
surface,
|
|
150
222
|
snapshot.selection,
|
|
@@ -180,8 +252,13 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
180
252
|
editable: () => canEdit,
|
|
181
253
|
decorations: () => decorations,
|
|
182
254
|
});
|
|
255
|
+
suppressSelectionEchoRef.current = true;
|
|
183
256
|
viewRef.current.updateState(state);
|
|
257
|
+
queueMicrotask(() => {
|
|
258
|
+
suppressSelectionEchoRef.current = false;
|
|
259
|
+
});
|
|
184
260
|
}
|
|
261
|
+
surfaceBuildKeyRef.current = surfaceBuildKey;
|
|
185
262
|
|
|
186
263
|
if (activeSearchRef.current) {
|
|
187
264
|
applySearch(
|
|
@@ -189,11 +266,60 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
189
266
|
activeSearchRef.current.options,
|
|
190
267
|
);
|
|
191
268
|
}
|
|
192
|
-
|
|
269
|
+
if (pendingTypingProbeRef.current) {
|
|
270
|
+
finishPerfProbe(pendingTypingProbeRef.current);
|
|
271
|
+
pendingTypingProbeRef.current = null;
|
|
272
|
+
}
|
|
273
|
+
}, [
|
|
274
|
+
snapshot.activeStory,
|
|
275
|
+
snapshot.revisionToken,
|
|
276
|
+
surface,
|
|
277
|
+
commentModel,
|
|
278
|
+
revisionModel,
|
|
279
|
+
markupDisplay,
|
|
280
|
+
canEdit,
|
|
281
|
+
showTrackedChanges,
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
const view = viewRef.current;
|
|
286
|
+
const positionMap = positionMapRef.current;
|
|
287
|
+
if (!view || !surface || !positionMap) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const nextSelection = createPMSelectionFromSnapshot(
|
|
292
|
+
view.state.doc,
|
|
293
|
+
positionMap,
|
|
294
|
+
snapshot.selection,
|
|
295
|
+
);
|
|
296
|
+
if (view.state.selection.eq(nextSelection)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
suppressSelectionEchoRef.current = true;
|
|
301
|
+
view.dispatch(view.state.tr.setSelection(nextSelection));
|
|
302
|
+
queueMicrotask(() => {
|
|
303
|
+
suppressSelectionEchoRef.current = false;
|
|
304
|
+
});
|
|
305
|
+
}, [snapshot.selection, surface]);
|
|
306
|
+
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (!pendingSelectionProbeRef.current) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
finishPerfProbe(pendingSelectionProbeRef.current);
|
|
312
|
+
pendingSelectionProbeRef.current = null;
|
|
313
|
+
}, [snapshot.selection]);
|
|
193
314
|
|
|
194
315
|
// Cleanup on unmount
|
|
195
316
|
useEffect(() => {
|
|
196
317
|
return () => {
|
|
318
|
+
const win = mountRef.current?.ownerDocument.defaultView;
|
|
319
|
+
if (selectionToolbarFrameRef.current !== null && win) {
|
|
320
|
+
win.cancelAnimationFrame(selectionToolbarFrameRef.current);
|
|
321
|
+
selectionToolbarFrameRef.current = null;
|
|
322
|
+
}
|
|
197
323
|
viewRef.current?.destroy();
|
|
198
324
|
viewRef.current = null;
|
|
199
325
|
};
|
|
@@ -230,46 +356,27 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
230
356
|
|
|
231
357
|
function applySearch(query: string, options: SearchOptions): SearchResultSnapshot[] {
|
|
232
358
|
const view = viewRef.current;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
359
|
+
if (view) {
|
|
360
|
+
const rawResults = performSearch(view.state, query, options).slice(
|
|
361
|
+
0,
|
|
362
|
+
options.limit ?? Number.POSITIVE_INFINITY,
|
|
363
|
+
);
|
|
364
|
+
view.dispatch(
|
|
365
|
+
view.state.tr.setMeta(searchPluginKey, {
|
|
366
|
+
results: rawResults,
|
|
367
|
+
highlightColor: DEFAULT_SEARCH_HIGHLIGHT_COLOR,
|
|
368
|
+
}),
|
|
369
|
+
);
|
|
236
370
|
}
|
|
237
371
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
options.limit ?? Number.POSITIVE_INFINITY,
|
|
241
|
-
);
|
|
242
|
-
view.dispatch(
|
|
243
|
-
view.state.tr.setMeta(searchPluginKey, {
|
|
244
|
-
results: rawResults,
|
|
245
|
-
highlightColor: DEFAULT_SEARCH_HIGHLIGHT_COLOR,
|
|
246
|
-
}),
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
const activeResultIndex = getActiveSearchResultIndex(
|
|
250
|
-
rawResults,
|
|
251
|
-
(position) => positionMap.pmToRuntime(position),
|
|
372
|
+
return searchDocument(
|
|
373
|
+
props.canonicalDocument,
|
|
252
374
|
snapshot.selection,
|
|
375
|
+
snapshot.activeStory,
|
|
376
|
+
props.documentNavigation,
|
|
377
|
+
query,
|
|
378
|
+
options,
|
|
253
379
|
);
|
|
254
|
-
const plainText = snapshot.surface?.plainText ?? "";
|
|
255
|
-
return rawResults.map((result, index) => {
|
|
256
|
-
const runtimeFrom = positionMap.pmToRuntime(result.from);
|
|
257
|
-
const runtimeTo = positionMap.pmToRuntime(result.to);
|
|
258
|
-
return {
|
|
259
|
-
resultId: `search-result-${index}`,
|
|
260
|
-
anchor: {
|
|
261
|
-
kind: "range",
|
|
262
|
-
from: runtimeFrom,
|
|
263
|
-
to: runtimeTo,
|
|
264
|
-
assoc: {
|
|
265
|
-
start: -1,
|
|
266
|
-
end: 1,
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
excerpt: createSearchExcerpt(plainText, runtimeFrom, runtimeTo),
|
|
270
|
-
isActive: index === activeResultIndex,
|
|
271
|
-
};
|
|
272
|
-
});
|
|
273
380
|
}
|
|
274
381
|
|
|
275
382
|
function clearLiveSearch(): void {
|
|
@@ -288,37 +395,192 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
288
395
|
? "font-[family-name:var(--font-legal-sans)]"
|
|
289
396
|
: "font-[family-name:var(--font-legal-serif)]";
|
|
290
397
|
|
|
398
|
+
// Story focus indicator — runtime-backed, not DOM-only
|
|
399
|
+
const storyKind = snapshot.activeStory.kind;
|
|
400
|
+
const storyFocusAttr = storyKind !== "main" ? storyKind : undefined;
|
|
401
|
+
|
|
402
|
+
// Table focus cue — add subtle ring when selection head is inside a table block
|
|
403
|
+
const tableFocusClass = (() => {
|
|
404
|
+
if (!surface || !snapshot.selection) return "";
|
|
405
|
+
const head = snapshot.selection.head;
|
|
406
|
+
const inTable = surface.blocks.some(
|
|
407
|
+
(b) => b.kind === "table" && head >= b.from && head <= b.to,
|
|
408
|
+
);
|
|
409
|
+
return inTable ? "prosemirror-table-focus" : "";
|
|
410
|
+
})();
|
|
411
|
+
|
|
412
|
+
const workspaceLabel = props.isPageWorkspace ? "Document page" : "Document canvas";
|
|
413
|
+
|
|
414
|
+
const selectionToolbarMeasurementKey = useMemo(
|
|
415
|
+
() => buildSelectionToolbarMeasurementKey(snapshot.selection, snapshot.activeStory),
|
|
416
|
+
[snapshot.activeStory, snapshot.selection],
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const emitSelectionToolbarAnchor = useCallback((): void => {
|
|
420
|
+
const callback = props.onSelectionToolbarAnchorChange;
|
|
421
|
+
if (!callback) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const nextAnchor = measureSelectionToolbarAnchor();
|
|
426
|
+
const previous = lastSelectionToolbarMeasurementRef.current;
|
|
427
|
+
if (
|
|
428
|
+
previous.key === selectionToolbarMeasurementKey &&
|
|
429
|
+
selectionToolbarAnchorsEqual(previous.anchor, nextAnchor)
|
|
430
|
+
) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
lastSelectionToolbarMeasurementRef.current = {
|
|
435
|
+
key: selectionToolbarMeasurementKey,
|
|
436
|
+
anchor: nextAnchor,
|
|
437
|
+
};
|
|
438
|
+
callback(nextAnchor);
|
|
439
|
+
}, [
|
|
440
|
+
props.onSelectionToolbarAnchorChange,
|
|
441
|
+
selectionToolbarMeasurementKey,
|
|
442
|
+
snapshot.activeStory,
|
|
443
|
+
snapshot.selection,
|
|
444
|
+
]);
|
|
445
|
+
|
|
446
|
+
const scheduleSelectionToolbarAnchorUpdate = useCallback((): void => {
|
|
447
|
+
const callback = props.onSelectionToolbarAnchorChange;
|
|
448
|
+
const mount = mountRef.current;
|
|
449
|
+
const win = mount?.ownerDocument.defaultView;
|
|
450
|
+
if (!callback || !win) {
|
|
451
|
+
emitSelectionToolbarAnchor();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (selectionToolbarFrameRef.current !== null) {
|
|
456
|
+
win.cancelAnimationFrame(selectionToolbarFrameRef.current);
|
|
457
|
+
selectionToolbarFrameRef.current = null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
selectionToolbarFrameRef.current = win.requestAnimationFrame(() => {
|
|
461
|
+
selectionToolbarFrameRef.current = null;
|
|
462
|
+
emitSelectionToolbarAnchor();
|
|
463
|
+
});
|
|
464
|
+
}, [emitSelectionToolbarAnchor, props.onSelectionToolbarAnchorChange]);
|
|
465
|
+
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
scheduleSelectionToolbarAnchorUpdate();
|
|
468
|
+
}, [
|
|
469
|
+
scheduleSelectionToolbarAnchorUpdate,
|
|
470
|
+
snapshot.revisionToken,
|
|
471
|
+
snapshot.selection,
|
|
472
|
+
snapshot.surface,
|
|
473
|
+
props.isPageWorkspace,
|
|
474
|
+
]);
|
|
475
|
+
|
|
476
|
+
useEffect(() => {
|
|
477
|
+
const mount = mountRef.current;
|
|
478
|
+
const callback = props.onSelectionToolbarAnchorChange;
|
|
479
|
+
if (!mount || !callback) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const updateAnchor = () => {
|
|
484
|
+
scheduleSelectionToolbarAnchorUpdate();
|
|
485
|
+
};
|
|
486
|
+
const scrollRoot = mount.closest<HTMLElement>("[data-wre-scroll-root='true']");
|
|
487
|
+
const win = mount.ownerDocument.defaultView;
|
|
488
|
+
const resizeObserver =
|
|
489
|
+
typeof ResizeObserver !== "undefined"
|
|
490
|
+
? new ResizeObserver(() => {
|
|
491
|
+
updateAnchor();
|
|
492
|
+
})
|
|
493
|
+
: null;
|
|
494
|
+
|
|
495
|
+
updateAnchor();
|
|
496
|
+
scrollRoot?.addEventListener("scroll", updateAnchor, { passive: true });
|
|
497
|
+
win?.addEventListener("resize", updateAnchor);
|
|
498
|
+
resizeObserver?.observe(mount);
|
|
499
|
+
if (scrollRoot) {
|
|
500
|
+
resizeObserver?.observe(scrollRoot);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return () => {
|
|
504
|
+
scrollRoot?.removeEventListener("scroll", updateAnchor);
|
|
505
|
+
win?.removeEventListener("resize", updateAnchor);
|
|
506
|
+
resizeObserver?.disconnect();
|
|
507
|
+
};
|
|
508
|
+
}, [
|
|
509
|
+
props.onSelectionToolbarAnchorChange,
|
|
510
|
+
scheduleSelectionToolbarAnchorUpdate,
|
|
511
|
+
snapshot.revisionToken,
|
|
512
|
+
snapshot.selection,
|
|
513
|
+
]);
|
|
514
|
+
|
|
515
|
+
useEffect(() => {
|
|
516
|
+
return () => {
|
|
517
|
+
lastSelectionToolbarMeasurementRef.current = {
|
|
518
|
+
key: null,
|
|
519
|
+
anchor: null,
|
|
520
|
+
};
|
|
521
|
+
props.onSelectionToolbarAnchorChange?.(null);
|
|
522
|
+
};
|
|
523
|
+
}, [props.onSelectionToolbarAnchorChange]);
|
|
524
|
+
|
|
291
525
|
return (
|
|
292
|
-
<section
|
|
526
|
+
<section
|
|
527
|
+
aria-label={workspaceLabel}
|
|
528
|
+
className="min-w-0"
|
|
529
|
+
data-active-story={storyFocusAttr}
|
|
530
|
+
data-workspace={props.isPageWorkspace ? "page" : "canvas"}
|
|
531
|
+
>
|
|
293
532
|
{/* ProseMirror mount point — document content including headings is editable */}
|
|
294
533
|
{surface ? (
|
|
295
534
|
<div
|
|
296
535
|
ref={mountRef}
|
|
297
536
|
role="textbox"
|
|
537
|
+
tabIndex={0}
|
|
298
538
|
aria-multiline="true"
|
|
299
|
-
className={`px-12 py-10 ${fontClass} text-[15px] text-primary leading-relaxed prosemirror-surface outline-none`}
|
|
300
|
-
onFocus={
|
|
539
|
+
className={`px-12 py-10 ${fontClass} text-[15px] text-primary leading-relaxed prosemirror-surface outline-none ${tableFocusClass}`}
|
|
540
|
+
onFocus={(event) => {
|
|
541
|
+
onFocus(event);
|
|
542
|
+
if (event.target === event.currentTarget) {
|
|
543
|
+
viewRef.current?.focus();
|
|
544
|
+
}
|
|
545
|
+
}}
|
|
301
546
|
onBlur={onBlur as unknown as React.FocusEventHandler<HTMLDivElement>}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const commentEl = target.closest?.("[data-comment-id]");
|
|
306
|
-
if (commentEl) {
|
|
307
|
-
const commentId = commentEl.getAttribute("data-comment-id");
|
|
308
|
-
if (commentId) {
|
|
309
|
-
props.onCommentActivated?.(commentId);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
547
|
+
onKeyDown={(event) => {
|
|
548
|
+
if (event.target !== event.currentTarget) {
|
|
549
|
+
return;
|
|
312
550
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
props.
|
|
318
|
-
|
|
551
|
+
|
|
552
|
+
switch (event.key) {
|
|
553
|
+
case "Backspace":
|
|
554
|
+
event.preventDefault();
|
|
555
|
+
props.onDeleteBackward?.();
|
|
556
|
+
return;
|
|
557
|
+
case "Delete":
|
|
558
|
+
event.preventDefault();
|
|
559
|
+
props.onDeleteForward?.();
|
|
560
|
+
return;
|
|
561
|
+
case "Enter":
|
|
562
|
+
event.preventDefault();
|
|
563
|
+
if (event.shiftKey) {
|
|
564
|
+
props.onInsertHardBreak?.();
|
|
565
|
+
} else {
|
|
566
|
+
props.onSplitParagraph?.();
|
|
567
|
+
}
|
|
568
|
+
return;
|
|
569
|
+
case "Tab":
|
|
570
|
+
event.preventDefault();
|
|
571
|
+
if (event.shiftKey) {
|
|
572
|
+
props.onOutdentTab?.();
|
|
573
|
+
} else {
|
|
574
|
+
props.onInsertTab?.();
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
default:
|
|
578
|
+
return;
|
|
319
579
|
}
|
|
320
580
|
}}
|
|
321
581
|
aria-label="Document surface"
|
|
582
|
+
data-wre-document-surface="true"
|
|
583
|
+
data-story-focus={storyFocusAttr}
|
|
322
584
|
/>
|
|
323
585
|
) : (
|
|
324
586
|
<div className="px-12 pb-10">
|
|
@@ -337,27 +599,82 @@ export const TwProseMirrorSurface = forwardRef<
|
|
|
337
599
|
) : null}
|
|
338
600
|
</section>
|
|
339
601
|
);
|
|
602
|
+
|
|
603
|
+
function measureSelectionToolbarAnchor(): SelectionToolbarAnchor | null {
|
|
604
|
+
const callback = props.onSelectionToolbarAnchorChange;
|
|
605
|
+
const view = viewRef.current;
|
|
606
|
+
const mount = mountRef.current;
|
|
607
|
+
const positionMap = positionMapRef.current;
|
|
608
|
+
const range = snapshot.selection.activeRange;
|
|
609
|
+
|
|
610
|
+
if (!callback || !view || !mount || !positionMap || snapshot.selection.isCollapsed || range.kind !== "range") {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const rootRect = mount.getBoundingClientRect();
|
|
615
|
+
if (rootRect.width <= 0 || rootRect.height <= 0) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
try {
|
|
620
|
+
const pmFrom = positionMap.runtimeToPm(range.from);
|
|
621
|
+
const pmTo = positionMap.runtimeToPm(range.to);
|
|
622
|
+
const startRect = view.coordsAtPos(pmFrom);
|
|
623
|
+
const endRect = view.coordsAtPos(pmTo);
|
|
624
|
+
const left = Math.max(rootRect.left, Math.min(startRect.left, endRect.left));
|
|
625
|
+
const right = Math.min(rootRect.right, Math.max(startRect.right, endRect.right));
|
|
626
|
+
const top = Math.max(rootRect.top, Math.min(startRect.top, endRect.top));
|
|
627
|
+
const bottom = Math.min(rootRect.bottom, Math.max(startRect.bottom, endRect.bottom));
|
|
628
|
+
|
|
629
|
+
if (
|
|
630
|
+
!Number.isFinite(left) ||
|
|
631
|
+
!Number.isFinite(right) ||
|
|
632
|
+
!Number.isFinite(top) ||
|
|
633
|
+
!Number.isFinite(bottom) ||
|
|
634
|
+
right <= left ||
|
|
635
|
+
bottom <= top ||
|
|
636
|
+
bottom < rootRect.top ||
|
|
637
|
+
top > rootRect.bottom
|
|
638
|
+
) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return { left, right, top, bottom };
|
|
643
|
+
} catch {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
340
647
|
});
|
|
341
648
|
|
|
342
|
-
function
|
|
343
|
-
results: Array<{ from: number; to: number }>,
|
|
344
|
-
toRuntimePosition: (position: number) => number,
|
|
649
|
+
function buildSelectionToolbarMeasurementKey(
|
|
345
650
|
selection: SelectionSnapshot,
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
651
|
+
activeStory: RuntimeRenderSnapshot["activeStory"],
|
|
652
|
+
): string | null {
|
|
653
|
+
if (selection.isCollapsed || selection.activeRange.kind !== "range") {
|
|
654
|
+
return null;
|
|
349
655
|
}
|
|
350
656
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const to = toRuntimePosition(result.to);
|
|
356
|
-
if (selectionFrom === selectionTo) {
|
|
357
|
-
return selectionFrom >= from && selectionFrom <= to;
|
|
358
|
-
}
|
|
359
|
-
return selectionFrom < to && selectionTo > from;
|
|
657
|
+
return JSON.stringify({
|
|
658
|
+
story: activeStory,
|
|
659
|
+
from: selection.activeRange.from,
|
|
660
|
+
to: selection.activeRange.to,
|
|
360
661
|
});
|
|
662
|
+
}
|
|
361
663
|
|
|
362
|
-
|
|
664
|
+
function selectionToolbarAnchorsEqual(
|
|
665
|
+
left: SelectionToolbarAnchor | null,
|
|
666
|
+
right: SelectionToolbarAnchor | null,
|
|
667
|
+
): boolean {
|
|
668
|
+
if (left === right) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
if (!left || !right) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
return (
|
|
675
|
+
left.left === right.left &&
|
|
676
|
+
left.right === right.right &&
|
|
677
|
+
left.top === right.top &&
|
|
678
|
+
left.bottom === right.bottom
|
|
679
|
+
);
|
|
363
680
|
}
|
|
@@ -350,7 +350,6 @@ export class TableCellNodeView {
|
|
|
350
350
|
*/
|
|
351
351
|
export const tableNodeViews: { [node: string]: NodeViewConstructor } = {
|
|
352
352
|
table: (node: PMNode) => new TableNodeView(node),
|
|
353
|
-
table_row: (node: PMNode) => new TableRowNodeView(node),
|
|
354
353
|
table_cell: (node: PMNode) => new TableCellNodeView(node),
|
|
355
354
|
table_header_cell: (node: PMNode) => new TableCellNodeView(node),
|
|
356
355
|
};
|
package/src/ui-tailwind/index.ts
CHANGED
|
@@ -16,8 +16,9 @@ export { TwRevisionSidebar } from "./review/tw-revision-sidebar";
|
|
|
16
16
|
export { TwHealthPanel } from "./review/tw-health-panel";
|
|
17
17
|
|
|
18
18
|
// Toolbar
|
|
19
|
-
export { TwToolbar, type TwToolbarProps
|
|
19
|
+
export { TwToolbar, type TwToolbarProps } from "./toolbar/tw-toolbar";
|
|
20
20
|
export { TwToolbarIconButton } from "./toolbar/tw-toolbar-icon-button";
|
|
21
|
+
export type { WorkspaceMode, ZoomLevel } from "../api/public-types";
|
|
21
22
|
|
|
22
23
|
// Status
|
|
23
24
|
export { TwStatusBar } from "./status/tw-status-bar";
|