@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.js CHANGED
@@ -1,1038 +1,1032 @@
1
1
  import { c } from "react/compiler-runtime";
2
- import { useEditDocument, useSanityInstance, useApplyDocumentActions, getDocumentState, subscribeDocumentEvents, editDocument, useReportPresence, usePresenceForDocument } from "@sanity/sdk-react";
2
+ import { editDocument, getDocumentState, subscribeDocumentEvents, useApplyDocumentActions, useEditDocument, usePresenceForDocument, useReportPresence, useSanityInstance } from "@sanity/sdk-react";
3
3
  import { useState } from "react";
4
- import { useEditor, useEditorSelector, PortableTextEditable } from "@portabletext/editor";
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
- return selection === null ? null : {
16
- anchor: selection.focus,
17
- focus: selection.focus
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
- return override && isEqualSelections(override.reported, cursor.selection) ? override.current : collapseToCaret(cursor.selection);
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
- const 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));
25
- if (alreadyRecorded && staleKeys.length === 0)
26
- return overrides;
27
- const next = new Map(overrides);
28
- for (const key of staleKeys)
29
- next.delete(key);
30
- return next.set(cursor.sessionId, {
31
- reported: cursor.selection,
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
- const {
37
- cursors,
38
- overrides,
39
- renderCursor,
40
- onCursorMoved
41
- } = options, decorations = [];
42
- for (const cursor of cursors) {
43
- const selection = resolveCursorSelection(cursor, overrides.get(cursor.sessionId));
44
- selection !== null && decorations.push({
45
- component: renderCursor(cursor),
46
- selection,
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
- const editor = useEditor();
57
- return useEditorSelector(editor, getSelection, isEqualSelections);
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
- const $ = c(7), {
61
- cursors,
62
- renderCursor
63
- } = options, [overrides, setOverrides] = useState(EMPTY_OVERRIDES);
64
- let t0;
65
- $[0] !== cursors ? (t0 = (cursor, selection) => {
66
- setOverrides((previous) => recordCursorMove(previous, cursors, cursor, selection));
67
- }, $[0] = cursors, $[1] = t0) : t0 = $[1];
68
- const onCursorMoved = t0;
69
- let t1;
70
- return $[2] !== cursors || $[3] !== onCursorMoved || $[4] !== overrides || $[5] !== renderCursor ? (t1 = toRangeDecorations({
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
- const namespace = `${rootName}${name}`;
80
- return rawDebug && rawDebug.enabled(namespace) ? rawDebug(namespace) : rawDebug(rootName);
120
+ let namespace = `${rootName}${name}`;
121
+ return rawDebug && rawDebug.enabled(namespace) ? rawDebug(namespace) : rawDebug(rootName);
81
122
  }
82
123
  const debug = {
83
- mutation: createDebugger("mutation"),
84
- push: createDebugger("push"),
85
- remote: createDebugger("remote"),
86
- repair: createDebugger("repair")
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
- node.base && (yield* getSegments(node.base)), node.segment.type !== "This" && (yield node.segment);
130
+ node.base && (yield* getSegments(node.base)), node.segment.type !== "This" && (yield node.segment);
90
131
  }
91
132
  function isKeyPath(node) {
92
- return node.type !== "Path" || node.base || node.recursive || node.segment.type !== "Identifier" ? !1 : node.segment.name === "_key";
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
- const node = parsePath(pathExpr);
96
- if (!node)
97
- return [];
98
- if (node.type !== "Path")
99
- throw new Error(ARRAYIFY_ERROR_MESSAGE);
100
- return Array.from(getSegments(node)).map((segment) => {
101
- if (segment.type === "Identifier")
102
- return segment.name;
103
- if (segment.type !== "Subscript")
104
- throw new Error(ARRAYIFY_ERROR_MESSAGE);
105
- if (segment.elements.length !== 1)
106
- throw new Error(ARRAYIFY_ERROR_MESSAGE);
107
- const [element] = segment.elements;
108
- if (element.type === "Number")
109
- return element.value;
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
- return patches.flatMap((p) => Object.entries(p).flatMap(([type, values]) => {
127
- const origin = "remote";
128
- switch (type) {
129
- case "set":
130
- case "setIfMissing":
131
- case "diffMatchPatch":
132
- case "inc":
133
- case "dec":
134
- return Object.entries(values).map(([pathExpr, value]) => ({
135
- type,
136
- value,
137
- origin,
138
- path: arrayifyPath(pathExpr)
139
- }));
140
- case "unset":
141
- return Array.isArray(values) ? values.map(arrayifyPath).map((path) => ({
142
- type,
143
- origin,
144
- path
145
- })) : [];
146
- case "insert": {
147
- const {
148
- items,
149
- ...rest
150
- } = values, position = Object.keys(rest).at(0);
151
- if (!position)
152
- return [];
153
- const pathExpr = rest[position];
154
- return [{
155
- type,
156
- origin,
157
- position,
158
- path: arrayifyPath(pathExpr),
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
- let result = "";
170
- for (const segment of path)
171
- if (typeof segment == "string")
172
- result = result === "" ? segment : `${result}.${segment}`;
173
- else if (typeof segment == "number")
174
- result = `${result}[${segment}]`;
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
- return expression === "" ? prefix : expression.startsWith("[") ? `${prefix}${expression}` : `${prefix}.${expression}`;
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
- return patches.map((patch) => {
186
- const pathExpression = prefixPathExpression(options.prefix, stringifyPatchPath(patch.path));
187
- switch (patch.type) {
188
- case "set":
189
- return {
190
- set: {
191
- [pathExpression]: patch.value
192
- }
193
- };
194
- case "setIfMissing":
195
- return {
196
- setIfMissing: {
197
- [pathExpression]: patch.value
198
- }
199
- };
200
- case "unset":
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
- 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;
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
- return a.length === b.length && a.every((segment, index) => segmentsEqual(segment, b[index]));
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
- let expectNode = !0;
246
- for (let index = 0; index < path.length; index++) {
247
- const segment = path[index];
248
- if (typeof segment == "string")
249
- expectNode = segment === "children";
250
- else {
251
- if (!expectNode)
252
- return path.slice(0, index);
253
- expectNode = !1;
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
- let current = value;
260
- for (const segment of path) {
261
- if (current == null)
262
- return;
263
- if (typeof segment == "number") {
264
- if (!Array.isArray(current))
265
- return;
266
- current = current[segment < 0 ? current.length + segment : segment];
267
- } else if (typeof segment == "string") {
268
- if (typeof current != "object" || Array.isArray(current))
269
- return;
270
- current = current[segment];
271
- } else {
272
- if (Array.isArray(segment) || !Array.isArray(current))
273
- return;
274
- current = current.find((item) => typeof item == "object" && item !== null && !Array.isArray(item) && item._key === segment._key);
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
- const safe = [], sidecarPaths = [];
281
- for (const patch of patches) {
282
- const sidecarPath = findSidecarArrayPath(patch.path);
283
- if (!sidecarPath) {
284
- safe.push(patch);
285
- continue;
286
- }
287
- sidecarPaths.some((existing) => pathsEqual(existing, sidecarPath)) || sidecarPaths.push(sidecarPath);
288
- }
289
- for (const sidecarPath of sidecarPaths) {
290
- const value = getValueAtPath(targetValue, sidecarPath);
291
- safe.push(value === void 0 ? {
292
- type: "unset",
293
- path: sidecarPath,
294
- origin: "remote"
295
- } : {
296
- type: "set",
297
- path: sidecarPath,
298
- value,
299
- origin: "remote"
300
- });
301
- }
302
- return safe;
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
- return patches.flatMap((patch) => {
306
- if (patch.type !== "insert")
307
- return [patch];
308
- const parentValue = getValueAtPath(remoteValue, patch.path.slice(0, -1));
309
- if (!Array.isArray(parentValue))
310
- return [patch];
311
- const items = patch.items.filter((item) => {
312
- const itemKey = getKey(item);
313
- return itemKey ? parentValue.some((candidate) => getKey(candidate) === itemKey) : !0;
314
- });
315
- return items.length > 0 ? [{
316
- ...patch,
317
- items
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
- if (typeof value != "object" || value === null || Array.isArray(value))
323
- return;
324
- const key = value._key;
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
- return patches.flatMap((patch) => {
329
- if (patch.type !== "set" || patch.path.at(-1) !== "markDefs" || !Array.isArray(patch.value))
330
- return [patch];
331
- const currentValue = getCurrentValue();
332
- if (!currentValue)
333
- return [patch];
334
- const root = currentValue, storeMarkDefs = getValueAtPath(root, patch.path), storeBlock = getValueAtPath(root, patch.path.slice(0, -1));
335
- if (!Array.isArray(storeMarkDefs) || typeof storeBlock != "object" || storeBlock === null)
336
- return [patch];
337
- const local = patch.value, store = storeMarkDefs;
338
- if (local.some((item) => item._key === void 0))
339
- return [patch];
340
- const 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));
341
- if (inserted.length > 0) {
342
- for (const item of inserted)
343
- item._key !== void 0 && ops.push({
344
- type: "unset",
345
- origin,
346
- path: [...patch.path, {
347
- _key: item._key
348
- }]
349
- });
350
- ops.push({
351
- type: "insert",
352
- origin,
353
- position: "after",
354
- path: [...patch.path, -1],
355
- items: inserted
356
- });
357
- }
358
- for (const item of local) {
359
- const existing = storeByKey.get(item._key);
360
- existing && JSON.stringify(existing) !== JSON.stringify(item) && ops.push({
361
- type: "set",
362
- origin,
363
- path: [...patch.path, {
364
- _key: item._key
365
- }],
366
- value: item
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
- if (!value)
382
- return !0;
383
- const root = value;
384
- switch (patch.type) {
385
- // unset needs the node itself; insert needs the sibling at `path`;
386
- // diffMatchPatch needs the existing string
387
- case "unset":
388
- case "insert":
389
- case "diffMatchPatch":
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
- let projected = value;
400
- const resolvable = [];
401
- for (const patch of patches)
402
- if (canApplyToValue(patch, projected) && (resolvable.push(patch), projected))
403
- try {
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
- const prefix = arrayifyPath(fieldPath), converted = convertPatches(patches), scoped = [];
411
- for (const patch of converted) {
412
- const overlap = Math.min(patch.path.length, prefix.length);
413
- let touchesField = !0;
414
- for (let index = 0; index < overlap; index++)
415
- if (!segmentsEqual(patch.path[index], prefix[index])) {
416
- touchesField = !1;
417
- break;
418
- }
419
- if (touchesField) {
420
- if (patch.path.length <= prefix.length)
421
- return null;
422
- scoped.push({
423
- ...patch,
424
- path: patch.path.slice(prefix.length)
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
- return Array.isArray(value) ? value.map((block) => Array.isArray(block.children) ? (block.children ?? []).map((child) => child.text ?? "").join("") : "").join(`
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
- const REPAIR_CONFIRM_DELAY = (
435
- // @ts-expect-error - dot notation required for Vite to replace at build time
436
- process.env.NODE_ENV === "test" ? 150 : 1e3
437
- ), pendingRepairs = /* @__PURE__ */ new WeakMap(), unflushedEdits = /* @__PURE__ */ new WeakMap();
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
- const snapshot = editor.getSnapshot().context.value, stateSignature = JSON.stringify([snapshot, remoteValue]);
440
- try {
441
- return {
442
- patches: toEngineSafePatches(convertPatches(diffValue(snapshot, remoteValue)), remoteValue),
443
- convertible: !0,
444
- signature: stateSignature
445
- };
446
- } catch {
447
- return {
448
- patches: [],
449
- convertible: !1,
450
- signature: `!${stateSignature}`
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
- const pending = pendingRepairs.get(editor);
456
- pending && (clearTimeout(pending.timer), pendingRepairs.delete(editor));
493
+ let pending = pendingRepairs.get(editor);
494
+ pending && (clearTimeout(pending.timer), pendingRepairs.delete(editor));
457
495
  }
458
- function applySync({
459
- editor,
460
- getRemoteValue
461
- }) {
462
- const remoteValue = getRemoteValue();
463
- if (!remoteValue)
464
- return;
465
- const first = computeRepair(editor, remoteValue);
466
- if (first.convertible && first.patches.length === 0) {
467
- cancelPendingRepair(editor);
468
- return;
469
- }
470
- debug.repair.enabled && debug.repair("editor and store texts diverged %o", {
471
- editorText: debugTextOf(editor.getSnapshot().context.value),
472
- remoteText: debugTextOf(remoteValue)
473
- });
474
- const pending = pendingRepairs.get(editor);
475
- if (pending && pending.signature === first.signature)
476
- return;
477
- cancelPendingRepair(editor);
478
- const timer = setTimeout(() => {
479
- if (pendingRepairs.delete(editor), unflushedEdits.get(editor)) {
480
- applySync({
481
- editor,
482
- getRemoteValue
483
- });
484
- return;
485
- }
486
- const latestRemote = getRemoteValue();
487
- if (!latestRemote)
488
- return;
489
- const second = computeRepair(editor, latestRemote);
490
- if (second.convertible && second.patches.length === 0)
491
- return;
492
- if (second.signature !== first.signature) {
493
- applySync({
494
- editor,
495
- getRemoteValue
496
- });
497
- return;
498
- }
499
- if (!second.convertible) {
500
- debug.repair("escalating to whole-value sync (unconvertible diff)"), editor.send({
501
- type: "update value",
502
- value: latestRemote
503
- });
504
- return;
505
- }
506
- const snapshot = editor.getSnapshot().context.value;
507
- debug.repair("applying confirmed repair patches %o", second.patches), editor.send({
508
- type: "patches",
509
- patches: second.patches,
510
- snapshot
511
- });
512
- const valueAfterPatches = editor.getSnapshot().context.value;
513
- diffValue(valueAfterPatches, latestRemote).length > 0 && (debug.repair.enabled && debug.repair("escalating to whole-value sync %o", {
514
- editorText: debugTextOf(valueAfterPatches),
515
- remoteText: debugTextOf(latestRemote)
516
- }), editor.send({
517
- type: "update value",
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
- sendBack,
528
- input
529
- }) => {
530
- const patchSubscription = input.editor.on("patch", () => {
531
- unflushedEdits.set(input.editor, !0), sendBack({
532
- type: "patch emitted"
533
- });
534
- }), mutationSubscription = input.editor.on("mutation", (event) => {
535
- unflushedEdits.set(input.editor, !1), debug.mutation.enabled && debug.mutation("flushed %o", {
536
- flushText: debugTextOf(event.value),
537
- snapshotText: debugTextOf(input.editor.getSnapshot().context.value)
538
- }), sendBack({
539
- type: "mutation flushed",
540
- value: event.value,
541
- patches: event.patches
542
- });
543
- });
544
- return () => {
545
- patchSubscription.unsubscribe(), mutationSubscription.unsubscribe();
546
- };
547
- }), listenToRemote = fromCallback(({
548
- sendBack,
549
- input
550
- }) => input.onRemoteValueChange(() => {
551
- sendBack({
552
- type: "remote value changed"
553
- });
554
- })), listenToRemotePatches = fromCallback(({
555
- sendBack,
556
- input
557
- }) => input.onRemotePatches?.((patches) => {
558
- sendBack({
559
- type: "remote patches received",
560
- patches
561
- });
562
- })), QUIESCENT_REPAIR_DELAY = 500, valueSyncMachine = setup({
563
- types: {
564
- context: {},
565
- input: {},
566
- events: {}
567
- },
568
- actions: {
569
- "send initial value": ({
570
- context
571
- }) => {
572
- context.editor.send({
573
- type: "update value",
574
- value: context.getRemoteValue() ?? []
575
- });
576
- },
577
- "push to remote": () => {
578
- throw new Error("push to remote must be provided via .provide()");
579
- },
580
- "apply sync": ({
581
- context
582
- }) => {
583
- applySync({
584
- editor: context.editor,
585
- getRemoteValue: context.getRemoteValue
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
- id: "value sync",
609
- context: ({
610
- input
611
- }) => ({
612
- editor: input.editor,
613
- getRemoteValue: input.getRemoteValue,
614
- onRemoteValueChange: input.onRemoteValueChange,
615
- onRemotePatches: input.onRemotePatches
616
- }),
617
- entry: ["send initial value"],
618
- invoke: [{
619
- src: "listen to editor",
620
- input: ({
621
- context
622
- }) => ({
623
- editor: context.editor
624
- })
625
- }, {
626
- src: "listen to remote",
627
- input: ({
628
- context
629
- }) => ({
630
- onRemoteValueChange: context.onRemoteValueChange
631
- })
632
- }, {
633
- src: "listen to remote patches",
634
- input: ({
635
- context
636
- }) => ({
637
- onRemotePatches: context.onRemotePatches
638
- })
639
- }],
640
- // Two invariants, learned the hard way:
641
- //
642
- // 1. EVERY 'mutation flushed' event must be pushed, in every state. The
643
- // editor emits mutation events in bursts (one per input batch, e.g.
644
- // each backspace of a quick delete), and any state without a handler
645
- // silently drops the flush. A dropped flush permanently diverges the
646
- // store from the editor, and the whole-value repair then "heals" the
647
- // editor backwards, resurrecting deleted text.
648
- // 2. The whole-value repair must only run when no local edits can be in
649
- // flight (quiescent 'idle'). Running it mid-typing diffs the editor
650
- // against a store that lags the user's keystrokes and stomps them.
651
- //
652
- // Operational patches from other clients still apply immediately in
653
- // every state; the editor merges them with in-flight local changes.
654
- initial: "idle",
655
- states: {
656
- idle: {
657
- // One-shot repair after a quiet period. Covers divergence left by
658
- // best-effort patch application while local edits were in flight
659
- // (those states never repair; see below) when no further store
660
- // event arrives to trigger the on-change repair.
661
- after: {
662
- [QUIESCENT_REPAIR_DELAY]: {
663
- actions: ["apply sync"]
664
- }
665
- },
666
- on: {
667
- "patch emitted": {
668
- target: "local write"
669
- },
670
- // Mutation events arrive in bursts (one per input batch), so a
671
- // flush can land after a sibling flush already advanced the state.
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
- return event.type === "remote-patches";
685
+ return event.type === "remote-patches";
741
686
  }
742
687
  function getPublishedDocumentId(id) {
743
- return id.startsWith("drafts.") ? id.slice(7) : id.startsWith("versions.") ? id.split(".").slice(2).join(".") : id;
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
- const $ = c(20), {
747
- documentId,
748
- documentType,
749
- path
750
- } = props, setSdkValue = useEditDocument(props), instance = useSanityInstance(props), applyActions = useApplyDocumentActions();
751
- let t0;
752
- $[0] !== documentId || $[1] !== documentType || $[2] !== instance || $[3] !== path ? (t0 = getDocumentState(instance, {
753
- documentId,
754
- documentType,
755
- path
756
- }), $[0] = documentId, $[1] = documentType, $[2] = instance, $[3] = path, $[4] = t0) : t0 = $[4];
757
- const {
758
- getCurrent,
759
- subscribe
760
- } = t0;
761
- let t1;
762
- $[5] !== documentId || $[6] !== instance || $[7] !== path ? (t1 = (callback) => subscribeDocumentEvents(instance, {
763
- eventHandler: (event) => {
764
- const candidate = event;
765
- if (!isRemotePatchesEvent(candidate) || candidate.origin !== "remote" || getPublishedDocumentId(candidate.documentId) !== getPublishedDocumentId(documentId))
766
- return;
767
- let patches;
768
- try {
769
- patches = scopeRemotePatches(candidate.patches, path);
770
- } catch {
771
- return;
772
- }
773
- patches && patches.length > 0 && callback(patches);
774
- }
775
- }), $[5] = documentId, $[6] = instance, $[7] = path, $[8] = t1) : t1 = $[8];
776
- const onRemotePatches = t1;
777
- let t2;
778
- $[9] !== applyActions || $[10] !== documentId || $[11] !== documentType || $[12] !== path ? (t2 = (patches_0) => {
779
- const sanityPatches = convertPatchesToSanity(patches_0, {
780
- prefix: path
781
- }), action = {
782
- ...editDocument({
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
- const $ = c(8), {
796
- getRemoteValue,
797
- pushValue,
798
- onRemoteValueChange,
799
- onRemotePatches,
800
- pushPatches
801
- } = props, editor = useEditor();
802
- let t0;
803
- $[0] !== pushPatches || $[1] !== pushValue ? (t0 = valueSyncMachine.provide({
804
- actions: {
805
- "push to remote": (t12) => {
806
- const {
807
- context,
808
- event
809
- } = t12;
810
- if (event.type === "mutation flushed") {
811
- if (pushPatches && event.patches.length > 0)
812
- try {
813
- const mergeable = toMergeableMarkDefsPatches(event.patches, context.getRemoteValue);
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
- const $ = c(7);
835
- let handle, path;
836
- $[0] !== props ? ({
837
- path,
838
- ...handle
839
- } = props, $[0] = props, $[1] = handle, $[2] = path) : (handle = $[1], path = $[2]);
840
- const selection = useLocalSelection(), fieldPath = useFieldPath(path);
841
- let t0;
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
- const $ = c(12);
850
- let handle, path, renderCursor;
851
- $[0] !== options ? ({
852
- path,
853
- renderCursor,
854
- ...handle
855
- } = options, $[0] = options, $[1] = handle, $[2] = path, $[3] = renderCursor) : (handle = $[1], path = $[2], renderCursor = $[3]);
856
- const fieldPath = useFieldPath(path);
857
- let t0;
858
- $[4] !== fieldPath || $[5] !== handle ? (t0 = {
859
- ...handle,
860
- path: fieldPath,
861
- excludeVersions: !0
862
- }, $[4] = fieldPath, $[5] = handle, $[6] = t0) : t0 = $[6];
863
- const {
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
- return participant.selection ? [{
877
- sessionId: participant.sessionId,
878
- selection: participant.selection,
879
- user: participant.user
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
- const $ = c(2);
884
- let t0;
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
- const CARET_COLORS = ["#e0508a", "#c05fd8", "#7c66e8", "#2f8fdd", "#1f9c8f", "#4f9c2f", "#c98a1c", "#d4603a"], DOT_SIZE = 6;
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
- let hash = 0;
890
- for (let index = 0; index < userId.length; index++)
891
- hash = (hash * 31 + userId.charCodeAt(index)) % 1000003;
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
- const renderDefaultCursor = (cursor) => (props) => /* @__PURE__ */ jsx(DefaultCaret, { cursor, children: props.children });
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
- const $ = c(17), {
897
- cursor,
898
- children
899
- } = props;
900
- let t0;
901
- $[0] !== cursor.user.sanityUserId ? (t0 = getCaretColor(cursor.user.sanityUserId), $[0] = cursor.user.sanityUserId, $[1] = t0) : t0 = $[1];
902
- const color = t0, displayName = cursor.user.profile.displayName, t1 = `presence-caret-${cursor.sessionId}`, t2 = `2px solid ${color}`;
903
- let t3;
904
- $[2] !== t2 ? (t3 = {
905
- borderLeft: t2,
906
- marginLeft: -1,
907
- position: "relative",
908
- pointerEvents: "none"
909
- }, $[2] = t2, $[3] = t3) : t3 = $[3];
910
- const t4 = `presence-caret-dot-${cursor.sessionId}`;
911
- let t5;
912
- $[4] !== color ? (t5 = {
913
- backgroundColor: color,
914
- borderRadius: "50%",
915
- height: DOT_SIZE,
916
- left: -1,
917
- pointerEvents: "auto",
918
- position: "absolute",
919
- top: -5,
920
- transform: "translateX(-50%)",
921
- width: DOT_SIZE
922
- }, $[4] = color, $[5] = t5) : t5 = $[5];
923
- let t6;
924
- $[6] !== displayName || $[7] !== t4 || $[8] !== t5 ? (t6 = /* @__PURE__ */ jsx("span", { "data-testid": t4, style: t5, title: displayName }), $[6] = displayName, $[7] = t4, $[8] = t5, $[9] = t6) : t6 = $[9];
925
- let t7;
926
- $[10] !== t1 || $[11] !== t3 || $[12] !== t6 ? (t7 = /* @__PURE__ */ jsx("span", { contentEditable: !1, "data-testid": t1, style: t3, children: t6 }), $[10] = t1, $[11] = t3, $[12] = t6, $[13] = t7) : t7 = $[13];
927
- let t8;
928
- return $[14] !== children || $[15] !== t7 ? (t8 = /* @__PURE__ */ jsxs(Fragment, { children: [
929
- t7,
930
- children
931
- ] }), $[14] = children, $[15] = t7, $[16] = t8) : t8 = $[16], t8;
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
- const $ = c(25);
935
- let editableProps, handle, path, rangeDecorations, t0;
936
- if ($[0] !== props) {
937
- const {
938
- handle: t12,
939
- path: t22,
940
- renderCursor,
941
- rangeDecorations: t32,
942
- editableProps: t42
943
- } = splitEditableProps(props);
944
- handle = t12, path = t22, rangeDecorations = t32, editableProps = t42, t0 = resolveCursorRenderer(renderCursor), $[0] = props, $[1] = editableProps, $[2] = handle, $[3] = path, $[4] = rangeDecorations, $[5] = t0;
945
- } else
946
- editableProps = $[1], handle = $[2], path = $[3], rangeDecorations = $[4], t0 = $[5];
947
- const renderer = t0;
948
- let t1;
949
- $[6] !== handle || $[7] !== path || $[8] !== renderer.renderCursor ? (t1 = {
950
- ...handle,
951
- path,
952
- renderCursor: renderer.renderCursor
953
- }, $[6] = handle, $[7] = path, $[8] = renderer.renderCursor, $[9] = t1) : t1 = $[9];
954
- const cursors = useSDKPresenceCursors(t1);
955
- let t2;
956
- $[10] !== cursors || $[11] !== rangeDecorations || $[12] !== renderer.drawCursors ? (t2 = mergePresenceDecorations(rangeDecorations, cursors, renderer.drawCursors), $[10] = cursors, $[11] = rangeDecorations, $[12] = renderer.drawCursors, $[13] = t2) : t2 = $[13];
957
- const decorations = t2;
958
- let t3;
959
- $[14] !== decorations || $[15] !== editableProps ? (t3 = /* @__PURE__ */ jsx(PortableTextEditable, { ...editableProps, rangeDecorations: decorations }), $[14] = decorations, $[15] = editableProps, $[16] = t3) : t3 = $[16];
960
- let t4, t5;
961
- $[17] !== handle || $[18] !== path ? (t4 = /* @__PURE__ */ jsx(SDKValuePlugin, { ...handle, path }), t5 = /* @__PURE__ */ jsx(SDKPresencePlugin, { ...handle, path }), $[17] = handle, $[18] = path, $[19] = t4, $[20] = t5) : (t4 = $[19], t5 = $[20]);
962
- let t6;
963
- return $[21] !== t3 || $[22] !== t4 || $[23] !== t5 ? (t6 = /* @__PURE__ */ jsxs(Fragment, { children: [
964
- t3,
965
- t4,
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
- const {
971
- documentId,
972
- documentType,
973
- projectId,
974
- dataset,
975
- resource,
976
- resourceName,
977
- source,
978
- liveEdit,
979
- perspective,
980
- path,
981
- renderCursor,
982
- rangeDecorations,
983
- ...editableProps
984
- } = props;
985
- return {
986
- // Only the fields the caller actually passed. The SDK resolves an ambient
987
- // perspective and resource from context with `Object.hasOwn`, so forwarding
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
- return {
1024
- renderCursor: renderCursor ?? renderDefaultCursor,
1025
- drawCursors: renderCursor !== null
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
- return drawCursors ? [...rangeDecorations ?? [], ...cursors] : rangeDecorations;
1022
+ return drawCursors ? [...rangeDecorations ?? [], ...cursors] : rangeDecorations;
1030
1023
  }
1031
- export {
1032
- SDKPortableTextEditable,
1033
- SDKPresencePlugin,
1034
- SDKValuePlugin,
1035
- ValueSyncPlugin,
1036
- useSDKPresenceCursors
1037
- };
1038
- //# sourceMappingURL=index.js.map
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