@portabletext/plugin-sdk-value 7.2.0 → 7.2.2
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/dist/index.d.ts +94 -125
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +931 -937
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
package/dist/index.js
CHANGED
|
@@ -1,1038 +1,1032 @@
|
|
|
1
1
|
import { c } from "react/compiler-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { editDocument, getDocumentState, subscribeDocumentEvents, useApplyDocumentActions, useEditDocument, usePresenceForDocument, useReportPresence, useSanityInstance } from "@sanity/sdk-react";
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { useEditor, useEditorSelector
|
|
4
|
+
import { PortableTextEditable, useEditor, useEditorSelector } from "@portabletext/editor";
|
|
5
5
|
import { getSelection } from "@portabletext/editor/selectors";
|
|
6
6
|
import { isEqualSelections } from "@portabletext/editor/utils";
|
|
7
|
-
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
8
7
|
import { applyAll } from "@portabletext/patches";
|
|
9
8
|
import { diffValue } from "@sanity/diff-patch";
|
|
10
9
|
import { parsePath } from "@sanity/json-match";
|
|
11
10
|
import { useActorRef } from "@xstate/react";
|
|
12
11
|
import { fromCallback, setup } from "xstate";
|
|
13
12
|
import rawDebug from "debug";
|
|
13
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
/**
|
|
15
|
+
* Reduces a selection to a caret at its focus point.
|
|
16
|
+
*
|
|
17
|
+
* Presence answers where someone is, so decorating their whole selection would
|
|
18
|
+
* highlight text the local user never selected. The Studio collapses the same
|
|
19
|
+
* way, which keeps the two consistent when both are open on a document.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
14
23
|
function collapseToCaret(selection) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
return selection === null ? null : {
|
|
25
|
+
anchor: selection.focus,
|
|
26
|
+
focus: selection.focus
|
|
27
|
+
};
|
|
19
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Where one remote caret should be drawn.
|
|
31
|
+
*
|
|
32
|
+
* A caret the local editor has moved keeps that moved position for as long as
|
|
33
|
+
* the participant keeps reporting the same selection. A changed report wins,
|
|
34
|
+
* because the participant has actually moved.
|
|
35
|
+
*
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
20
38
|
function resolveCursorSelection(cursor, override) {
|
|
21
|
-
|
|
39
|
+
return override && isEqualSelections(override.reported, cursor.selection) ? override.current : collapseToCaret(cursor.selection);
|
|
22
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Records where the editor moved a caret to.
|
|
43
|
+
*
|
|
44
|
+
* Overrides for sessions that are no longer present are dropped, so a long
|
|
45
|
+
* editing session does not accumulate one entry per participant who ever
|
|
46
|
+
* visited. Returns the map untouched when nothing changed.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
23
50
|
function recordCursorMove(overrides, cursors, cursor, selection) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
current: moved
|
|
33
|
-
}), next;
|
|
51
|
+
let moved = collapseToCaret(selection), existing = overrides.get(cursor.sessionId), alreadyRecorded = existing !== void 0 && isEqualSelections(existing.reported, cursor.selection) && isEqualSelections(existing.current, moved), live = new Set(cursors.map((candidate) => candidate.sessionId)), staleKeys = [...overrides.keys()].filter((key) => !live.has(key));
|
|
52
|
+
if (alreadyRecorded && staleKeys.length === 0) return overrides;
|
|
53
|
+
let next = new Map(overrides);
|
|
54
|
+
for (let key of staleKeys) next.delete(key);
|
|
55
|
+
return next.set(cursor.sessionId, {
|
|
56
|
+
reported: cursor.selection,
|
|
57
|
+
current: moved
|
|
58
|
+
}), next;
|
|
34
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Maps remote cursors to range decorations for
|
|
62
|
+
* `<PortableTextEditable rangeDecorations={...} />`.
|
|
63
|
+
*
|
|
64
|
+
* Participants with no caret to draw are skipped, so someone who clears their
|
|
65
|
+
* selection stops drawing without being treated as having left.
|
|
66
|
+
*
|
|
67
|
+
* @public
|
|
68
|
+
*/
|
|
35
69
|
function toRangeDecorations(options) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
payload: {
|
|
48
|
-
sessionId: cursor.sessionId
|
|
49
|
-
},
|
|
50
|
-
onMoved: onCursorMoved ? (details) => onCursorMoved(cursor, details.newSelection) : void 0
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
return decorations;
|
|
70
|
+
let { cursors, overrides, renderCursor, onCursorMoved } = options, decorations = [];
|
|
71
|
+
for (let cursor of cursors) {
|
|
72
|
+
let selection = resolveCursorSelection(cursor, overrides.get(cursor.sessionId));
|
|
73
|
+
selection !== null && decorations.push({
|
|
74
|
+
component: renderCursor(cursor),
|
|
75
|
+
selection,
|
|
76
|
+
payload: { sessionId: cursor.sessionId },
|
|
77
|
+
onMoved: onCursorMoved ? (details) => onCursorMoved(cursor, details.newSelection) : void 0
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return decorations;
|
|
54
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The local user's selection, deduped by value.
|
|
84
|
+
*
|
|
85
|
+
* The editor produces a fresh selection object on every snapshot change, so
|
|
86
|
+
* subscribing to it directly would report presence far more often than the
|
|
87
|
+
* caret actually moves.
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
55
91
|
function useLocalSelection() {
|
|
56
|
-
|
|
57
|
-
|
|
92
|
+
let editor = useEditor();
|
|
93
|
+
return useEditorSelector(editor, getSelection, isEqualSelections);
|
|
58
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Turns remote cursors into range decorations, keeping each caret anchored as
|
|
97
|
+
* the local user edits.
|
|
98
|
+
*
|
|
99
|
+
* Knows nothing about Sanity or the SDK, so it can also drive carets from
|
|
100
|
+
* another source or from a test fixture. `useSDKPresenceCursors` is the version
|
|
101
|
+
* wired to SDK presence.
|
|
102
|
+
*
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
59
105
|
function useRemoteCursors(options) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
cursors,
|
|
72
|
-
overrides,
|
|
73
|
-
renderCursor,
|
|
74
|
-
onCursorMoved
|
|
75
|
-
}), $[2] = cursors, $[3] = onCursorMoved, $[4] = overrides, $[5] = renderCursor, $[6] = t1) : t1 = $[6], t1;
|
|
106
|
+
let $ = c(7), { cursors, renderCursor } = options, [overrides, setOverrides] = useState(EMPTY_OVERRIDES), t0;
|
|
107
|
+
$[0] === cursors ? t0 = $[1] : (t0 = (cursor, selection) => {
|
|
108
|
+
setOverrides((previous) => recordCursorMove(previous, cursors, cursor, selection));
|
|
109
|
+
}, $[0] = cursors, $[1] = t0);
|
|
110
|
+
let onCursorMoved = t0, t1;
|
|
111
|
+
return $[2] !== cursors || $[3] !== onCursorMoved || $[4] !== overrides || $[5] !== renderCursor ? (t1 = toRangeDecorations({
|
|
112
|
+
cursors,
|
|
113
|
+
overrides,
|
|
114
|
+
renderCursor,
|
|
115
|
+
onCursorMoved
|
|
116
|
+
}), $[2] = cursors, $[3] = onCursorMoved, $[4] = overrides, $[5] = renderCursor, $[6] = t1) : t1 = $[6], t1;
|
|
76
117
|
}
|
|
77
118
|
const EMPTY_OVERRIDES = /* @__PURE__ */ new Map(), rootName = "pte:plugin-sdk-value:";
|
|
78
119
|
function createDebugger(name) {
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
let namespace = `${rootName}${name}`;
|
|
121
|
+
return rawDebug && rawDebug.enabled(namespace) ? rawDebug(namespace) : rawDebug(rootName);
|
|
81
122
|
}
|
|
82
123
|
const debug = {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
124
|
+
mutation: createDebugger("mutation"),
|
|
125
|
+
push: createDebugger("push"),
|
|
126
|
+
remote: createDebugger("remote"),
|
|
127
|
+
repair: createDebugger("repair")
|
|
87
128
|
}, ARRAYIFY_ERROR_MESSAGE = "Unexpected path format from diffValue output. Please report this issue.";
|
|
88
129
|
function* getSegments(node) {
|
|
89
|
-
|
|
130
|
+
node.base && (yield* getSegments(node.base)), node.segment.type !== "This" && (yield node.segment);
|
|
90
131
|
}
|
|
91
132
|
function isKeyPath(node) {
|
|
92
|
-
|
|
133
|
+
return node.type !== "Path" || node.base || node.recursive || node.segment.type !== "Identifier" ? !1 : node.segment.name === "_key";
|
|
93
134
|
}
|
|
94
135
|
function arrayifyPath(pathExpr) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (element.type !== "Comparison")
|
|
111
|
-
throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
112
|
-
if (element.operator !== "==")
|
|
113
|
-
throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
114
|
-
const keyPathNode = [element.left, element.right].find(isKeyPath);
|
|
115
|
-
if (!keyPathNode)
|
|
116
|
-
throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
117
|
-
const other = element.left === keyPathNode ? element.right : element.left;
|
|
118
|
-
if (other.type !== "String")
|
|
119
|
-
throw new Error(ARRAYIFY_ERROR_MESSAGE);
|
|
120
|
-
return {
|
|
121
|
-
_key: other.value
|
|
122
|
-
};
|
|
123
|
-
});
|
|
136
|
+
let node = parsePath(pathExpr);
|
|
137
|
+
if (!node) return [];
|
|
138
|
+
if (node.type !== "Path") throw Error(ARRAYIFY_ERROR_MESSAGE);
|
|
139
|
+
return Array.from(getSegments(node)).map((segment) => {
|
|
140
|
+
if (segment.type === "Identifier") return segment.name;
|
|
141
|
+
if (segment.type !== "Subscript" || segment.elements.length !== 1) throw Error(ARRAYIFY_ERROR_MESSAGE);
|
|
142
|
+
let [element] = segment.elements;
|
|
143
|
+
if (element.type === "Number") return element.value;
|
|
144
|
+
if (element.type !== "Comparison" || element.operator !== "==") throw Error(ARRAYIFY_ERROR_MESSAGE);
|
|
145
|
+
let keyPathNode = [element.left, element.right].find(isKeyPath);
|
|
146
|
+
if (!keyPathNode) throw Error(ARRAYIFY_ERROR_MESSAGE);
|
|
147
|
+
let other = element.left === keyPathNode ? element.right : element.left;
|
|
148
|
+
if (other.type !== "String") throw Error(ARRAYIFY_ERROR_MESSAGE);
|
|
149
|
+
return { _key: other.value };
|
|
150
|
+
});
|
|
124
151
|
}
|
|
125
152
|
function convertPatches(patches) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
items
|
|
160
|
-
}];
|
|
161
|
-
}
|
|
162
|
-
default:
|
|
163
|
-
return [];
|
|
164
|
-
}
|
|
165
|
-
}));
|
|
153
|
+
return patches.flatMap((p) => Object.entries(p).flatMap(([type, values]) => {
|
|
154
|
+
let origin = "remote";
|
|
155
|
+
switch (type) {
|
|
156
|
+
case "set":
|
|
157
|
+
case "setIfMissing":
|
|
158
|
+
case "diffMatchPatch":
|
|
159
|
+
case "inc":
|
|
160
|
+
case "dec": return Object.entries(values).map(([pathExpr, value]) => ({
|
|
161
|
+
type,
|
|
162
|
+
value,
|
|
163
|
+
origin,
|
|
164
|
+
path: arrayifyPath(pathExpr)
|
|
165
|
+
}));
|
|
166
|
+
case "unset": return Array.isArray(values) ? values.map(arrayifyPath).map((path) => ({
|
|
167
|
+
type,
|
|
168
|
+
origin,
|
|
169
|
+
path
|
|
170
|
+
})) : [];
|
|
171
|
+
case "insert": {
|
|
172
|
+
let { items, ...rest } = values, position = Object.keys(rest).at(0);
|
|
173
|
+
if (!position) return [];
|
|
174
|
+
let pathExpr = rest[position];
|
|
175
|
+
return [{
|
|
176
|
+
type,
|
|
177
|
+
origin,
|
|
178
|
+
position,
|
|
179
|
+
path: arrayifyPath(pathExpr),
|
|
180
|
+
items
|
|
181
|
+
}];
|
|
182
|
+
}
|
|
183
|
+
default: return [];
|
|
184
|
+
}
|
|
185
|
+
}));
|
|
166
186
|
}
|
|
167
187
|
const STRINGIFY_ERROR_MESSAGE = "Unable to convert an editor patch path to a Sanity path expression.";
|
|
188
|
+
/**
|
|
189
|
+
* Converts a Portable Text Editor patch path (an array of segments) into a
|
|
190
|
+
* Sanity json-match path expression. The inverse of `arrayifyPath`.
|
|
191
|
+
*
|
|
192
|
+
* @internal
|
|
193
|
+
*/
|
|
168
194
|
function stringifyPatchPath(path) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
else if (typeof segment == "object" && segment !== null && "_key" in segment)
|
|
176
|
-
result = `${result}[_key=="${segment._key}"]`;
|
|
177
|
-
else
|
|
178
|
-
throw new Error(STRINGIFY_ERROR_MESSAGE);
|
|
179
|
-
return result;
|
|
195
|
+
let result = "";
|
|
196
|
+
for (let segment of path) if (typeof segment == "string") result = result === "" ? segment : `${result}.${segment}`;
|
|
197
|
+
else if (typeof segment == "number") result = `${result}[${segment}]`;
|
|
198
|
+
else if (typeof segment == "object" && segment && "_key" in segment) result = `${result}[_key=="${segment._key}"]`;
|
|
199
|
+
else throw Error(STRINGIFY_ERROR_MESSAGE);
|
|
200
|
+
return result;
|
|
180
201
|
}
|
|
181
202
|
function prefixPathExpression(prefix, expression) {
|
|
182
|
-
|
|
203
|
+
return expression === "" ? prefix : expression.startsWith("[") ? `${prefix}${expression}` : `${prefix}.${expression}`;
|
|
183
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Converts Portable Text Editor patches into Sanity patch operations rooted
|
|
207
|
+
* at the given document field path. Throws if a patch cannot be converted;
|
|
208
|
+
* callers should fall back to pushing the whole value.
|
|
209
|
+
*
|
|
210
|
+
* @internal
|
|
211
|
+
*/
|
|
184
212
|
function convertPatchesToSanity(patches, options) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return patch.path.length === 0 ? {
|
|
202
|
-
set: {
|
|
203
|
-
[pathExpression]: []
|
|
204
|
-
}
|
|
205
|
-
} : {
|
|
206
|
-
unset: [pathExpression]
|
|
207
|
-
};
|
|
208
|
-
case "diffMatchPatch":
|
|
209
|
-
return {
|
|
210
|
-
diffMatchPatch: {
|
|
211
|
-
[pathExpression]: patch.value
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
case "inc":
|
|
215
|
-
return {
|
|
216
|
-
inc: {
|
|
217
|
-
[pathExpression]: patch.value
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
case "dec":
|
|
221
|
-
return {
|
|
222
|
-
dec: {
|
|
223
|
-
[pathExpression]: patch.value
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
case "insert":
|
|
227
|
-
return {
|
|
228
|
-
insert: {
|
|
229
|
-
[patch.position]: pathExpression,
|
|
230
|
-
items: patch.items
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
default:
|
|
234
|
-
throw new Error(STRINGIFY_ERROR_MESSAGE);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
213
|
+
return patches.map((patch) => {
|
|
214
|
+
let pathExpression = prefixPathExpression(options.prefix, stringifyPatchPath(patch.path));
|
|
215
|
+
switch (patch.type) {
|
|
216
|
+
case "set": return { set: { [pathExpression]: patch.value } };
|
|
217
|
+
case "setIfMissing": return { setIfMissing: { [pathExpression]: patch.value } };
|
|
218
|
+
case "unset": return patch.path.length === 0 ? { set: { [pathExpression]: [] } } : { unset: [pathExpression] };
|
|
219
|
+
case "diffMatchPatch": return { diffMatchPatch: { [pathExpression]: patch.value } };
|
|
220
|
+
case "inc": return { inc: { [pathExpression]: patch.value } };
|
|
221
|
+
case "dec": return { dec: { [pathExpression]: patch.value } };
|
|
222
|
+
case "insert": return { insert: {
|
|
223
|
+
[patch.position]: pathExpression,
|
|
224
|
+
items: patch.items
|
|
225
|
+
} };
|
|
226
|
+
default: throw Error(STRINGIFY_ERROR_MESSAGE);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
237
229
|
}
|
|
238
230
|
function segmentsEqual(a, b) {
|
|
239
|
-
|
|
231
|
+
return typeof a == "string" || typeof a == "number" ? a === b : typeof b == "string" || typeof b == "number" || Array.isArray(a) || Array.isArray(b) ? !1 : a._key === b._key;
|
|
240
232
|
}
|
|
241
233
|
function pathsEqual(a, b) {
|
|
242
|
-
|
|
234
|
+
return a.length === b.length && a.every((segment, index) => segmentsEqual(segment, b[index]));
|
|
243
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* The editor engine can only resolve keyed/indexed segments against the
|
|
238
|
+
* root block array and (possibly nested) `children` arrays. Operations
|
|
239
|
+
* that address items of any other array, e.g. a block's `markDefs` or a
|
|
240
|
+
* span's `marks`, misapply. When a patch path enters such a sidecar
|
|
241
|
+
* array, this returns the path of the array itself so callers can fall
|
|
242
|
+
* back to replacing the whole property; returns `null` for paths the
|
|
243
|
+
* engine can apply.
|
|
244
|
+
*
|
|
245
|
+
* @internal
|
|
246
|
+
*/
|
|
244
247
|
function findSidecarArrayPath(path) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
return null;
|
|
248
|
+
let expectNode = !0;
|
|
249
|
+
for (let index = 0; index < path.length; index++) {
|
|
250
|
+
let segment = path[index];
|
|
251
|
+
if (typeof segment == "string") expectNode = segment === "children";
|
|
252
|
+
else {
|
|
253
|
+
if (!expectNode) return path.slice(0, index);
|
|
254
|
+
expectNode = !1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
257
258
|
}
|
|
258
259
|
function getValueAtPath(value, path) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return current;
|
|
260
|
+
let current = value;
|
|
261
|
+
for (let segment of path) {
|
|
262
|
+
if (current == null) return;
|
|
263
|
+
if (typeof segment == "number") {
|
|
264
|
+
if (!Array.isArray(current)) return;
|
|
265
|
+
current = current[segment < 0 ? current.length + segment : segment];
|
|
266
|
+
} else if (typeof segment == "string") {
|
|
267
|
+
if (typeof current != "object" || Array.isArray(current)) return;
|
|
268
|
+
current = current[segment];
|
|
269
|
+
} else if (Array.isArray(segment)) return;
|
|
270
|
+
else {
|
|
271
|
+
if (!Array.isArray(current)) return;
|
|
272
|
+
current = current.find((item) => typeof item == "object" && !!item && !Array.isArray(item) && item._key === segment._key);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return current;
|
|
278
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Converts a target-value diff into patches the editor engine can apply.
|
|
279
|
+
* Patches addressing items inside sidecar arrays are coalesced into whole
|
|
280
|
+
* `set`s (or `unset`s) of the owning property, taken from the target
|
|
281
|
+
* value.
|
|
282
|
+
*
|
|
283
|
+
* @internal
|
|
284
|
+
*/
|
|
279
285
|
function toEngineSafePatches(patches, targetValue) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
286
|
+
let safe = [], sidecarPaths = [];
|
|
287
|
+
for (let patch of patches) {
|
|
288
|
+
let sidecarPath = findSidecarArrayPath(patch.path);
|
|
289
|
+
if (!sidecarPath) {
|
|
290
|
+
safe.push(patch);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
sidecarPaths.some((existing) => pathsEqual(existing, sidecarPath)) || sidecarPaths.push(sidecarPath);
|
|
294
|
+
}
|
|
295
|
+
for (let sidecarPath of sidecarPaths) {
|
|
296
|
+
let value = getValueAtPath(targetValue, sidecarPath);
|
|
297
|
+
safe.push(value === void 0 ? {
|
|
298
|
+
type: "unset",
|
|
299
|
+
path: sidecarPath,
|
|
300
|
+
origin: "remote"
|
|
301
|
+
} : {
|
|
302
|
+
type: "set",
|
|
303
|
+
path: sidecarPath,
|
|
304
|
+
value,
|
|
305
|
+
origin: "remote"
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return safe;
|
|
303
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* Drops insert items whose `_key` is absent from the remote value. A text
|
|
312
|
+
* paste can stage a temporary span and remove it again within the same
|
|
313
|
+
* transaction; applying the insert without the cleanup would flash the
|
|
314
|
+
* staged content. Callers must only run this once the store value reflects
|
|
315
|
+
* the transaction the patches belong to (see `'apply remote patches'`),
|
|
316
|
+
* otherwise every legitimately new node would be dropped.
|
|
317
|
+
*/
|
|
304
318
|
function filterInsertsMissingFromRemoteValue(patches, remoteValue) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}] : [];
|
|
319
|
-
});
|
|
319
|
+
return patches.flatMap((patch) => {
|
|
320
|
+
if (patch.type !== "insert") return [patch];
|
|
321
|
+
let parentValue = getValueAtPath(remoteValue, patch.path.slice(0, -1));
|
|
322
|
+
if (!Array.isArray(parentValue)) return [patch];
|
|
323
|
+
let items = patch.items.filter((item) => {
|
|
324
|
+
let itemKey = getKey(item);
|
|
325
|
+
return !itemKey || parentValue.some((candidate) => getKey(candidate) === itemKey);
|
|
326
|
+
});
|
|
327
|
+
return items.length > 0 ? [{
|
|
328
|
+
...patch,
|
|
329
|
+
items
|
|
330
|
+
}] : [];
|
|
331
|
+
});
|
|
320
332
|
}
|
|
321
333
|
function getKey(value) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return typeof key == "string" ? key : void 0;
|
|
334
|
+
if (typeof value != "object" || !value || Array.isArray(value)) return;
|
|
335
|
+
let key = value._key;
|
|
336
|
+
return typeof key == "string" ? key : void 0;
|
|
326
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* The editor writes `markDefs` as whole-array `set`s. Two clients
|
|
340
|
+
* formatting the same block concurrently then overwrite each other's
|
|
341
|
+
* arrays at the server (last writer wins) while both clients' span
|
|
342
|
+
* `marks` references survive, stranding marks without definitions.
|
|
343
|
+
* Outgoing `markDefs` sets are therefore decomposed into item-level
|
|
344
|
+
* operations against the store (server truth): new definitions insert,
|
|
345
|
+
* changed definitions set by key, and removed definitions unset by key,
|
|
346
|
+
* except definitions the store's own spans still reference (a diverged
|
|
347
|
+
* client's normalizer prunes those spuriously; a later, converged flush
|
|
348
|
+
* removes them for real). Item-keyed operations merge at the server
|
|
349
|
+
* instead of overwriting.
|
|
350
|
+
*
|
|
351
|
+
* @internal
|
|
352
|
+
*/
|
|
327
353
|
function toMergeableMarkDefsPatches(patches, getCurrentValue) {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
for (const item of store)
|
|
370
|
-
item._key !== void 0 && !localKeys.has(item._key) && !referencedKeys.has(item._key) && ops.push({
|
|
371
|
-
type: "unset",
|
|
372
|
-
origin,
|
|
373
|
-
path: [...patch.path, {
|
|
374
|
-
_key: item._key
|
|
375
|
-
}]
|
|
376
|
-
});
|
|
377
|
-
return ops;
|
|
378
|
-
});
|
|
354
|
+
return patches.flatMap((patch) => {
|
|
355
|
+
if (patch.type !== "set" || patch.path.at(-1) !== "markDefs" || !Array.isArray(patch.value)) return [patch];
|
|
356
|
+
let currentValue = getCurrentValue();
|
|
357
|
+
if (!currentValue) return [patch];
|
|
358
|
+
let root = currentValue, storeMarkDefs = getValueAtPath(root, patch.path), storeBlock = getValueAtPath(root, patch.path.slice(0, -1));
|
|
359
|
+
if (!Array.isArray(storeMarkDefs) || typeof storeBlock != "object" || !storeBlock) return [patch];
|
|
360
|
+
let local = patch.value, store = storeMarkDefs;
|
|
361
|
+
if (local.some((item) => item._key === void 0)) return [patch];
|
|
362
|
+
let referencedKeys = new Set((storeBlock.children ?? []).flatMap((child) => child.marks ?? [])), storeByKey = new Map(store.map((item) => [item._key, item])), localKeys = new Set(local.map((item) => item._key)), origin = patch.origin, ops = [], inserted = local.filter((item) => !storeByKey.has(item._key));
|
|
363
|
+
if (inserted.length > 0) {
|
|
364
|
+
for (let item of inserted) item._key !== void 0 && ops.push({
|
|
365
|
+
type: "unset",
|
|
366
|
+
origin,
|
|
367
|
+
path: [...patch.path, { _key: item._key }]
|
|
368
|
+
});
|
|
369
|
+
ops.push({
|
|
370
|
+
type: "insert",
|
|
371
|
+
origin,
|
|
372
|
+
position: "after",
|
|
373
|
+
path: [...patch.path, -1],
|
|
374
|
+
items: inserted
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
for (let item of local) {
|
|
378
|
+
let existing = storeByKey.get(item._key);
|
|
379
|
+
existing && JSON.stringify(existing) !== JSON.stringify(item) && ops.push({
|
|
380
|
+
type: "set",
|
|
381
|
+
origin,
|
|
382
|
+
path: [...patch.path, { _key: item._key }],
|
|
383
|
+
value: item
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
for (let item of store) item._key !== void 0 && !localKeys.has(item._key) && !referencedKeys.has(item._key) && ops.push({
|
|
387
|
+
type: "unset",
|
|
388
|
+
origin,
|
|
389
|
+
path: [...patch.path, { _key: item._key }]
|
|
390
|
+
});
|
|
391
|
+
return ops;
|
|
392
|
+
});
|
|
379
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Whether a remote patch can resolve against the given editor value.
|
|
396
|
+
* Concurrent edits routinely produce operations addressing nodes another
|
|
397
|
+
* client has already removed or not yet created; sending those into the
|
|
398
|
+
* engine fails loudly (console errors) before being skipped. Callers drop
|
|
399
|
+
* unresolvable patches up front and rely on the follow-up repair sync to
|
|
400
|
+
* converge instead.
|
|
401
|
+
*
|
|
402
|
+
* @internal
|
|
403
|
+
*/
|
|
380
404
|
function canApplyToValue(patch, value) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
return getValueAtPath(root, patch.path) !== void 0;
|
|
391
|
-
// set creates its target property, so only the parent must resolve
|
|
392
|
-
case "set":
|
|
393
|
-
return patch.path.length === 0 || getValueAtPath(root, patch.path.slice(0, -1)) !== void 0;
|
|
394
|
-
default:
|
|
395
|
-
return !0;
|
|
396
|
-
}
|
|
405
|
+
if (!value) return !0;
|
|
406
|
+
let root = value;
|
|
407
|
+
switch (patch.type) {
|
|
408
|
+
case "unset":
|
|
409
|
+
case "insert":
|
|
410
|
+
case "diffMatchPatch": return getValueAtPath(root, patch.path) !== void 0;
|
|
411
|
+
case "set": return patch.path.length === 0 || getValueAtPath(root, patch.path.slice(0, -1)) !== void 0;
|
|
412
|
+
default: return !0;
|
|
413
|
+
}
|
|
397
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Filters a patch batch down to the patches that can resolve (see
|
|
417
|
+
* `canApplyToValue`), checking each patch against the value as it stands
|
|
418
|
+
* after the preceding patches applied. A transaction routinely inserts a
|
|
419
|
+
* node and then addresses it, e.g. a span split followed by a `marks` set
|
|
420
|
+
* on the new span, so checking every patch against the starting value
|
|
421
|
+
* would drop valid operations.
|
|
422
|
+
*/
|
|
398
423
|
function filterResolvablePatches(patches, value) {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
projected = applyAll(projected, [patch]);
|
|
405
|
-
} catch {
|
|
406
|
-
}
|
|
407
|
-
return resolvable;
|
|
424
|
+
let projected = value, resolvable = [];
|
|
425
|
+
for (let patch of patches) if (canApplyToValue(patch, projected) && (resolvable.push(patch), projected)) try {
|
|
426
|
+
projected = applyAll(projected, [patch]);
|
|
427
|
+
} catch {}
|
|
428
|
+
return resolvable;
|
|
408
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Scopes document-rooted Sanity patches to the given field path, returning
|
|
432
|
+
* field-relative Portable Text Editor patches. Patches outside the field are
|
|
433
|
+
* dropped. Returns `null` when the field (or an ancestor of it) is replaced
|
|
434
|
+
* wholesale, in which case the caller should fall back to a full value sync.
|
|
435
|
+
* Throws when a path expression cannot be converted.
|
|
436
|
+
*
|
|
437
|
+
* @internal
|
|
438
|
+
*/
|
|
409
439
|
function scopeRemotePatches(patches, fieldPath) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return scoped;
|
|
440
|
+
let prefix = arrayifyPath(fieldPath), converted = convertPatches(patches), scoped = [];
|
|
441
|
+
for (let patch of converted) {
|
|
442
|
+
let overlap = Math.min(patch.path.length, prefix.length), touchesField = !0;
|
|
443
|
+
for (let index = 0; index < overlap; index++) if (!segmentsEqual(patch.path[index], prefix[index])) {
|
|
444
|
+
touchesField = !1;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
if (touchesField) {
|
|
448
|
+
if (patch.path.length <= prefix.length) return null;
|
|
449
|
+
scoped.push({
|
|
450
|
+
...patch,
|
|
451
|
+
path: patch.path.slice(prefix.length)
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return scoped;
|
|
429
456
|
}
|
|
430
457
|
function debugTextOf(value) {
|
|
431
|
-
|
|
432
|
-
`) : "";
|
|
458
|
+
return Array.isArray(value) ? value.map((block) => Array.isArray(block.children) ? (block.children ?? []).map((child) => child.text ?? "").join("") : "").join("\n") : "";
|
|
433
459
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
460
|
+
/**
|
|
461
|
+
* How long an editor-versus-store divergence must persist, unchanged,
|
|
462
|
+
* before the whole-value repair acts on it. When a remote transaction
|
|
463
|
+
* arrives interleaved with the listener echoes of this client's own recent
|
|
464
|
+
* edits, the store value is transiently wrong until the echo returns and
|
|
465
|
+
* the rebase corrects it. A repair fired inside that window copies the
|
|
466
|
+
* garbage into the editor and a follow-up repair restores the text at a
|
|
467
|
+
* drifted offset, scrambling words the user typed in the meantime. The
|
|
468
|
+
* window therefore has to outlast a slow listener echo round trip; real
|
|
469
|
+
* divergence is stable and loses nothing by being repaired a beat later.
|
|
470
|
+
*
|
|
471
|
+
* Tests use a short window: their mock stores are synchronous, so echo
|
|
472
|
+
* transients cannot occur, and waiting the production interval would only
|
|
473
|
+
* slow every repair assertion down.
|
|
474
|
+
*/
|
|
475
|
+
const REPAIR_CONFIRM_DELAY = process.env.NODE_ENV === "test" ? 150 : 1e3, pendingRepairs = /* @__PURE__ */ new WeakMap(), unflushedEdits = /* @__PURE__ */ new WeakMap();
|
|
438
476
|
function computeRepair(editor, remoteValue) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
477
|
+
let snapshot = editor.getSnapshot().context.value, stateSignature = JSON.stringify([snapshot, remoteValue]);
|
|
478
|
+
try {
|
|
479
|
+
return {
|
|
480
|
+
patches: toEngineSafePatches(convertPatches(diffValue(snapshot, remoteValue)), remoteValue),
|
|
481
|
+
convertible: !0,
|
|
482
|
+
signature: stateSignature
|
|
483
|
+
};
|
|
484
|
+
} catch {
|
|
485
|
+
return {
|
|
486
|
+
patches: [],
|
|
487
|
+
convertible: !1,
|
|
488
|
+
signature: `!${stateSignature}`
|
|
489
|
+
};
|
|
490
|
+
}
|
|
453
491
|
}
|
|
454
492
|
function cancelPendingRepair(editor) {
|
|
455
|
-
|
|
456
|
-
|
|
493
|
+
let pending = pendingRepairs.get(editor);
|
|
494
|
+
pending && (clearTimeout(pending.timer), pendingRepairs.delete(editor));
|
|
457
495
|
}
|
|
458
|
-
function applySync({
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
value: latestRemote
|
|
519
|
-
}));
|
|
520
|
-
}, REPAIR_CONFIRM_DELAY);
|
|
521
|
-
pendingRepairs.set(editor, {
|
|
522
|
-
signature: first.signature,
|
|
523
|
-
timer
|
|
524
|
-
});
|
|
496
|
+
function applySync({ editor, getRemoteValue }) {
|
|
497
|
+
let remoteValue = getRemoteValue();
|
|
498
|
+
if (!remoteValue) return;
|
|
499
|
+
let first = computeRepair(editor, remoteValue);
|
|
500
|
+
if (first.convertible && first.patches.length === 0) {
|
|
501
|
+
cancelPendingRepair(editor);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
debug.repair.enabled && debug.repair("editor and store texts diverged %o", {
|
|
505
|
+
editorText: debugTextOf(editor.getSnapshot().context.value),
|
|
506
|
+
remoteText: debugTextOf(remoteValue)
|
|
507
|
+
});
|
|
508
|
+
let pending = pendingRepairs.get(editor);
|
|
509
|
+
if (pending && pending.signature === first.signature) return;
|
|
510
|
+
cancelPendingRepair(editor);
|
|
511
|
+
let timer = setTimeout(() => {
|
|
512
|
+
if (pendingRepairs.delete(editor), unflushedEdits.get(editor)) {
|
|
513
|
+
applySync({
|
|
514
|
+
editor,
|
|
515
|
+
getRemoteValue
|
|
516
|
+
});
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
let latestRemote = getRemoteValue();
|
|
520
|
+
if (!latestRemote) return;
|
|
521
|
+
let second = computeRepair(editor, latestRemote);
|
|
522
|
+
if (second.convertible && second.patches.length === 0) return;
|
|
523
|
+
if (second.signature !== first.signature) {
|
|
524
|
+
applySync({
|
|
525
|
+
editor,
|
|
526
|
+
getRemoteValue
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (!second.convertible) {
|
|
531
|
+
debug.repair("escalating to whole-value sync (unconvertible diff)"), editor.send({
|
|
532
|
+
type: "update value",
|
|
533
|
+
value: latestRemote
|
|
534
|
+
});
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
let snapshot = editor.getSnapshot().context.value;
|
|
538
|
+
debug.repair("applying confirmed repair patches %o", second.patches), editor.send({
|
|
539
|
+
type: "patches",
|
|
540
|
+
patches: second.patches,
|
|
541
|
+
snapshot
|
|
542
|
+
});
|
|
543
|
+
let valueAfterPatches = editor.getSnapshot().context.value;
|
|
544
|
+
diffValue(valueAfterPatches, latestRemote).length > 0 && (debug.repair.enabled && debug.repair("escalating to whole-value sync %o", {
|
|
545
|
+
editorText: debugTextOf(valueAfterPatches),
|
|
546
|
+
remoteText: debugTextOf(latestRemote)
|
|
547
|
+
}), editor.send({
|
|
548
|
+
type: "update value",
|
|
549
|
+
value: latestRemote
|
|
550
|
+
}));
|
|
551
|
+
}, REPAIR_CONFIRM_DELAY);
|
|
552
|
+
pendingRepairs.set(editor, {
|
|
553
|
+
signature: first.signature,
|
|
554
|
+
timer
|
|
555
|
+
});
|
|
525
556
|
}
|
|
526
|
-
const listenToEditor = fromCallback(({
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}) => {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
},
|
|
588
|
-
"apply remote patches": ({
|
|
589
|
-
context,
|
|
590
|
-
event
|
|
591
|
-
}) => {
|
|
592
|
-
event.type === "remote patches received" && (debug.remote("applying remote patches %o", event.patches), queueMicrotask(() => {
|
|
593
|
-
const snapshot = context.editor.getSnapshot().context.value, remoteValue = context.getRemoteValue(), coalesced = remoteValue ? toEngineSafePatches(event.patches, remoteValue) : event.patches, patches = filterResolvablePatches(remoteValue ? filterInsertsMissingFromRemoteValue(coalesced, remoteValue) : coalesced, snapshot);
|
|
594
|
-
patches.length !== 0 && context.editor.send({
|
|
595
|
-
type: "patches",
|
|
596
|
-
patches,
|
|
597
|
-
snapshot
|
|
598
|
-
});
|
|
599
|
-
}));
|
|
600
|
-
}
|
|
601
|
-
},
|
|
602
|
-
actors: {
|
|
603
|
-
"listen to editor": listenToEditor,
|
|
604
|
-
"listen to remote": listenToRemote,
|
|
605
|
-
"listen to remote patches": listenToRemotePatches
|
|
606
|
-
}
|
|
557
|
+
const listenToEditor = fromCallback(({ sendBack, input }) => {
|
|
558
|
+
let patchSubscription = input.editor.on("patch", () => {
|
|
559
|
+
unflushedEdits.set(input.editor, !0), sendBack({ type: "patch emitted" });
|
|
560
|
+
}), mutationSubscription = input.editor.on("mutation", (event) => {
|
|
561
|
+
unflushedEdits.set(input.editor, !1), debug.mutation.enabled && debug.mutation("flushed %o", {
|
|
562
|
+
flushText: debugTextOf(event.value),
|
|
563
|
+
snapshotText: debugTextOf(input.editor.getSnapshot().context.value)
|
|
564
|
+
}), sendBack({
|
|
565
|
+
type: "mutation flushed",
|
|
566
|
+
value: event.value,
|
|
567
|
+
patches: event.patches
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
return () => {
|
|
571
|
+
patchSubscription.unsubscribe(), mutationSubscription.unsubscribe();
|
|
572
|
+
};
|
|
573
|
+
}), listenToRemote = fromCallback(({ sendBack, input }) => input.onRemoteValueChange(() => {
|
|
574
|
+
sendBack({ type: "remote value changed" });
|
|
575
|
+
})), listenToRemotePatches = fromCallback(({ sendBack, input }) => input.onRemotePatches?.((patches) => {
|
|
576
|
+
sendBack({
|
|
577
|
+
type: "remote patches received",
|
|
578
|
+
patches
|
|
579
|
+
});
|
|
580
|
+
})), valueSyncMachine = setup({
|
|
581
|
+
types: {
|
|
582
|
+
context: {},
|
|
583
|
+
input: {},
|
|
584
|
+
events: {}
|
|
585
|
+
},
|
|
586
|
+
actions: {
|
|
587
|
+
"send initial value": ({ context }) => {
|
|
588
|
+
context.editor.send({
|
|
589
|
+
type: "update value",
|
|
590
|
+
value: context.getRemoteValue() ?? []
|
|
591
|
+
});
|
|
592
|
+
},
|
|
593
|
+
"push to remote": () => {
|
|
594
|
+
throw Error("push to remote must be provided via .provide()");
|
|
595
|
+
},
|
|
596
|
+
"apply sync": ({ context }) => {
|
|
597
|
+
applySync({
|
|
598
|
+
editor: context.editor,
|
|
599
|
+
getRemoteValue: context.getRemoteValue
|
|
600
|
+
});
|
|
601
|
+
},
|
|
602
|
+
"apply remote patches": ({ context, event }) => {
|
|
603
|
+
event.type === "remote patches received" && (debug.remote("applying remote patches %o", event.patches), queueMicrotask(() => {
|
|
604
|
+
let snapshot = context.editor.getSnapshot().context.value, remoteValue = context.getRemoteValue(), coalesced = remoteValue ? toEngineSafePatches(event.patches, remoteValue) : event.patches, patches = filterResolvablePatches(remoteValue ? filterInsertsMissingFromRemoteValue(coalesced, remoteValue) : coalesced, snapshot);
|
|
605
|
+
patches.length !== 0 && context.editor.send({
|
|
606
|
+
type: "patches",
|
|
607
|
+
patches,
|
|
608
|
+
snapshot
|
|
609
|
+
});
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
actors: {
|
|
614
|
+
"listen to editor": listenToEditor,
|
|
615
|
+
"listen to remote": listenToRemote,
|
|
616
|
+
"listen to remote patches": listenToRemotePatches
|
|
617
|
+
}
|
|
607
618
|
}).createMachine({
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
"mutation flushed": {
|
|
673
|
-
target: "pushing to remote",
|
|
674
|
-
actions: ["push to remote"]
|
|
675
|
-
},
|
|
676
|
-
"remote value changed": {
|
|
677
|
-
actions: ["apply sync"]
|
|
678
|
-
},
|
|
679
|
-
// No immediate repair here: every remote patch batch also updates
|
|
680
|
-
// the store value, so the accompanying 'remote value changed'
|
|
681
|
-
// event runs the whole-value repair right after.
|
|
682
|
-
"remote patches received": {
|
|
683
|
-
actions: ["apply remote patches"]
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
},
|
|
687
|
-
"local write": {
|
|
688
|
-
on: {
|
|
689
|
-
"patch emitted": {},
|
|
690
|
-
"mutation flushed": {
|
|
691
|
-
target: "pushing to remote",
|
|
692
|
-
actions: ["push to remote"]
|
|
693
|
-
},
|
|
694
|
-
"remote value changed": {
|
|
695
|
-
target: "pending sync"
|
|
696
|
-
},
|
|
697
|
-
"remote patches received": {
|
|
698
|
-
target: "pending sync",
|
|
699
|
-
actions: ["apply remote patches"]
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
},
|
|
703
|
-
"pushing to remote": {
|
|
704
|
-
on: {
|
|
705
|
-
"patch emitted": {
|
|
706
|
-
target: "local write"
|
|
707
|
-
},
|
|
708
|
-
"mutation flushed": {
|
|
709
|
-
actions: ["push to remote"]
|
|
710
|
-
},
|
|
711
|
-
// No repair on the push acknowledgment: the editor snapshot can
|
|
712
|
-
// lag the live document while the user keeps typing, so a diff
|
|
713
|
-
// against the store here resurrects deleted text and duplicates
|
|
714
|
-
// in-flight keystrokes. Once truly idle, the store change or the
|
|
715
|
-
// quiescent delay runs the repair with a caught-up snapshot.
|
|
716
|
-
"remote value changed": {
|
|
717
|
-
target: "idle"
|
|
718
|
-
},
|
|
719
|
-
"remote patches received": {
|
|
720
|
-
actions: ["apply remote patches"]
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
},
|
|
724
|
-
"pending sync": {
|
|
725
|
-
on: {
|
|
726
|
-
"patch emitted": {},
|
|
727
|
-
"mutation flushed": {
|
|
728
|
-
target: "pushing to remote",
|
|
729
|
-
actions: ["push to remote"]
|
|
730
|
-
},
|
|
731
|
-
"remote value changed": {},
|
|
732
|
-
"remote patches received": {
|
|
733
|
-
actions: ["apply remote patches"]
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
}
|
|
619
|
+
id: "value sync",
|
|
620
|
+
context: ({ input }) => ({
|
|
621
|
+
editor: input.editor,
|
|
622
|
+
getRemoteValue: input.getRemoteValue,
|
|
623
|
+
onRemoteValueChange: input.onRemoteValueChange,
|
|
624
|
+
onRemotePatches: input.onRemotePatches
|
|
625
|
+
}),
|
|
626
|
+
entry: ["send initial value"],
|
|
627
|
+
invoke: [
|
|
628
|
+
{
|
|
629
|
+
src: "listen to editor",
|
|
630
|
+
input: ({ context }) => ({ editor: context.editor })
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
src: "listen to remote",
|
|
634
|
+
input: ({ context }) => ({ onRemoteValueChange: context.onRemoteValueChange })
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
src: "listen to remote patches",
|
|
638
|
+
input: ({ context }) => ({ onRemotePatches: context.onRemotePatches })
|
|
639
|
+
}
|
|
640
|
+
],
|
|
641
|
+
initial: "idle",
|
|
642
|
+
states: {
|
|
643
|
+
idle: {
|
|
644
|
+
after: { 500: { actions: ["apply sync"] } },
|
|
645
|
+
on: {
|
|
646
|
+
"patch emitted": { target: "local write" },
|
|
647
|
+
"mutation flushed": {
|
|
648
|
+
target: "pushing to remote",
|
|
649
|
+
actions: ["push to remote"]
|
|
650
|
+
},
|
|
651
|
+
"remote value changed": { actions: ["apply sync"] },
|
|
652
|
+
"remote patches received": { actions: ["apply remote patches"] }
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
"local write": { on: {
|
|
656
|
+
"patch emitted": {},
|
|
657
|
+
"mutation flushed": {
|
|
658
|
+
target: "pushing to remote",
|
|
659
|
+
actions: ["push to remote"]
|
|
660
|
+
},
|
|
661
|
+
"remote value changed": { target: "pending sync" },
|
|
662
|
+
"remote patches received": {
|
|
663
|
+
target: "pending sync",
|
|
664
|
+
actions: ["apply remote patches"]
|
|
665
|
+
}
|
|
666
|
+
} },
|
|
667
|
+
"pushing to remote": { on: {
|
|
668
|
+
"patch emitted": { target: "local write" },
|
|
669
|
+
"mutation flushed": { actions: ["push to remote"] },
|
|
670
|
+
"remote value changed": { target: "idle" },
|
|
671
|
+
"remote patches received": { actions: ["apply remote patches"] }
|
|
672
|
+
} },
|
|
673
|
+
"pending sync": { on: {
|
|
674
|
+
"patch emitted": {},
|
|
675
|
+
"mutation flushed": {
|
|
676
|
+
target: "pushing to remote",
|
|
677
|
+
actions: ["push to remote"]
|
|
678
|
+
},
|
|
679
|
+
"remote value changed": {},
|
|
680
|
+
"remote patches received": { actions: ["apply remote patches"] }
|
|
681
|
+
} }
|
|
682
|
+
}
|
|
738
683
|
});
|
|
739
684
|
function isRemotePatchesEvent(event) {
|
|
740
|
-
|
|
685
|
+
return event.type === "remote-patches";
|
|
741
686
|
}
|
|
742
687
|
function getPublishedDocumentId(id) {
|
|
743
|
-
|
|
688
|
+
return id.startsWith("drafts.") ? id.slice(7) : id.startsWith("versions.") ? id.split(".").slice(2).join(".") : id;
|
|
744
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* @public
|
|
692
|
+
*/
|
|
745
693
|
function SDKValuePlugin(props) {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
documentId,
|
|
784
|
-
documentType
|
|
785
|
-
}, sanityPatches),
|
|
786
|
-
preserveOperations: !0
|
|
787
|
-
};
|
|
788
|
-
applyActions(action);
|
|
789
|
-
}, $[9] = applyActions, $[10] = documentId, $[11] = documentType, $[12] = path, $[13] = t2) : t2 = $[13];
|
|
790
|
-
const pushPatches = t2;
|
|
791
|
-
let t3;
|
|
792
|
-
return $[14] !== getCurrent || $[15] !== onRemotePatches || $[16] !== pushPatches || $[17] !== setSdkValue || $[18] !== subscribe ? (t3 = /* @__PURE__ */ jsx(ValueSyncPlugin, { getRemoteValue: getCurrent, pushValue: setSdkValue, onRemoteValueChange: subscribe, onRemotePatches, pushPatches }), $[14] = getCurrent, $[15] = onRemotePatches, $[16] = pushPatches, $[17] = setSdkValue, $[18] = subscribe, $[19] = t3) : t3 = $[19], t3;
|
|
694
|
+
let $ = c(20), { documentId, documentType, path } = props, setSdkValue = useEditDocument(props), instance = useSanityInstance(props), applyActions = useApplyDocumentActions(), t0;
|
|
695
|
+
$[0] !== documentId || $[1] !== documentType || $[2] !== instance || $[3] !== path ? (t0 = getDocumentState(instance, {
|
|
696
|
+
documentId,
|
|
697
|
+
documentType,
|
|
698
|
+
path
|
|
699
|
+
}), $[0] = documentId, $[1] = documentType, $[2] = instance, $[3] = path, $[4] = t0) : t0 = $[4];
|
|
700
|
+
let { getCurrent, subscribe } = t0, t1;
|
|
701
|
+
$[5] !== documentId || $[6] !== instance || $[7] !== path ? (t1 = (callback) => subscribeDocumentEvents(instance, { eventHandler: (event) => {
|
|
702
|
+
let candidate = event;
|
|
703
|
+
if (!isRemotePatchesEvent(candidate) || candidate.origin !== "remote" || getPublishedDocumentId(candidate.documentId) !== getPublishedDocumentId(documentId)) return;
|
|
704
|
+
let patches;
|
|
705
|
+
try {
|
|
706
|
+
patches = scopeRemotePatches(candidate.patches, path);
|
|
707
|
+
} catch {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
patches && patches.length > 0 && callback(patches);
|
|
711
|
+
} }), $[5] = documentId, $[6] = instance, $[7] = path, $[8] = t1) : t1 = $[8];
|
|
712
|
+
let onRemotePatches = t1, t2;
|
|
713
|
+
$[9] !== applyActions || $[10] !== documentId || $[11] !== documentType || $[12] !== path ? (t2 = (patches_0) => {
|
|
714
|
+
let sanityPatches = convertPatchesToSanity(patches_0, { prefix: path }), action = {
|
|
715
|
+
...editDocument({
|
|
716
|
+
documentId,
|
|
717
|
+
documentType
|
|
718
|
+
}, sanityPatches),
|
|
719
|
+
preserveOperations: !0
|
|
720
|
+
};
|
|
721
|
+
applyActions(action);
|
|
722
|
+
}, $[9] = applyActions, $[10] = documentId, $[11] = documentType, $[12] = path, $[13] = t2) : t2 = $[13];
|
|
723
|
+
let pushPatches = t2, t3;
|
|
724
|
+
return $[14] !== getCurrent || $[15] !== onRemotePatches || $[16] !== pushPatches || $[17] !== setSdkValue || $[18] !== subscribe ? (t3 = /* @__PURE__ */ jsx(ValueSyncPlugin, {
|
|
725
|
+
getRemoteValue: getCurrent,
|
|
726
|
+
pushValue: setSdkValue,
|
|
727
|
+
onRemoteValueChange: subscribe,
|
|
728
|
+
onRemotePatches,
|
|
729
|
+
pushPatches
|
|
730
|
+
}), $[14] = getCurrent, $[15] = onRemotePatches, $[16] = pushPatches, $[17] = setSdkValue, $[18] = subscribe, $[19] = t3) : t3 = $[19], t3;
|
|
793
731
|
}
|
|
732
|
+
/**
|
|
733
|
+
* NOTE: You are probably looking for SDKValuePlugin instead of this.
|
|
734
|
+
* This is a lower-level plugin that only handles syncing the value
|
|
735
|
+
* between the editor and a remote source. It does not know anything
|
|
736
|
+
* about Sanity documents or how to fetch/update them.
|
|
737
|
+
*
|
|
738
|
+
* May be removed in the future, do not rely on this directly.
|
|
739
|
+
*
|
|
740
|
+
* @internal
|
|
741
|
+
*/
|
|
794
742
|
function ValueSyncPlugin(props) {
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
debug.push("pushing patches %o", mergeable), pushPatches(mergeable);
|
|
815
|
-
return;
|
|
816
|
-
} catch {
|
|
817
|
-
}
|
|
818
|
-
debug.push.enabled && debug.push("pushing whole value %s", debugTextOf(event.value ?? context.editor.getSnapshot().context.value)), pushValue(event.value ?? context.editor.getSnapshot().context.value);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}), $[0] = pushPatches, $[1] = pushValue, $[2] = t0) : t0 = $[2];
|
|
823
|
-
let t1;
|
|
824
|
-
return $[3] !== editor || $[4] !== getRemoteValue || $[5] !== onRemotePatches || $[6] !== onRemoteValueChange ? (t1 = {
|
|
825
|
-
input: {
|
|
826
|
-
editor,
|
|
827
|
-
getRemoteValue,
|
|
828
|
-
onRemoteValueChange,
|
|
829
|
-
onRemotePatches
|
|
830
|
-
}
|
|
831
|
-
}, $[3] = editor, $[4] = getRemoteValue, $[5] = onRemotePatches, $[6] = onRemoteValueChange, $[7] = t1) : t1 = $[7], useActorRef(t0, t1), null;
|
|
743
|
+
let $ = c(8), { getRemoteValue, pushValue, onRemoteValueChange, onRemotePatches, pushPatches } = props, editor = useEditor(), t0;
|
|
744
|
+
$[0] !== pushPatches || $[1] !== pushValue ? (t0 = valueSyncMachine.provide({ actions: { "push to remote": (t1) => {
|
|
745
|
+
let { context, event } = t1;
|
|
746
|
+
if (event.type === "mutation flushed") {
|
|
747
|
+
if (pushPatches && event.patches.length > 0) try {
|
|
748
|
+
let mergeable = toMergeableMarkDefsPatches(event.patches, context.getRemoteValue);
|
|
749
|
+
debug.push("pushing patches %o", mergeable), pushPatches(mergeable);
|
|
750
|
+
return;
|
|
751
|
+
} catch {}
|
|
752
|
+
debug.push.enabled && debug.push("pushing whole value %s", debugTextOf(event.value ?? context.editor.getSnapshot().context.value)), pushValue(event.value ?? context.editor.getSnapshot().context.value);
|
|
753
|
+
}
|
|
754
|
+
} } }), $[0] = pushPatches, $[1] = pushValue, $[2] = t0) : t0 = $[2];
|
|
755
|
+
let t1;
|
|
756
|
+
return $[3] !== editor || $[4] !== getRemoteValue || $[5] !== onRemotePatches || $[6] !== onRemoteValueChange ? (t1 = { input: {
|
|
757
|
+
editor,
|
|
758
|
+
getRemoteValue,
|
|
759
|
+
onRemoteValueChange,
|
|
760
|
+
onRemotePatches
|
|
761
|
+
} }, $[3] = editor, $[4] = getRemoteValue, $[5] = onRemotePatches, $[6] = onRemoteValueChange, $[7] = t1) : t1 = $[7], useActorRef(t0, t1), null;
|
|
832
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* Reports the local user's caret in a Portable Text field, so other people in
|
|
765
|
+
* the same document can see where they are.
|
|
766
|
+
*
|
|
767
|
+
* Place it inside `EditorProvider`. Prefer `SDKPortableTextEditable`, which
|
|
768
|
+
* reports and draws in one component; reach for this when you render
|
|
769
|
+
* `PortableTextEditable` yourself.
|
|
770
|
+
*
|
|
771
|
+
* Which document is reported follows the handle's perspective, or the ambient
|
|
772
|
+
* one from `ResourceProvider`. Pass the plain document id and let the
|
|
773
|
+
* perspective select the draft, the published document, or a release version.
|
|
774
|
+
*
|
|
775
|
+
* @public
|
|
776
|
+
*/
|
|
833
777
|
function SDKPresencePlugin(props) {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
return $[3] !== fieldPath || $[4] !== handle || $[5] !== selection ? (t0 = {
|
|
843
|
-
...handle,
|
|
844
|
-
path: fieldPath,
|
|
845
|
-
selection
|
|
846
|
-
}, $[3] = fieldPath, $[4] = handle, $[5] = selection, $[6] = t0) : t0 = $[6], useReportPresence(t0), null;
|
|
778
|
+
let $ = c(7), handle, path;
|
|
779
|
+
$[0] === props ? (handle = $[1], path = $[2]) : ({path, ...handle} = props, $[0] = props, $[1] = handle, $[2] = path);
|
|
780
|
+
let selection = useLocalSelection(), fieldPath = useFieldPath(path), t0;
|
|
781
|
+
return $[3] !== fieldPath || $[4] !== handle || $[5] !== selection ? (t0 = {
|
|
782
|
+
...handle,
|
|
783
|
+
path: fieldPath,
|
|
784
|
+
selection
|
|
785
|
+
}, $[3] = fieldPath, $[4] = handle, $[5] = selection, $[6] = t0) : t0 = $[6], useReportPresence(t0), null;
|
|
847
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* Other people's carets in a Portable Text field, as range decorations.
|
|
789
|
+
*
|
|
790
|
+
* Pass the result to `<PortableTextEditable rangeDecorations={...} />`, or use
|
|
791
|
+
* `SDKPortableTextEditable` and skip the wiring. Each caret stays anchored as
|
|
792
|
+
* the local user types, and disappears when the participant leaves or their
|
|
793
|
+
* session expires.
|
|
794
|
+
*
|
|
795
|
+
* The local user is never included, so an app does not draw its own caret.
|
|
796
|
+
* Participants are counted by session, so the same person in two tabs draws two
|
|
797
|
+
* carets.
|
|
798
|
+
*
|
|
799
|
+
* @public
|
|
800
|
+
*/
|
|
848
801
|
function useSDKPresenceCursors(options) {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
presence
|
|
865
|
-
} = usePresenceForDocument(t0);
|
|
866
|
-
let t1;
|
|
867
|
-
$[7] !== presence ? (t1 = presence.flatMap(_temp), $[7] = presence, $[8] = t1) : t1 = $[8];
|
|
868
|
-
const cursors = t1;
|
|
869
|
-
let t2;
|
|
870
|
-
return $[9] !== cursors || $[10] !== renderCursor ? (t2 = {
|
|
871
|
-
cursors,
|
|
872
|
-
renderCursor
|
|
873
|
-
}, $[9] = cursors, $[10] = renderCursor, $[11] = t2) : t2 = $[11], useRemoteCursors(t2);
|
|
802
|
+
let $ = c(12), handle, path, renderCursor;
|
|
803
|
+
$[0] === options ? (handle = $[1], path = $[2], renderCursor = $[3]) : ({path, renderCursor, ...handle} = options, $[0] = options, $[1] = handle, $[2] = path, $[3] = renderCursor);
|
|
804
|
+
let fieldPath = useFieldPath(path), t0;
|
|
805
|
+
$[4] !== fieldPath || $[5] !== handle ? (t0 = {
|
|
806
|
+
...handle,
|
|
807
|
+
path: fieldPath,
|
|
808
|
+
excludeVersions: !0
|
|
809
|
+
}, $[4] = fieldPath, $[5] = handle, $[6] = t0) : t0 = $[6];
|
|
810
|
+
let { presence } = usePresenceForDocument(t0), t1;
|
|
811
|
+
$[7] === presence ? t1 = $[8] : (t1 = presence.flatMap(_temp), $[7] = presence, $[8] = t1);
|
|
812
|
+
let cursors = t1, t2;
|
|
813
|
+
return $[9] !== cursors || $[10] !== renderCursor ? (t2 = {
|
|
814
|
+
cursors,
|
|
815
|
+
renderCursor
|
|
816
|
+
}, $[9] = cursors, $[10] = renderCursor, $[11] = t2) : t2 = $[11], useRemoteCursors(t2);
|
|
874
817
|
}
|
|
818
|
+
/**
|
|
819
|
+
* The SDK's presence hooks address fields by path array, because a field-level
|
|
820
|
+
* path inside Portable Text needs keyed segments. This package takes the same
|
|
821
|
+
* string expression as `SDKValuePlugin` everywhere and converts here.
|
|
822
|
+
*/
|
|
875
823
|
function _temp(participant) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
824
|
+
return participant.selection ? [{
|
|
825
|
+
sessionId: participant.sessionId,
|
|
826
|
+
selection: participant.selection,
|
|
827
|
+
user: participant.user
|
|
828
|
+
}] : [];
|
|
881
829
|
}
|
|
882
830
|
function useFieldPath(path) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
return $[0] !== path ? (t0 = arrayifyPath(path), $[0] = path, $[1] = t0) : t0 = $[1], t0;
|
|
831
|
+
let $ = c(2), t0;
|
|
832
|
+
return $[0] === path ? t0 = $[1] : (t0 = arrayifyPath(path), $[0] = path, $[1] = t0), t0;
|
|
886
833
|
}
|
|
887
|
-
|
|
834
|
+
/**
|
|
835
|
+
* Mid-tone hues, so a caret stays legible whether the app is light or dark. This
|
|
836
|
+
* package cannot read the app's theme, so it does not try.
|
|
837
|
+
*/
|
|
838
|
+
const CARET_COLORS = [
|
|
839
|
+
"#e0508a",
|
|
840
|
+
"#c05fd8",
|
|
841
|
+
"#7c66e8",
|
|
842
|
+
"#2f8fdd",
|
|
843
|
+
"#1f9c8f",
|
|
844
|
+
"#4f9c2f",
|
|
845
|
+
"#c98a1c",
|
|
846
|
+
"#d4603a"
|
|
847
|
+
];
|
|
848
|
+
/**
|
|
849
|
+
* Picks a stable colour for a participant.
|
|
850
|
+
*
|
|
851
|
+
* Keyed on the user rather than the session, so one person in two tabs draws two
|
|
852
|
+
* carets in the same colour. The Studio colours by user for the same reason.
|
|
853
|
+
*
|
|
854
|
+
* @public
|
|
855
|
+
*/
|
|
888
856
|
function getCaretColor(userId) {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
return CARET_COLORS[hash % CARET_COLORS.length];
|
|
857
|
+
let hash = 0;
|
|
858
|
+
for (let index = 0; index < userId.length; index++) hash = (hash * 31 + userId.charCodeAt(index)) % 1000003;
|
|
859
|
+
return CARET_COLORS[hash % CARET_COLORS.length];
|
|
893
860
|
}
|
|
894
|
-
|
|
861
|
+
/**
|
|
862
|
+
* Draws a remote caret when no `renderCursor` was given: a coloured line with a
|
|
863
|
+
* dot above it, and the participant's name on hover.
|
|
864
|
+
*
|
|
865
|
+
* Deliberately plain, and styled inline so it needs no stylesheet. Pass your own
|
|
866
|
+
* `renderCursor` to match your design.
|
|
867
|
+
*
|
|
868
|
+
* @public
|
|
869
|
+
*/
|
|
870
|
+
const renderDefaultCursor = (cursor) => (props) => /* @__PURE__ */ jsx(DefaultCaret, {
|
|
871
|
+
cursor,
|
|
872
|
+
children: props.children
|
|
873
|
+
});
|
|
895
874
|
function DefaultCaret(props) {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
875
|
+
let $ = c(17), { cursor, children } = props, t0;
|
|
876
|
+
$[0] === cursor.user.sanityUserId ? t0 = $[1] : (t0 = getCaretColor(cursor.user.sanityUserId), $[0] = cursor.user.sanityUserId, $[1] = t0);
|
|
877
|
+
let color = t0, displayName = cursor.user.profile.displayName, t1 = `presence-caret-${cursor.sessionId}`, t2 = `2px solid ${color}`, t3;
|
|
878
|
+
$[2] === t2 ? t3 = $[3] : (t3 = {
|
|
879
|
+
borderLeft: t2,
|
|
880
|
+
marginLeft: -1,
|
|
881
|
+
position: "relative",
|
|
882
|
+
pointerEvents: "none"
|
|
883
|
+
}, $[2] = t2, $[3] = t3);
|
|
884
|
+
let t4 = `presence-caret-dot-${cursor.sessionId}`, t5;
|
|
885
|
+
$[4] === color ? t5 = $[5] : (t5 = {
|
|
886
|
+
backgroundColor: color,
|
|
887
|
+
borderRadius: "50%",
|
|
888
|
+
height: 6,
|
|
889
|
+
left: -1,
|
|
890
|
+
pointerEvents: "auto",
|
|
891
|
+
position: "absolute",
|
|
892
|
+
top: -5,
|
|
893
|
+
transform: "translateX(-50%)",
|
|
894
|
+
width: 6
|
|
895
|
+
}, $[4] = color, $[5] = t5);
|
|
896
|
+
let t6;
|
|
897
|
+
$[6] !== displayName || $[7] !== t4 || $[8] !== t5 ? (t6 = /* @__PURE__ */ jsx("span", {
|
|
898
|
+
"data-testid": t4,
|
|
899
|
+
style: t5,
|
|
900
|
+
title: displayName
|
|
901
|
+
}), $[6] = displayName, $[7] = t4, $[8] = t5, $[9] = t6) : t6 = $[9];
|
|
902
|
+
let t7;
|
|
903
|
+
$[10] !== t1 || $[11] !== t3 || $[12] !== t6 ? (t7 = /* @__PURE__ */ jsx("span", {
|
|
904
|
+
contentEditable: !1,
|
|
905
|
+
"data-testid": t1,
|
|
906
|
+
style: t3,
|
|
907
|
+
children: t6
|
|
908
|
+
}), $[10] = t1, $[11] = t3, $[12] = t6, $[13] = t7) : t7 = $[13];
|
|
909
|
+
let t8;
|
|
910
|
+
return $[14] !== children || $[15] !== t7 ? (t8 = /* @__PURE__ */ jsxs(Fragment, { children: [t7, children] }), $[14] = children, $[15] = t7, $[16] = t8) : t8 = $[16], t8;
|
|
932
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* A `PortableTextEditable` wired to a Sanity document: the field's value syncs
|
|
914
|
+
* both ways, the local user's caret is reported, and other people's carets are
|
|
915
|
+
* drawn.
|
|
916
|
+
*
|
|
917
|
+
* Place it inside `EditorProvider` in place of `PortableTextEditable`. Every
|
|
918
|
+
* other prop is forwarded untouched, and `rangeDecorations` you pass are kept
|
|
919
|
+
* and merged with the presence carets rather than replaced.
|
|
920
|
+
*
|
|
921
|
+
* Nothing else is needed: this replaces a separate `SDKValuePlugin`.
|
|
922
|
+
*
|
|
923
|
+
* @example
|
|
924
|
+
* ```tsx
|
|
925
|
+
* <EditorProvider initialConfig={{schemaDefinition}}>
|
|
926
|
+
* <SDKPortableTextEditable
|
|
927
|
+
* {...documentHandle}
|
|
928
|
+
* path="content"
|
|
929
|
+
* renderCursor={({user}) => (props) => (
|
|
930
|
+
* <Caret user={user}>{props.children}</Caret>
|
|
931
|
+
* )}
|
|
932
|
+
* />
|
|
933
|
+
* </EditorProvider>
|
|
934
|
+
* ```
|
|
935
|
+
*
|
|
936
|
+
* @public
|
|
937
|
+
*/
|
|
933
938
|
function SDKPortableTextEditable(props) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
t5
|
|
967
|
-
] }), $[21] = t3, $[22] = t4, $[23] = t5, $[24] = t6) : t6 = $[24], t6;
|
|
939
|
+
let $ = c(25), editableProps, handle, path, rangeDecorations, t0;
|
|
940
|
+
if ($[0] !== props) {
|
|
941
|
+
let { handle: t1, path: t2, renderCursor, rangeDecorations: t3, editableProps: t4 } = splitEditableProps(props);
|
|
942
|
+
handle = t1, path = t2, rangeDecorations = t3, editableProps = t4, t0 = resolveCursorRenderer(renderCursor), $[0] = props, $[1] = editableProps, $[2] = handle, $[3] = path, $[4] = rangeDecorations, $[5] = t0;
|
|
943
|
+
} else editableProps = $[1], handle = $[2], path = $[3], rangeDecorations = $[4], t0 = $[5];
|
|
944
|
+
let renderer = t0, t1;
|
|
945
|
+
$[6] !== handle || $[7] !== path || $[8] !== renderer.renderCursor ? (t1 = {
|
|
946
|
+
...handle,
|
|
947
|
+
path,
|
|
948
|
+
renderCursor: renderer.renderCursor
|
|
949
|
+
}, $[6] = handle, $[7] = path, $[8] = renderer.renderCursor, $[9] = t1) : t1 = $[9];
|
|
950
|
+
let cursors = useSDKPresenceCursors(t1), t2;
|
|
951
|
+
$[10] !== cursors || $[11] !== rangeDecorations || $[12] !== renderer.drawCursors ? (t2 = mergePresenceDecorations(rangeDecorations, cursors, renderer.drawCursors), $[10] = cursors, $[11] = rangeDecorations, $[12] = renderer.drawCursors, $[13] = t2) : t2 = $[13];
|
|
952
|
+
let decorations = t2, t3;
|
|
953
|
+
$[14] !== decorations || $[15] !== editableProps ? (t3 = /* @__PURE__ */ jsx(PortableTextEditable, {
|
|
954
|
+
...editableProps,
|
|
955
|
+
rangeDecorations: decorations
|
|
956
|
+
}), $[14] = decorations, $[15] = editableProps, $[16] = t3) : t3 = $[16];
|
|
957
|
+
let t4, t5;
|
|
958
|
+
$[17] !== handle || $[18] !== path ? (t4 = /* @__PURE__ */ jsx(SDKValuePlugin, {
|
|
959
|
+
...handle,
|
|
960
|
+
path
|
|
961
|
+
}), t5 = /* @__PURE__ */ jsx(SDKPresencePlugin, {
|
|
962
|
+
...handle,
|
|
963
|
+
path
|
|
964
|
+
}), $[17] = handle, $[18] = path, $[19] = t4, $[20] = t5) : (t4 = $[19], t5 = $[20]);
|
|
965
|
+
let t6;
|
|
966
|
+
return $[21] !== t3 || $[22] !== t4 || $[23] !== t5 ? (t6 = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
967
|
+
t3,
|
|
968
|
+
t4,
|
|
969
|
+
t5
|
|
970
|
+
] }), $[21] = t3, $[22] = t4, $[23] = t5, $[24] = t6) : t6 = $[24], t6;
|
|
968
971
|
}
|
|
972
|
+
/**
|
|
973
|
+
* Splits the props into the document handle, this component's own props, and
|
|
974
|
+
* what is forwarded to `PortableTextEditable`. Keeping handle fields out of the
|
|
975
|
+
* forwarded set is what stops them reaching the DOM as attributes.
|
|
976
|
+
*
|
|
977
|
+
* @internal
|
|
978
|
+
*/
|
|
969
979
|
function splitEditableProps(props) {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
// `perspective: undefined` would override what `ResourceProvider` set rather
|
|
989
|
-
// than defer to it, and the field would sync and report against the draft
|
|
990
|
-
// instead of the release the app is showing.
|
|
991
|
-
handle: {
|
|
992
|
-
documentId,
|
|
993
|
-
documentType,
|
|
994
|
-
..."projectId" in props && {
|
|
995
|
-
projectId
|
|
996
|
-
},
|
|
997
|
-
..."dataset" in props && {
|
|
998
|
-
dataset
|
|
999
|
-
},
|
|
1000
|
-
..."resource" in props && {
|
|
1001
|
-
resource
|
|
1002
|
-
},
|
|
1003
|
-
..."resourceName" in props && {
|
|
1004
|
-
resourceName
|
|
1005
|
-
},
|
|
1006
|
-
..."source" in props && {
|
|
1007
|
-
source
|
|
1008
|
-
},
|
|
1009
|
-
..."liveEdit" in props && {
|
|
1010
|
-
liveEdit
|
|
1011
|
-
},
|
|
1012
|
-
..."perspective" in props && {
|
|
1013
|
-
perspective
|
|
1014
|
-
}
|
|
1015
|
-
},
|
|
1016
|
-
path,
|
|
1017
|
-
renderCursor,
|
|
1018
|
-
rangeDecorations,
|
|
1019
|
-
editableProps
|
|
1020
|
-
};
|
|
980
|
+
let { documentId, documentType, projectId, dataset, resource, resourceName, source, liveEdit, perspective, path, renderCursor, rangeDecorations, ...editableProps } = props;
|
|
981
|
+
return {
|
|
982
|
+
handle: {
|
|
983
|
+
documentId,
|
|
984
|
+
documentType,
|
|
985
|
+
..."projectId" in props && { projectId },
|
|
986
|
+
..."dataset" in props && { dataset },
|
|
987
|
+
..."resource" in props && { resource },
|
|
988
|
+
..."resourceName" in props && { resourceName },
|
|
989
|
+
..."source" in props && { source },
|
|
990
|
+
..."liveEdit" in props && { liveEdit },
|
|
991
|
+
..."perspective" in props && { perspective }
|
|
992
|
+
},
|
|
993
|
+
path,
|
|
994
|
+
renderCursor,
|
|
995
|
+
rangeDecorations,
|
|
996
|
+
editableProps
|
|
997
|
+
};
|
|
1021
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* Decides which caret component to draw with, and whether to draw at all.
|
|
1001
|
+
*
|
|
1002
|
+
* Presence is subscribed to either way, because hooks cannot be called
|
|
1003
|
+
* conditionally, so switching carets off discards the decorations rather than
|
|
1004
|
+
* skipping the work.
|
|
1005
|
+
*
|
|
1006
|
+
* @internal
|
|
1007
|
+
*/
|
|
1022
1008
|
function resolveCursorRenderer(renderCursor) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1009
|
+
return {
|
|
1010
|
+
renderCursor: renderCursor ?? renderDefaultCursor,
|
|
1011
|
+
drawCursors: renderCursor !== null
|
|
1012
|
+
};
|
|
1027
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Appends the presence carets to whatever decorations the caller passed, so
|
|
1016
|
+
* theirs survive. The Studio merges the same way. When carets are switched off
|
|
1017
|
+
* the caller's own decorations pass straight through.
|
|
1018
|
+
*
|
|
1019
|
+
* @internal
|
|
1020
|
+
*/
|
|
1028
1021
|
function mergePresenceDecorations(rangeDecorations, cursors, drawCursors) {
|
|
1029
|
-
|
|
1022
|
+
return drawCursors ? [...rangeDecorations ?? [], ...cursors] : rangeDecorations;
|
|
1030
1023
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1024
|
+
/**
|
|
1025
|
+
* Splitting the props by hand is what keeps document handle fields off the DOM,
|
|
1026
|
+
* so every field has to be listed above. `sdk-editable.test.ts` fails to compile
|
|
1027
|
+
* if `DocumentHandle` gains one. Note that `@sanity/sdk-react` adds fields to the
|
|
1028
|
+
* core handle, so it has to be read from there.
|
|
1029
|
+
*/
|
|
1030
|
+
export { SDKPortableTextEditable, SDKPresencePlugin, SDKValuePlugin, ValueSyncPlugin, useSDKPresenceCursors };
|
|
1031
|
+
|
|
1032
|
+
//# sourceMappingURL=index.js.map
|