@beyondwork/docx-react-component 1.0.43 → 1.0.46
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 +35 -1
- package/package.json +44 -32
- package/src/api/public-types.ts +156 -3
- package/src/core/commands/formatting-commands.ts +7 -1
- package/src/core/commands/index.ts +27 -2
- package/src/core/commands/text-commands.ts +59 -0
- package/src/core/selection/review-anchors.ts +131 -21
- package/src/index.ts +16 -1
- package/src/io/chart-preview-resolver.ts +281 -0
- package/src/io/docx-session.ts +21 -1
- package/src/io/export/build-app-properties-xml.ts +1 -1
- package/src/io/export/serialize-comments.ts +38 -9
- package/src/io/export/twip.ts +1 -1
- package/src/io/normalize/normalize-text.ts +33 -0
- package/src/io/ooxml/parse-comments.ts +0 -33
- package/src/io/ooxml/parse-complex-content.ts +14 -0
- package/src/io/ooxml/parse-main-document.ts +4 -0
- package/src/preservation/opaque-region.ts +5 -0
- package/src/review/store/comment-remapping.ts +2 -2
- package/src/runtime/document-runtime.ts +351 -25
- package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
- package/src/runtime/edit-dispatch/index.ts +2 -0
- package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
- package/src/runtime/editor-surface/capabilities.ts +411 -0
- package/src/runtime/event-refresh-hints.ts +1 -0
- package/src/runtime/layout/docx-font-loader.ts +30 -11
- package/src/runtime/layout/inert-layout-facet.ts +2 -0
- package/src/runtime/layout/layout-engine-instance.ts +46 -0
- package/src/runtime/layout/layout-engine-version.ts +41 -0
- package/src/runtime/layout/public-facet.ts +30 -0
- package/src/runtime/prerender/cache-envelope.ts +29 -0
- package/src/runtime/prerender/cache-key.ts +66 -0
- package/src/runtime/prerender/font-fingerprint.ts +17 -0
- package/src/runtime/prerender/graph-canonicalize.ts +121 -0
- package/src/runtime/prerender/indexeddb-cache.ts +184 -0
- package/src/runtime/prerender/prerender-document.ts +145 -0
- package/src/runtime/render/block-fragment-projection.ts +2 -0
- package/src/runtime/selection/post-edit-validator.ts +77 -0
- package/src/runtime/surface-projection.ts +35 -2
- package/src/ui/WordReviewEditor.tsx +75 -192
- package/src/ui/editor-runtime-boundary.ts +5 -1
- package/src/ui/runtime-shortcut-dispatch.ts +28 -68
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
- package/src/ui-tailwind/editor-surface/pm-schema.ts +18 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +23 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +38 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
- package/src/ui-tailwind/theme/editor-theme.css +47 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +131 -29
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
|
|
2
|
+
import type { SelectionSnapshot } from "../../core/state/editor-state.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Snap a selection to a valid position relative to the document.
|
|
6
|
+
*
|
|
7
|
+
* Pure function. O(1) on the identity (in-bounds) fast path — returns
|
|
8
|
+
* the SAME object reference when no change is needed. Callers should
|
|
9
|
+
* compare with `!==` to detect a snap (e.g. to decide whether to
|
|
10
|
+
* re-spread runtime state).
|
|
11
|
+
*
|
|
12
|
+
* Wired into the runtime snapshot-emit chokepoint
|
|
13
|
+
* (`applyTransactionToState` -> `cachedRenderSnapshot = refreshRenderSnapshot()`),
|
|
14
|
+
* so it runs once per transaction commit. Must NOT walk the document;
|
|
15
|
+
* the caller is responsible for passing a valid `maxOffset` (the
|
|
16
|
+
* POST-mutation `surface.storySize`, primed via
|
|
17
|
+
* `getCachedSurface(state.document, activeStory).storySize`).
|
|
18
|
+
*
|
|
19
|
+
* NodeAnchor invalidation is deferred until CanonicalDocumentEnvelope
|
|
20
|
+
* grows an O(1) node-by-id accessor. Until then, NodeAnchor selections
|
|
21
|
+
* are returned unchanged (identity).
|
|
22
|
+
*
|
|
23
|
+
* @param document The post-mutation canonical document. Currently
|
|
24
|
+
* unused except for the deferred NodeAnchor branch;
|
|
25
|
+
* the parameter is kept for API stability.
|
|
26
|
+
* @param selection The selection to validate.
|
|
27
|
+
* @param maxOffset The POST-mutation maximum story offset. Caller
|
|
28
|
+
* passes `getCachedSurface(state.document,
|
|
29
|
+
* activeStory).storySize` (which primes the cache
|
|
30
|
+
* that `refreshRenderSnapshot` reuses on its next
|
|
31
|
+
* call — no extra surface walk). The validator does
|
|
32
|
+
* NOT walk the document to compute this. Do NOT pass
|
|
33
|
+
* the pre-mutation snapshot's storySize: at end-of-doc
|
|
34
|
+
* inserts, the new selection legitimately exceeds the
|
|
35
|
+
* old bound and the validator would clamp the caret
|
|
36
|
+
* backward by one position per keystroke. Pass
|
|
37
|
+
* `Number.POSITIVE_INFINITY` to skip the upper-bound
|
|
38
|
+
* clamp.
|
|
39
|
+
*/
|
|
40
|
+
export function validateSelectionAgainstDocument(
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- reserved for deferred NodeAnchor lookup
|
|
42
|
+
document: CanonicalDocumentEnvelope,
|
|
43
|
+
selection: SelectionSnapshot,
|
|
44
|
+
maxOffset: number,
|
|
45
|
+
): SelectionSnapshot {
|
|
46
|
+
if (selection.activeRange.kind === "node") {
|
|
47
|
+
// Deferred: NodeAnchor invalidation requires an O(1) node-by-id
|
|
48
|
+
// accessor on CanonicalDocumentEnvelope. Until that lands, return
|
|
49
|
+
// identity so we never falsely invalidate a still-valid node anchor.
|
|
50
|
+
return selection;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const anchor = clamp(selection.anchor, 0, maxOffset);
|
|
54
|
+
const head = clamp(selection.head, 0, maxOffset);
|
|
55
|
+
|
|
56
|
+
if (anchor === selection.anchor && head === selection.head) {
|
|
57
|
+
// Identity fast path — no allocation, same reference returned.
|
|
58
|
+
return selection;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const range = { from: Math.min(anchor, head), to: Math.max(anchor, head) };
|
|
62
|
+
const assoc =
|
|
63
|
+
selection.activeRange.kind === "range"
|
|
64
|
+
? selection.activeRange.assoc
|
|
65
|
+
: { start: 1 as const, end: 1 as const };
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
anchor,
|
|
69
|
+
head,
|
|
70
|
+
isCollapsed: anchor === head,
|
|
71
|
+
activeRange: { kind: "range", range, assoc },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clamp(n: number, lo: number, hi: number): number {
|
|
76
|
+
return n < lo ? lo : n > hi ? hi : n;
|
|
77
|
+
}
|
|
@@ -68,11 +68,17 @@ interface ParagraphAccumulator {
|
|
|
68
68
|
segments: SurfaceInlineSegment[];
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export interface SurfaceProjectionOptions {
|
|
72
|
+
viewportBlockRange?: { start: number; end: number } | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
export function createEditorSurfaceSnapshot(
|
|
72
76
|
document: CanonicalDocumentEnvelope,
|
|
73
77
|
_selection: SelectionSnapshot,
|
|
74
78
|
activeStory: EditorStoryTarget = { kind: "main" },
|
|
79
|
+
options: SurfaceProjectionOptions = {},
|
|
75
80
|
): EditorSurfaceSnapshot {
|
|
81
|
+
const viewportBlockRange = options.viewportBlockRange ?? null;
|
|
76
82
|
const root = normalizeDocumentRoot({
|
|
77
83
|
type: "doc",
|
|
78
84
|
children: [...getStoryBlocks(document, activeStory)],
|
|
@@ -99,8 +105,34 @@ export function createEditorSurfaceSnapshot(
|
|
|
99
105
|
numberingPrefixResolver,
|
|
100
106
|
activeStory.kind !== "main",
|
|
101
107
|
);
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
const isInViewport =
|
|
109
|
+
viewportBlockRange === null ||
|
|
110
|
+
(index >= viewportBlockRange.start && index < viewportBlockRange.end);
|
|
111
|
+
|
|
112
|
+
if (isInViewport) {
|
|
113
|
+
blocks.push(surfaceBlock.block);
|
|
114
|
+
lockedFragmentIds.push(...surfaceBlock.lockedFragmentIds);
|
|
115
|
+
} else {
|
|
116
|
+
// Replace with size-preserving placeholder. from/to track the SAME
|
|
117
|
+
// position range as the real block, so selection and anchor stability
|
|
118
|
+
// outside the viewport is preserved.
|
|
119
|
+
const placeholderSize = surfaceBlock.nextCursor - cursor;
|
|
120
|
+
const placeholderBlockId = `placeholder-culled-${index}`;
|
|
121
|
+
blocks.push({
|
|
122
|
+
blockId: placeholderBlockId,
|
|
123
|
+
kind: "opaque_block",
|
|
124
|
+
from: cursor,
|
|
125
|
+
to: surfaceBlock.nextCursor,
|
|
126
|
+
fragmentId: placeholderBlockId,
|
|
127
|
+
warningId: placeholderBlockId,
|
|
128
|
+
label: "",
|
|
129
|
+
detail: "",
|
|
130
|
+
placeholderSize,
|
|
131
|
+
state: "placeholder-culled",
|
|
132
|
+
} as SurfaceBlockSnapshot);
|
|
133
|
+
// Do NOT push lockedFragmentIds — placeholder has no real fragment.
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
cursor = surfaceBlock.nextCursor;
|
|
105
137
|
if (index < root.children.length - 1 && root.children[index + 1]?.type === "paragraph") {
|
|
106
138
|
cursor += 1;
|
|
@@ -115,6 +147,7 @@ export function createEditorSurfaceSnapshot(
|
|
|
115
147
|
blocks,
|
|
116
148
|
lockedFragmentIds,
|
|
117
149
|
secondaryStories,
|
|
150
|
+
viewportBlockRange,
|
|
118
151
|
};
|
|
119
152
|
}
|
|
120
153
|
|
|
@@ -97,14 +97,15 @@ import {
|
|
|
97
97
|
} from "../core/commands/style-commands.ts";
|
|
98
98
|
import {
|
|
99
99
|
continueNumbering as continueListNumbering,
|
|
100
|
-
backspaceAtListStart,
|
|
101
|
-
indentListItems,
|
|
102
|
-
outdentListItems,
|
|
103
100
|
restartNumbering as restartListNumbering,
|
|
104
|
-
splitListParagraph,
|
|
105
101
|
toggleBulletedList,
|
|
106
102
|
toggleNumberedList,
|
|
107
103
|
} from "../core/commands/list-commands.ts";
|
|
104
|
+
import {
|
|
105
|
+
dispatchTextCommand,
|
|
106
|
+
type DispatchContext,
|
|
107
|
+
type DispatchTextCommand,
|
|
108
|
+
} from "../runtime/edit-dispatch/index.ts";
|
|
108
109
|
import {
|
|
109
110
|
resolveActiveParagraphIndex,
|
|
110
111
|
setActiveParagraphIndentation,
|
|
@@ -242,6 +243,12 @@ const BROWSER_SAFE_PREVIEW_TYPES = new Set([
|
|
|
242
243
|
"image/gif",
|
|
243
244
|
"image/webp",
|
|
244
245
|
"image/bmp",
|
|
246
|
+
// SVG is served through `<img src="data:image/svg+xml;base64,...">` by
|
|
247
|
+
// `createImageDataUrl`. Chromium sandboxes SVGs loaded via <img> — scripts
|
|
248
|
+
// don't execute, external references are blocked, XSS surface matches PNG.
|
|
249
|
+
// Needed for docs/plans/lane-5-charts.md Stage 0B synthesized chart previews and
|
|
250
|
+
// any host that ships .svg inside `word/media/` as a logo or figure.
|
|
251
|
+
"image/svg+xml",
|
|
245
252
|
]);
|
|
246
253
|
|
|
247
254
|
const ACCESSIBLE_REGION_ORDER = [
|
|
@@ -1001,16 +1008,9 @@ export function __createWordReviewEditorRefBridge(
|
|
|
1001
1008
|
|
|
1002
1009
|
export function __applyRuntimeTextCommand(
|
|
1003
1010
|
runtime: WordReviewEditorRuntime,
|
|
1004
|
-
command:
|
|
1005
|
-
| { type: "insert-text"; text: string }
|
|
1006
|
-
| { type: "delete-backward" }
|
|
1007
|
-
| { type: "delete-forward" }
|
|
1008
|
-
| { type: "insert-tab" }
|
|
1009
|
-
| { type: "outdent-tab" }
|
|
1010
|
-
| { type: "insert-hard-break" }
|
|
1011
|
-
| { type: "split-paragraph" },
|
|
1011
|
+
command: DispatchTextCommand,
|
|
1012
1012
|
): void {
|
|
1013
|
-
|
|
1013
|
+
dispatchTextCommand(runtime, command, DISPATCH_CONTEXT);
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
1016
|
export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditorProps>(
|
|
@@ -1039,6 +1039,9 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
1039
1039
|
onWarning,
|
|
1040
1040
|
onReviewSidebarTrackedChanges,
|
|
1041
1041
|
onReviewSidebarComments,
|
|
1042
|
+
onFindRequested,
|
|
1043
|
+
onPrintRequested,
|
|
1044
|
+
onZoomRequested,
|
|
1042
1045
|
readOnly = false,
|
|
1043
1046
|
reviewMode = "review",
|
|
1044
1047
|
suggestionsEnabled = false,
|
|
@@ -2141,7 +2144,7 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2141
2144
|
|
|
2142
2145
|
function addReviewComment(): string | null {
|
|
2143
2146
|
try {
|
|
2144
|
-
const commentId = activeRuntime.addComment({
|
|
2147
|
+
const { commentId } = activeRuntime.addComment({
|
|
2145
2148
|
anchor: snapshot.selection.activeRange,
|
|
2146
2149
|
body: "",
|
|
2147
2150
|
authorId: currentUser.userId,
|
|
@@ -2570,7 +2573,46 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2570
2573
|
},
|
|
2571
2574
|
);
|
|
2572
2575
|
|
|
2573
|
-
if (shortcut.kind === "none"
|
|
2576
|
+
if (shortcut.kind === "none") {
|
|
2577
|
+
return;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
if (shortcut.kind === "delegate") {
|
|
2581
|
+
// Host-delegated shortcuts: if the host has wired a typed
|
|
2582
|
+
// callback we call it and suppress the browser default; if
|
|
2583
|
+
// not, the event falls through to the browser (Ctrl+F opens
|
|
2584
|
+
// Find, Ctrl+Plus zooms, etc.) — matching the legacy behavior.
|
|
2585
|
+
let handled = false;
|
|
2586
|
+
if (shortcut.shortcut === "find" && onFindRequested) {
|
|
2587
|
+
// selectionText is intentionally empty — hosts that need the
|
|
2588
|
+
// selected text already receive it via the selection_changed
|
|
2589
|
+
// event + canonicalDocument they have via onEvent listeners.
|
|
2590
|
+
// The range is the load-bearing field so host Find panels can
|
|
2591
|
+
// scope their search to the selection or pre-populate from it.
|
|
2592
|
+
onFindRequested({
|
|
2593
|
+
selectionText: "",
|
|
2594
|
+
selectionRange: snapshot.selection,
|
|
2595
|
+
});
|
|
2596
|
+
handled = true;
|
|
2597
|
+
} else if (shortcut.shortcut === "print" && onPrintRequested) {
|
|
2598
|
+
onPrintRequested();
|
|
2599
|
+
handled = true;
|
|
2600
|
+
} else if (
|
|
2601
|
+
(shortcut.shortcut === "zoom-in" ||
|
|
2602
|
+
shortcut.shortcut === "zoom-out" ||
|
|
2603
|
+
shortcut.shortcut === "zoom-reset") &&
|
|
2604
|
+
onZoomRequested
|
|
2605
|
+
) {
|
|
2606
|
+
const direction =
|
|
2607
|
+
shortcut.shortcut === "zoom-in" ? "in" :
|
|
2608
|
+
shortcut.shortcut === "zoom-out" ? "out" : "reset";
|
|
2609
|
+
onZoomRequested(direction);
|
|
2610
|
+
handled = true;
|
|
2611
|
+
}
|
|
2612
|
+
if (handled) {
|
|
2613
|
+
event.preventDefault();
|
|
2614
|
+
event.stopPropagation();
|
|
2615
|
+
}
|
|
2574
2616
|
return;
|
|
2575
2617
|
}
|
|
2576
2618
|
|
|
@@ -2636,13 +2678,13 @@ export const WordReviewEditor = forwardRef<WordReviewEditorRef, WordReviewEditor
|
|
|
2636
2678
|
onFocus: handleSurfaceFocus,
|
|
2637
2679
|
onBlur: handleSurfaceBlur,
|
|
2638
2680
|
onSelectionChange: dispatchSelection,
|
|
2639
|
-
onInsertText: (text: string) =>
|
|
2640
|
-
onDeleteBackward: () =>
|
|
2641
|
-
onDeleteForward: () =>
|
|
2642
|
-
onInsertTab: () =>
|
|
2643
|
-
onOutdentTab: () =>
|
|
2644
|
-
onInsertHardBreak: () =>
|
|
2645
|
-
onSplitParagraph: () =>
|
|
2681
|
+
onInsertText: (text: string) => dispatchTextCommand(activeRuntime, { type: "insert-text", text }, DISPATCH_CONTEXT),
|
|
2682
|
+
onDeleteBackward: () => dispatchTextCommand(activeRuntime, { type: "delete-backward" }, DISPATCH_CONTEXT),
|
|
2683
|
+
onDeleteForward: () => dispatchTextCommand(activeRuntime, { type: "delete-forward" }, DISPATCH_CONTEXT),
|
|
2684
|
+
onInsertTab: () => dispatchTextCommand(activeRuntime, { type: "insert-tab" }, DISPATCH_CONTEXT),
|
|
2685
|
+
onOutdentTab: () => dispatchTextCommand(activeRuntime, { type: "outdent-tab" }, DISPATCH_CONTEXT),
|
|
2686
|
+
onInsertHardBreak: () => dispatchTextCommand(activeRuntime, { type: "insert-hard-break" }, DISPATCH_CONTEXT),
|
|
2687
|
+
onSplitParagraph: () => dispatchTextCommand(activeRuntime, { type: "split-paragraph" }, DISPATCH_CONTEXT),
|
|
2646
2688
|
onUndo: () => activeRuntime.undo(),
|
|
2647
2689
|
onRedo: () => activeRuntime.redo(),
|
|
2648
2690
|
onBlockedInput: (command: "paste" | "drop", message: string) =>
|
|
@@ -3294,7 +3336,11 @@ function applyRuntimeListToggle(
|
|
|
3294
3336
|
dispatchStoryMutationResult(
|
|
3295
3337
|
runtime,
|
|
3296
3338
|
context,
|
|
3297
|
-
|
|
3339
|
+
{
|
|
3340
|
+
changed: result.affectedParagraphIndexes.length > 0,
|
|
3341
|
+
document: result.document,
|
|
3342
|
+
selection: toRuntimeSelectionSnapshot(context.localSnapshot.selection),
|
|
3343
|
+
},
|
|
3298
3344
|
context.timestamp,
|
|
3299
3345
|
);
|
|
3300
3346
|
}
|
|
@@ -4418,175 +4464,12 @@ function buildTablesFacet(
|
|
|
4418
4464
|
|
|
4419
4465
|
export { buildTablesFacet as __buildTablesFacet };
|
|
4420
4466
|
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
| { type: "insert-tab" }
|
|
4428
|
-
| { type: "outdent-tab" }
|
|
4429
|
-
| { type: "insert-hard-break" }
|
|
4430
|
-
| { type: "split-paragraph" },
|
|
4431
|
-
): void {
|
|
4432
|
-
const snapshot = runtime.getRenderSnapshot();
|
|
4433
|
-
const context = getStoryMutationContext(runtime, getMountedTextCommandName(command));
|
|
4434
|
-
if (!context) {
|
|
4435
|
-
return;
|
|
4436
|
-
}
|
|
4437
|
-
|
|
4438
|
-
const effectiveSelectionMode = runtime.getInteractionGuardSnapshot().effectiveMode;
|
|
4439
|
-
const listAwareResult = applyListAwareTextCommand(context, command);
|
|
4440
|
-
if (effectiveSelectionMode === "suggest" && listAwareResult) {
|
|
4441
|
-
runtime.emitBlockedCommand(getMountedTextCommandName(command), [{
|
|
4442
|
-
code: "suggesting_unsupported",
|
|
4443
|
-
message: "List structure changes are not supported in suggesting mode.",
|
|
4444
|
-
}]);
|
|
4445
|
-
return;
|
|
4446
|
-
}
|
|
4447
|
-
|
|
4448
|
-
if (listAwareResult) {
|
|
4449
|
-
dispatchStoryMutationResult(runtime, context, listAwareResult, context.timestamp);
|
|
4450
|
-
return;
|
|
4451
|
-
}
|
|
4452
|
-
|
|
4453
|
-
switch (command.type) {
|
|
4454
|
-
case "insert-text":
|
|
4455
|
-
runtime.applyActiveStoryTextCommand({ type: "text.insert", text: command.text });
|
|
4456
|
-
return;
|
|
4457
|
-
case "delete-backward":
|
|
4458
|
-
runtime.applyActiveStoryTextCommand({ type: "text.delete-backward" });
|
|
4459
|
-
return;
|
|
4460
|
-
case "delete-forward":
|
|
4461
|
-
runtime.applyActiveStoryTextCommand({ type: "text.delete-forward" });
|
|
4462
|
-
return;
|
|
4463
|
-
case "insert-tab":
|
|
4464
|
-
runtime.applyActiveStoryTextCommand({ type: "text.insert-tab" });
|
|
4465
|
-
return;
|
|
4466
|
-
case "outdent-tab":
|
|
4467
|
-
return;
|
|
4468
|
-
case "insert-hard-break":
|
|
4469
|
-
runtime.applyActiveStoryTextCommand({ type: "text.insert-hard-break" });
|
|
4470
|
-
return;
|
|
4471
|
-
case "split-paragraph":
|
|
4472
|
-
runtime.applyActiveStoryTextCommand({ type: "paragraph.split" });
|
|
4473
|
-
return;
|
|
4474
|
-
}
|
|
4475
|
-
}
|
|
4476
|
-
|
|
4477
|
-
function getMountedTextCommandName(
|
|
4478
|
-
command:
|
|
4479
|
-
| { type: "insert-text"; text: string }
|
|
4480
|
-
| { type: "delete-backward" }
|
|
4481
|
-
| { type: "delete-forward" }
|
|
4482
|
-
| { type: "insert-tab" }
|
|
4483
|
-
| { type: "outdent-tab" }
|
|
4484
|
-
| { type: "insert-hard-break" }
|
|
4485
|
-
| { type: "split-paragraph" },
|
|
4486
|
-
): string {
|
|
4487
|
-
switch (command.type) {
|
|
4488
|
-
case "insert-text":
|
|
4489
|
-
return "text.insert";
|
|
4490
|
-
case "delete-backward":
|
|
4491
|
-
return "text.delete-backward";
|
|
4492
|
-
case "delete-forward":
|
|
4493
|
-
return "text.delete-forward";
|
|
4494
|
-
case "insert-tab":
|
|
4495
|
-
case "outdent-tab":
|
|
4496
|
-
return "text.insert-tab";
|
|
4497
|
-
case "insert-hard-break":
|
|
4498
|
-
return "text.insert-hard-break";
|
|
4499
|
-
case "split-paragraph":
|
|
4500
|
-
return "paragraph.split";
|
|
4501
|
-
}
|
|
4502
|
-
}
|
|
4503
|
-
|
|
4504
|
-
function applyListAwareTextCommand(
|
|
4505
|
-
context: NonNullable<ReturnType<typeof getStoryMutationContext>>,
|
|
4506
|
-
command:
|
|
4507
|
-
| { type: "insert-text"; text: string }
|
|
4508
|
-
| { type: "delete-backward" }
|
|
4509
|
-
| { type: "delete-forward" }
|
|
4510
|
-
| { type: "insert-tab" }
|
|
4511
|
-
| { type: "outdent-tab" }
|
|
4512
|
-
| { type: "insert-hard-break" }
|
|
4513
|
-
| { type: "split-paragraph" },
|
|
4514
|
-
): {
|
|
4515
|
-
changed: boolean;
|
|
4516
|
-
document: EditorSessionState["canonicalDocument"];
|
|
4517
|
-
selection: InternalSelectionSnapshot;
|
|
4518
|
-
} | null {
|
|
4519
|
-
const paragraphContext = resolveActiveParagraphContext(context.localSnapshot);
|
|
4520
|
-
if (!paragraphContext?.paragraph.numbering) {
|
|
4521
|
-
return null;
|
|
4522
|
-
}
|
|
4523
|
-
|
|
4524
|
-
switch (command.type) {
|
|
4525
|
-
case "insert-tab": {
|
|
4526
|
-
const result = indentListItems(
|
|
4527
|
-
context.localDocument,
|
|
4528
|
-
[paragraphContext.paragraphIndex],
|
|
4529
|
-
{ timestamp: context.timestamp },
|
|
4530
|
-
);
|
|
4531
|
-
return createListMutationResult(result, context.localSnapshot.selection);
|
|
4532
|
-
}
|
|
4533
|
-
case "outdent-tab": {
|
|
4534
|
-
const result = outdentListItems(
|
|
4535
|
-
context.localDocument,
|
|
4536
|
-
[paragraphContext.paragraphIndex],
|
|
4537
|
-
{ timestamp: context.timestamp },
|
|
4538
|
-
);
|
|
4539
|
-
return createListMutationResult(result, context.localSnapshot.selection);
|
|
4540
|
-
}
|
|
4541
|
-
case "delete-backward": {
|
|
4542
|
-
if (!paragraphContext.atParagraphStart || !context.localSnapshot.selection.isCollapsed) {
|
|
4543
|
-
return null;
|
|
4544
|
-
}
|
|
4545
|
-
const result = backspaceAtListStart(
|
|
4546
|
-
context.localDocument,
|
|
4547
|
-
paragraphContext.paragraphIndex,
|
|
4548
|
-
{ timestamp: context.timestamp },
|
|
4549
|
-
);
|
|
4550
|
-
return result.handled
|
|
4551
|
-
? createListMutationResult(result, context.localSnapshot.selection)
|
|
4552
|
-
: null;
|
|
4553
|
-
}
|
|
4554
|
-
case "split-paragraph": {
|
|
4555
|
-
if (!context.localSnapshot.selection.isCollapsed || !paragraphContext.isEmpty) {
|
|
4556
|
-
return null;
|
|
4557
|
-
}
|
|
4558
|
-
const result = splitListParagraph(
|
|
4559
|
-
context.localDocument,
|
|
4560
|
-
paragraphContext.paragraphIndex,
|
|
4561
|
-
true,
|
|
4562
|
-
{ timestamp: context.timestamp },
|
|
4563
|
-
);
|
|
4564
|
-
return result.action === "split"
|
|
4565
|
-
? null
|
|
4566
|
-
: createListMutationResult(result, context.localSnapshot.selection);
|
|
4567
|
-
}
|
|
4568
|
-
default:
|
|
4569
|
-
return null;
|
|
4570
|
-
}
|
|
4571
|
-
}
|
|
4572
|
-
|
|
4573
|
-
function createListMutationResult(
|
|
4574
|
-
result: {
|
|
4575
|
-
document: EditorSessionState["canonicalDocument"];
|
|
4576
|
-
affectedParagraphIndexes: number[];
|
|
4577
|
-
},
|
|
4578
|
-
selection: RuntimeRenderSnapshot["selection"],
|
|
4579
|
-
): {
|
|
4580
|
-
changed: boolean;
|
|
4581
|
-
document: EditorSessionState["canonicalDocument"];
|
|
4582
|
-
selection: InternalSelectionSnapshot;
|
|
4583
|
-
} {
|
|
4584
|
-
return {
|
|
4585
|
-
changed: result.affectedParagraphIndexes.length > 0,
|
|
4586
|
-
document: result.document,
|
|
4587
|
-
selection: toRuntimeSelectionSnapshot(selection),
|
|
4588
|
-
};
|
|
4589
|
-
}
|
|
4467
|
+
const DISPATCH_CONTEXT: DispatchContext = {
|
|
4468
|
+
getStoryMutationContext,
|
|
4469
|
+
dispatchStoryMutationResult,
|
|
4470
|
+
resolveActiveParagraphContext,
|
|
4471
|
+
toRuntimeSelectionSnapshot,
|
|
4472
|
+
};
|
|
4590
4473
|
|
|
4591
4474
|
function resolveActiveParagraphContext(
|
|
4592
4475
|
snapshot: Pick<RuntimeRenderSnapshot, "surface" | "selection">,
|
|
@@ -896,7 +896,9 @@ function createLoadingRuntimeBridge(input: {
|
|
|
896
896
|
openComment: () => undefined,
|
|
897
897
|
resolveComment: () => undefined,
|
|
898
898
|
reopenComment: () => undefined,
|
|
899
|
-
addCommentReply: () =>
|
|
899
|
+
addCommentReply: () => {
|
|
900
|
+
throw createLoadingBoundaryError(input.snapshot.documentId, "comment");
|
|
901
|
+
},
|
|
900
902
|
editCommentBody: () => undefined,
|
|
901
903
|
acceptChange: () => undefined,
|
|
902
904
|
rejectChange: () => undefined,
|
|
@@ -1007,6 +1009,8 @@ function createLoadingRuntimeBridge(input: {
|
|
|
1007
1009
|
editorStateChannel: createEditorStateChannel(),
|
|
1008
1010
|
getPerfCountersSnapshot: () => ({}),
|
|
1009
1011
|
resetPerfCounters: () => undefined,
|
|
1012
|
+
setVisibleBlockRange: () => undefined,
|
|
1013
|
+
requestViewportRefresh: () => undefined,
|
|
1010
1014
|
};
|
|
1011
1015
|
}
|
|
1012
1016
|
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
StyleCatalogSnapshot,
|
|
3
3
|
WorkflowBlockedCommandReason,
|
|
4
4
|
} from "../api/public-types.ts";
|
|
5
|
+
import { CAPABILITY_BY_ID } from "../runtime/editor-surface/capabilities.ts";
|
|
5
6
|
|
|
6
7
|
export interface ShortcutKeyInput {
|
|
7
8
|
key: string;
|
|
@@ -113,87 +114,35 @@ export function resolveShellShortcut(
|
|
|
113
114
|
return { kind: "delegate", shortcut: "zoom-reset" };
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
if (isPasteTextOnlyShortcut(input, key)) {
|
|
117
|
-
return {
|
|
118
|
-
kind: "block",
|
|
119
|
-
command: "pasteTextOnly",
|
|
120
|
-
reason: createUnsupportedShortcutReason(
|
|
121
|
-
"Plain-text paste is not supported in the mounted editor yet.",
|
|
122
|
-
),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
117
|
if (isReplaceShortcut(input, key)) {
|
|
127
|
-
return
|
|
128
|
-
kind: "block",
|
|
129
|
-
command: "replaceText",
|
|
130
|
-
reason: createUnsupportedShortcutReason(
|
|
131
|
-
"Replace shortcuts are not supported in the mounted editor yet.",
|
|
132
|
-
),
|
|
133
|
-
};
|
|
118
|
+
return resolveBlockedCapability("replaceText");
|
|
134
119
|
}
|
|
135
120
|
|
|
136
121
|
if (
|
|
137
122
|
isGoToShortcut(input, key) ||
|
|
138
123
|
(key === "f5" && !input.shiftKey)
|
|
139
124
|
) {
|
|
140
|
-
return
|
|
141
|
-
kind: "block",
|
|
142
|
-
command: "goTo",
|
|
143
|
-
reason: createUnsupportedShortcutReason(
|
|
144
|
-
"Go To shortcuts are not supported in the mounted editor yet.",
|
|
145
|
-
),
|
|
146
|
-
};
|
|
125
|
+
return resolveBlockedCapability("goTo");
|
|
147
126
|
}
|
|
148
127
|
|
|
149
128
|
if (isModShiftShortcut(input, key, "e")) {
|
|
150
|
-
return
|
|
151
|
-
kind: "block",
|
|
152
|
-
command: "toggleTrackChanges",
|
|
153
|
-
reason: createUnsupportedShortcutReason(
|
|
154
|
-
"Track changes authoring shortcuts are not supported in the mounted editor.",
|
|
155
|
-
),
|
|
156
|
-
};
|
|
129
|
+
return resolveBlockedCapability("toggleTrackChanges");
|
|
157
130
|
}
|
|
158
131
|
|
|
159
132
|
if (key === "f7" && !input.shiftKey) {
|
|
160
|
-
return
|
|
161
|
-
kind: "block",
|
|
162
|
-
command: "checkSpelling",
|
|
163
|
-
reason: createUnsupportedShortcutReason(
|
|
164
|
-
"Spelling shortcuts are not supported in the mounted editor.",
|
|
165
|
-
),
|
|
166
|
-
};
|
|
133
|
+
return resolveBlockedCapability("checkSpelling");
|
|
167
134
|
}
|
|
168
135
|
|
|
169
136
|
if (key === "f7" && input.shiftKey) {
|
|
170
|
-
return
|
|
171
|
-
kind: "block",
|
|
172
|
-
command: "openThesaurus",
|
|
173
|
-
reason: createUnsupportedShortcutReason(
|
|
174
|
-
"Thesaurus shortcuts are not supported in the mounted editor.",
|
|
175
|
-
),
|
|
176
|
-
};
|
|
137
|
+
return resolveBlockedCapability("openThesaurus");
|
|
177
138
|
}
|
|
178
139
|
|
|
179
140
|
if (key === "f8") {
|
|
180
|
-
return
|
|
181
|
-
kind: "block",
|
|
182
|
-
command: "extendSelection",
|
|
183
|
-
reason: createUnsupportedShortcutReason(
|
|
184
|
-
"Extend-selection shortcuts are not supported in the mounted editor.",
|
|
185
|
-
),
|
|
186
|
-
};
|
|
141
|
+
return resolveBlockedCapability("extendSelection");
|
|
187
142
|
}
|
|
188
143
|
|
|
189
144
|
if (key === "f5" && input.shiftKey) {
|
|
190
|
-
return
|
|
191
|
-
kind: "block",
|
|
192
|
-
command: "lastEdit",
|
|
193
|
-
reason: createUnsupportedShortcutReason(
|
|
194
|
-
"Last-edit shortcuts are not supported in the mounted editor.",
|
|
195
|
-
),
|
|
196
|
-
};
|
|
145
|
+
return resolveBlockedCapability("lastEdit");
|
|
197
146
|
}
|
|
198
147
|
|
|
199
148
|
return { kind: "none" };
|
|
@@ -262,10 +211,27 @@ export function resolveHeadingShortcutStyleId(
|
|
|
262
211
|
return null;
|
|
263
212
|
}
|
|
264
213
|
|
|
265
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Look up a `blocked` capability by id and produce the matching
|
|
216
|
+
* `ShellShortcutResolution`. The capability's `blockReason` becomes
|
|
217
|
+
* the dispatcher's returned reason, so editing the user-facing
|
|
218
|
+
* message in `capabilities.ts` is enough to update every shell-layer
|
|
219
|
+
* block path — no dispatcher change needed. Throws if the id is not
|
|
220
|
+
* registered as a blocked capability; the C.1 anchor-check test
|
|
221
|
+
* guarantees every id this file references is present.
|
|
222
|
+
*/
|
|
223
|
+
function resolveBlockedCapability(id: string): ShellShortcutResolution {
|
|
224
|
+
const cap = CAPABILITY_BY_ID.get(id);
|
|
225
|
+
if (!cap || cap.kind !== "blocked" || !cap.blockReason) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`resolveShellShortcut: capability ${id} is not registered as a blocked entry. ` +
|
|
228
|
+
"See src/runtime/editor-surface/capabilities.ts.",
|
|
229
|
+
);
|
|
230
|
+
}
|
|
266
231
|
return {
|
|
267
|
-
|
|
268
|
-
|
|
232
|
+
kind: "block",
|
|
233
|
+
command: id,
|
|
234
|
+
reason: cap.blockReason as WorkflowBlockedCommandReason,
|
|
269
235
|
};
|
|
270
236
|
}
|
|
271
237
|
|
|
@@ -357,9 +323,3 @@ function isZoomOutShortcut(input: ShortcutKeyInput, key: string): boolean {
|
|
|
357
323
|
(key === "-" || key === "_" || key === "subtract");
|
|
358
324
|
}
|
|
359
325
|
|
|
360
|
-
function isPasteTextOnlyShortcut(input: ShortcutKeyInput, key: string): boolean {
|
|
361
|
-
return key === "v" &&
|
|
362
|
-
Boolean(input.ctrlKey || input.metaKey) &&
|
|
363
|
-
!input.altKey &&
|
|
364
|
-
Boolean(input.shiftKey);
|
|
365
|
-
}
|