@beyondwork/docx-react-component 1.0.63 → 1.0.65
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
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.65",
|
|
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": [
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-selection remap for remote-origin replays.
|
|
3
|
+
*
|
|
4
|
+
* When a remote peer's command arrives, the runtime replays that command
|
|
5
|
+
* through `executeEditorCommand`. The resulting transaction carries a
|
|
6
|
+
* `nextState.selection` which represents the AUTHOR's reducer result —
|
|
7
|
+
* where the remote peer's caret landed after their own insert / delete.
|
|
8
|
+
* That value is useful for deterministic replay of the mutation but it is
|
|
9
|
+
* NOT the correct local caret for a passive viewer who did not initiate
|
|
10
|
+
* the edit.
|
|
11
|
+
*
|
|
12
|
+
* Without this helper, a passive viewer's caret would jump to wherever
|
|
13
|
+
* the remote author's caret ended up — a visible, confusing artefact
|
|
14
|
+
* reported by users as "my cursor moves when someone else types".
|
|
15
|
+
*
|
|
16
|
+
* Semantics (matches Yjs / y-prosemirror convention for local cursor
|
|
17
|
+
* preservation across remote edits):
|
|
18
|
+
*
|
|
19
|
+
* - Remote insert strictly BEFORE the local caret shifts the caret
|
|
20
|
+
* forward by the insert size. The caret still points at the same
|
|
21
|
+
* logical character.
|
|
22
|
+
* - Remote insert strictly AFTER the local caret leaves it unchanged.
|
|
23
|
+
* - Remote insert AT the caret respects forward association for the
|
|
24
|
+
* head (+1) and backward association for the anchor (-1) — inserts
|
|
25
|
+
* appear before the caret on the head side, matching PM / Yjs.
|
|
26
|
+
* - Remote delete entirely BEFORE the caret shifts it backward by the
|
|
27
|
+
* deleted span.
|
|
28
|
+
* - Remote delete containing the caret collapses it to the start of
|
|
29
|
+
* the deleted range (the character it pointed at is gone).
|
|
30
|
+
* - Remote delete of a range selection collapses the selection to the
|
|
31
|
+
* mapped boundary; we never leave the live local caret in a
|
|
32
|
+
* `detached` state — detached is a review-anchor concept, not a
|
|
33
|
+
* live caret concept.
|
|
34
|
+
*
|
|
35
|
+
* This helper is a pure function over the internal `SelectionSnapshot`
|
|
36
|
+
* shape (runtime-facing, NOT the public projection). Cross-story
|
|
37
|
+
* replays are filtered out by the caller (`applyRemoteCommand` /
|
|
38
|
+
* `applyRemoteCommandBatch`); this helper assumes the mapping is
|
|
39
|
+
* applicable to the local selection's story.
|
|
40
|
+
*
|
|
41
|
+
* Related deferred work: per-story anchor persistence across reload
|
|
42
|
+
* (Y.RelativePosition against a Y.Text mirror) is intentionally NOT
|
|
43
|
+
* done here — the current `TransactionMapping` substrate is sufficient
|
|
44
|
+
* for live-session caret stability, which is what this bug requires.
|
|
45
|
+
* See `docs/plans/lane-4-collab-clm-vallor.md` P11 sub-bullet for the
|
|
46
|
+
* follow-up.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
type SelectionSnapshot,
|
|
51
|
+
createSelectionSnapshot,
|
|
52
|
+
} from "../../core/state/editor-state.ts";
|
|
53
|
+
import {
|
|
54
|
+
anchorUnaffectedByMapping,
|
|
55
|
+
createNodeAnchor,
|
|
56
|
+
createRangeAnchor,
|
|
57
|
+
DEFAULT_BOUNDARY_ASSOC,
|
|
58
|
+
mapAnchor,
|
|
59
|
+
mapPosition,
|
|
60
|
+
type TransactionMapping,
|
|
61
|
+
} from "../../core/selection/mapping.ts";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Map a LIVE local selection forward through a remote transaction's
|
|
65
|
+
* mapping. Guarantees the returned selection never holds a `detached`
|
|
66
|
+
* `activeRange` — a detached anchor is a review-state concept
|
|
67
|
+
* (comments / revisions that lost their text) and is never a correct
|
|
68
|
+
* representation for a live caret. If the remote edit deleted the text
|
|
69
|
+
* the caret pointed at, the caret is collapsed to the logical boundary
|
|
70
|
+
* of the deletion.
|
|
71
|
+
*
|
|
72
|
+
* Returns the same reference when the mapping provably cannot shift the
|
|
73
|
+
* selection, so callers can use referential equality to skip downstream
|
|
74
|
+
* work.
|
|
75
|
+
*/
|
|
76
|
+
export function mapLocalSelectionOnRemoteReplay(
|
|
77
|
+
selection: SelectionSnapshot,
|
|
78
|
+
mapping: TransactionMapping,
|
|
79
|
+
): SelectionSnapshot {
|
|
80
|
+
if (mapping.steps.length === 0) {
|
|
81
|
+
return selection;
|
|
82
|
+
}
|
|
83
|
+
if (anchorUnaffectedByMapping(selection.activeRange, mapping)) {
|
|
84
|
+
return selection;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const active = selection.activeRange;
|
|
88
|
+
|
|
89
|
+
if (active.kind === "range") {
|
|
90
|
+
const directionForward = selection.anchor <= selection.head;
|
|
91
|
+
const isCollapsed = active.range.from === active.range.to;
|
|
92
|
+
|
|
93
|
+
if (isCollapsed) {
|
|
94
|
+
const collapsedAt = mapPosition(active.range.from, 1, mapping).position;
|
|
95
|
+
return {
|
|
96
|
+
anchor: collapsedAt,
|
|
97
|
+
head: collapsedAt,
|
|
98
|
+
isCollapsed: true,
|
|
99
|
+
activeRange: createRangeAnchor(
|
|
100
|
+
collapsedAt,
|
|
101
|
+
collapsedAt,
|
|
102
|
+
DEFAULT_BOUNDARY_ASSOC,
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const mapped = mapAnchor(active, mapping);
|
|
108
|
+
|
|
109
|
+
if (mapped.kind === "range") {
|
|
110
|
+
const { from, to } = mapped.range;
|
|
111
|
+
return {
|
|
112
|
+
anchor: directionForward ? from : to,
|
|
113
|
+
head: directionForward ? to : from,
|
|
114
|
+
isCollapsed: from === to,
|
|
115
|
+
activeRange: mapped,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Detached — the text under the selection is gone. Collapse to the
|
|
120
|
+
// deletion boundary. We use `mapPosition` on the original anchor
|
|
121
|
+
// with forward association (+1); `mapPositionThroughStep` collapses
|
|
122
|
+
// positions that land inside a deleted span to `step.from +
|
|
123
|
+
// insertSize`, which is the correct logical "where the selection
|
|
124
|
+
// used to start" position.
|
|
125
|
+
const collapsedAt = mapPosition(active.range.from, 1, mapping).position;
|
|
126
|
+
return {
|
|
127
|
+
anchor: collapsedAt,
|
|
128
|
+
head: collapsedAt,
|
|
129
|
+
isCollapsed: true,
|
|
130
|
+
activeRange: createRangeAnchor(
|
|
131
|
+
collapsedAt,
|
|
132
|
+
collapsedAt,
|
|
133
|
+
DEFAULT_BOUNDARY_ASSOC,
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (active.kind === "node") {
|
|
139
|
+
const mapped = mapAnchor(active, mapping);
|
|
140
|
+
if (mapped.kind === "node") {
|
|
141
|
+
return {
|
|
142
|
+
anchor: mapped.at,
|
|
143
|
+
head: mapped.at,
|
|
144
|
+
isCollapsed: true,
|
|
145
|
+
activeRange: mapped,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (mapped.kind === "range") {
|
|
149
|
+
return {
|
|
150
|
+
anchor: mapped.range.from,
|
|
151
|
+
head: mapped.range.to,
|
|
152
|
+
isCollapsed: mapped.range.from === mapped.range.to,
|
|
153
|
+
activeRange: mapped,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const collapsedAt = mapPosition(active.at, active.assoc, mapping).position;
|
|
157
|
+
return {
|
|
158
|
+
anchor: collapsedAt,
|
|
159
|
+
head: collapsedAt,
|
|
160
|
+
isCollapsed: true,
|
|
161
|
+
activeRange: createNodeAnchor(collapsedAt, active.assoc),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// `kind === "detached"` — already review-state-detached before the
|
|
166
|
+
// remote edit. The public `SelectionSnapshot` is not supposed to hold
|
|
167
|
+
// a detached activeRange (it's for comment/revision anchors), but if
|
|
168
|
+
// one slips in here, collapse it to a safe zero position. This arm
|
|
169
|
+
// exists purely for type exhaustiveness.
|
|
170
|
+
return createSelectionSnapshot(0, 0);
|
|
171
|
+
}
|
|
@@ -285,6 +285,7 @@ import {
|
|
|
285
285
|
} from "./object-grab/index.ts";
|
|
286
286
|
import type { EditorStatePayload } from "../io/ooxml/workflow-payload.ts";
|
|
287
287
|
import type { SharedWorkflowState } from "./collab/workflow-shared.ts";
|
|
288
|
+
import { mapLocalSelectionOnRemoteReplay } from "./collab/map-local-selection-on-remote-replay.ts";
|
|
288
289
|
import { formatPageNumber } from "./page-number-format.ts";
|
|
289
290
|
|
|
290
291
|
/** Internal extension of ExportDocxOptions that threads the collected
|
|
@@ -2983,13 +2984,9 @@ export function createDocumentRuntime(
|
|
|
2983
2984
|
const transaction = executeEditorCommand(replayState, command, replayContext);
|
|
2984
2985
|
if (crossStoryReplay) {
|
|
2985
2986
|
// Cross-story replay: the transaction's resulting selection is
|
|
2986
|
-
// in the remote story's region.
|
|
2987
|
-
//
|
|
2988
|
-
// so
|
|
2989
|
-
// document delta still applies; only the selection is filtered.
|
|
2990
|
-
// Full position-mapping of the local cursor through the remote
|
|
2991
|
-
// edit is a separate P11 sub-bullet (Awareness cursor transaction
|
|
2992
|
-
// mapping).
|
|
2987
|
+
// in the remote story's region. Preserve B's local selection
|
|
2988
|
+
// unchanged — the document delta applies to a story B isn't
|
|
2989
|
+
// looking at, so B's caret shouldn't move at all.
|
|
2993
2990
|
commitRemote({
|
|
2994
2991
|
...transaction,
|
|
2995
2992
|
nextState: {
|
|
@@ -2998,7 +2995,23 @@ export function createDocumentRuntime(
|
|
|
2998
2995
|
},
|
|
2999
2996
|
});
|
|
3000
2997
|
} else {
|
|
3001
|
-
|
|
2998
|
+
// Same-story replay: map B's CURRENT local selection through
|
|
2999
|
+
// the remote transaction's mapping so the caret stays pointed
|
|
3000
|
+
// at the same logical content. Do NOT adopt
|
|
3001
|
+
// `transaction.nextState.selection` — that is the remote
|
|
3002
|
+
// author's caret position, not B's. Without this, B would see
|
|
3003
|
+
// their own cursor jump whenever A types.
|
|
3004
|
+
const mappedLocalSelection = mapLocalSelectionOnRemoteReplay(
|
|
3005
|
+
state.selection,
|
|
3006
|
+
transaction.mapping,
|
|
3007
|
+
);
|
|
3008
|
+
commitRemote({
|
|
3009
|
+
...transaction,
|
|
3010
|
+
nextState: {
|
|
3011
|
+
...transaction.nextState,
|
|
3012
|
+
selection: mappedLocalSelection,
|
|
3013
|
+
},
|
|
3014
|
+
});
|
|
3002
3015
|
}
|
|
3003
3016
|
} catch (error) {
|
|
3004
3017
|
emitError(toRuntimeError(error));
|
|
@@ -3022,7 +3035,11 @@ export function createDocumentRuntime(
|
|
|
3022
3035
|
const stepsAccumulator: import("../core/selection/mapping.ts").MappingStep[] = [];
|
|
3023
3036
|
let anyDirty = false;
|
|
3024
3037
|
let lastNextState: typeof state | null = null;
|
|
3025
|
-
|
|
3038
|
+
// Running local selection: seeded from CURRENT local state, then
|
|
3039
|
+
// mapped forward through each same-story envelope's mapping.
|
|
3040
|
+
// Cross-story envelopes do not touch this value — B's caret
|
|
3041
|
+
// should never move from a story B isn't looking at.
|
|
3042
|
+
let runningLocalSelection: typeof state.selection = state.selection;
|
|
3026
3043
|
const warningsAdded: import("../core/state/editor-state.ts").EditorWarning[] = [];
|
|
3027
3044
|
const warningsCleared: Array<{ warningId: string; code: import("../core/state/editor-state.ts").EditorWarning["code"] }> = [];
|
|
3028
3045
|
const SINGULAR_EFFECT_KEYS = [
|
|
@@ -3089,7 +3106,12 @@ export function createDocumentRuntime(
|
|
|
3089
3106
|
}
|
|
3090
3107
|
}
|
|
3091
3108
|
lastNextState = transaction.nextState;
|
|
3092
|
-
|
|
3109
|
+
if (!crossStoryReplay) {
|
|
3110
|
+
runningLocalSelection = mapLocalSelectionOnRemoteReplay(
|
|
3111
|
+
runningLocalSelection,
|
|
3112
|
+
transaction.mapping,
|
|
3113
|
+
);
|
|
3114
|
+
}
|
|
3093
3115
|
}
|
|
3094
3116
|
|
|
3095
3117
|
// Singular-effect collision (≥2 envelopes both setting the same
|
|
@@ -3114,9 +3136,7 @@ export function createDocumentRuntime(
|
|
|
3114
3136
|
return;
|
|
3115
3137
|
}
|
|
3116
3138
|
const consolidated = {
|
|
3117
|
-
nextState:
|
|
3118
|
-
? { ...lastNextState, selection: state.selection }
|
|
3119
|
-
: lastNextState,
|
|
3139
|
+
nextState: { ...lastNextState, selection: runningLocalSelection },
|
|
3120
3140
|
mapping: { steps: stepsAccumulator },
|
|
3121
3141
|
effects: {
|
|
3122
3142
|
warningsAdded,
|