@portabletext/plugin-sdk-value 8.0.5 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -3
- package/dist/index.d.ts +100 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +390 -47
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { c } from "react/compiler-runtime";
|
|
2
|
-
import { editDocument, getDocumentState, subscribeDocumentEvents, useApplyDocumentActions, useEditDocument, usePresenceForDocument, useReportPresence, useSanityInstance } from "@sanity/sdk-react";
|
|
2
|
+
import { editDocument, getDocumentState, subscribeDocumentEvents, useApplyDocumentActions, useCommentActions, useComments, useEditDocument, usePresenceForDocument, useReportPresence, useSanityInstance } from "@sanity/sdk-react";
|
|
3
3
|
import { PortableTextEditable, useEditor, useEditorSelector } from "@portabletext/editor";
|
|
4
|
-
import { getSelection } from "@portabletext/editor/selectors";
|
|
4
|
+
import { getSelectedTextBlocks, getSelection } from "@portabletext/editor/selectors";
|
|
5
5
|
import { isEqualSelections } from "@portabletext/editor/utils";
|
|
6
6
|
import { useState } from "react";
|
|
7
7
|
import { applyAll } from "@portabletext/patches";
|
|
8
8
|
import { diffValue } from "@sanity/diff-patch";
|
|
9
|
-
import { parsePath } from "@sanity/json-match";
|
|
9
|
+
import { parsePath, stringifyPath } from "@sanity/json-match";
|
|
10
10
|
import { useActorRef } from "@xstate/react";
|
|
11
11
|
import { fromCallback, setup } from "xstate";
|
|
12
12
|
import rawDebug from "debug";
|
|
13
13
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
import { DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, applyPatches, cleanupEfficiency, makeDiff, makePatches } from "@sanity/diff-match-patch";
|
|
14
15
|
/**
|
|
15
16
|
* Reduces a selection to a caret at its focus point.
|
|
16
17
|
*
|
|
@@ -125,7 +126,15 @@ const debug = {
|
|
|
125
126
|
push: createDebugger("push"),
|
|
126
127
|
remote: createDebugger("remote"),
|
|
127
128
|
repair: createDebugger("repair")
|
|
128
|
-
}
|
|
129
|
+
};
|
|
130
|
+
function normalizeDocumentHandle(handle) {
|
|
131
|
+
let { source, ...normalizedHandle } = handle;
|
|
132
|
+
return normalizedHandle.resource !== void 0 || source === void 0 ? normalizedHandle : {
|
|
133
|
+
...normalizedHandle,
|
|
134
|
+
resource: source
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const ARRAYIFY_ERROR_MESSAGE = "Unexpected path format from diffValue output. Please report this issue.";
|
|
129
138
|
function* getSegments(node) {
|
|
130
139
|
node.base && (yield* getSegments(node.base)), node.segment.type !== "This" && (yield node.segment);
|
|
131
140
|
}
|
|
@@ -227,11 +236,11 @@ function convertPatchesToSanity(patches, options) {
|
|
|
227
236
|
}
|
|
228
237
|
});
|
|
229
238
|
}
|
|
230
|
-
function segmentsEqual(a, b) {
|
|
239
|
+
function segmentsEqual$1(a, b) {
|
|
231
240
|
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;
|
|
232
241
|
}
|
|
233
|
-
function pathsEqual(a, b) {
|
|
234
|
-
return a.length === b.length && a.every((segment, index) => segmentsEqual(segment, b[index]));
|
|
242
|
+
function pathsEqual$1(a, b) {
|
|
243
|
+
return a.length === b.length && a.every((segment, index) => segmentsEqual$1(segment, b[index]));
|
|
235
244
|
}
|
|
236
245
|
/**
|
|
237
246
|
* The editor engine can only resolve keyed/indexed segments against the
|
|
@@ -256,7 +265,7 @@ function findSidecarArrayPath(path) {
|
|
|
256
265
|
}
|
|
257
266
|
return null;
|
|
258
267
|
}
|
|
259
|
-
function getValueAtPath(value, path) {
|
|
268
|
+
function getValueAtPath$1(value, path) {
|
|
260
269
|
let current = value;
|
|
261
270
|
for (let segment of path) {
|
|
262
271
|
if (current == null) return;
|
|
@@ -290,10 +299,10 @@ function toEngineSafePatches(patches, targetValue) {
|
|
|
290
299
|
safe.push(patch);
|
|
291
300
|
continue;
|
|
292
301
|
}
|
|
293
|
-
sidecarPaths.some((existing) => pathsEqual(existing, sidecarPath)) || sidecarPaths.push(sidecarPath);
|
|
302
|
+
sidecarPaths.some((existing) => pathsEqual$1(existing, sidecarPath)) || sidecarPaths.push(sidecarPath);
|
|
294
303
|
}
|
|
295
304
|
for (let sidecarPath of sidecarPaths) {
|
|
296
|
-
let value = getValueAtPath(targetValue, sidecarPath);
|
|
305
|
+
let value = getValueAtPath$1(targetValue, sidecarPath);
|
|
297
306
|
safe.push(value === void 0 ? {
|
|
298
307
|
type: "unset",
|
|
299
308
|
path: sidecarPath,
|
|
@@ -318,7 +327,7 @@ function toEngineSafePatches(patches, targetValue) {
|
|
|
318
327
|
function filterInsertsMissingFromRemoteValue(patches, remoteValue) {
|
|
319
328
|
return patches.flatMap((patch) => {
|
|
320
329
|
if (patch.type !== "insert") return [patch];
|
|
321
|
-
let parentValue = getValueAtPath(remoteValue, patch.path.slice(0, -1));
|
|
330
|
+
let parentValue = getValueAtPath$1(remoteValue, patch.path.slice(0, -1));
|
|
322
331
|
if (!Array.isArray(parentValue)) return [patch];
|
|
323
332
|
let items = patch.items.filter((item) => {
|
|
324
333
|
let itemKey = getKey(item);
|
|
@@ -355,7 +364,7 @@ function toMergeableMarkDefsPatches(patches, getCurrentValue) {
|
|
|
355
364
|
if (patch.type !== "set" || patch.path.at(-1) !== "markDefs" || !Array.isArray(patch.value)) return [patch];
|
|
356
365
|
let currentValue = getCurrentValue();
|
|
357
366
|
if (!currentValue) return [patch];
|
|
358
|
-
let root = currentValue, storeMarkDefs = getValueAtPath(root, patch.path), storeBlock = getValueAtPath(root, patch.path.slice(0, -1));
|
|
367
|
+
let root = currentValue, storeMarkDefs = getValueAtPath$1(root, patch.path), storeBlock = getValueAtPath$1(root, patch.path.slice(0, -1));
|
|
359
368
|
if (!Array.isArray(storeMarkDefs) || typeof storeBlock != "object" || !storeBlock) return [patch];
|
|
360
369
|
let local = patch.value, store = storeMarkDefs;
|
|
361
370
|
if (local.some((item) => item._key === void 0)) return [patch];
|
|
@@ -407,8 +416,8 @@ function canApplyToValue(patch, value) {
|
|
|
407
416
|
switch (patch.type) {
|
|
408
417
|
case "unset":
|
|
409
418
|
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;
|
|
419
|
+
case "diffMatchPatch": return getValueAtPath$1(root, patch.path) !== void 0;
|
|
420
|
+
case "set": return patch.path.length === 0 || getValueAtPath$1(root, patch.path.slice(0, -1)) !== void 0;
|
|
412
421
|
default: return !0;
|
|
413
422
|
}
|
|
414
423
|
}
|
|
@@ -440,7 +449,7 @@ function scopeRemotePatches(patches, fieldPath) {
|
|
|
440
449
|
let prefix = arrayifyPath(fieldPath), converted = convertPatches(patches), scoped = [];
|
|
441
450
|
for (let patch of converted) {
|
|
442
451
|
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])) {
|
|
452
|
+
for (let index = 0; index < overlap; index++) if (!segmentsEqual$1(patch.path[index], prefix[index])) {
|
|
444
453
|
touchesField = !1;
|
|
445
454
|
break;
|
|
446
455
|
}
|
|
@@ -691,14 +700,12 @@ function getPublishedDocumentId(id) {
|
|
|
691
700
|
* @public
|
|
692
701
|
*/
|
|
693
702
|
function SDKValuePlugin(props) {
|
|
694
|
-
let $ = c(
|
|
695
|
-
$[0] !== documentId || $[1] !== documentType || $[2] !== instance || $[3] !== path ? (t0 = getDocumentState(instance, {
|
|
703
|
+
let $ = c(15), normalizedProps = normalizeDocumentHandle(props), { documentId, documentType, path } = normalizedProps, setSdkValue = useEditDocument(normalizedProps), instance = useSanityInstance(), applyActions = useApplyDocumentActions(), { getCurrent, subscribe } = getDocumentState(instance, {
|
|
696
704
|
documentId,
|
|
697
705
|
documentType,
|
|
698
706
|
path
|
|
699
|
-
}),
|
|
700
|
-
|
|
701
|
-
$[5] !== documentId || $[6] !== instance || $[7] !== path ? (t1 = (callback) => subscribeDocumentEvents(instance, { eventHandler: (event) => {
|
|
707
|
+
}), t0;
|
|
708
|
+
$[0] !== documentId || $[1] !== instance || $[2] !== path ? (t0 = (callback) => subscribeDocumentEvents(instance, { eventHandler: (event) => {
|
|
702
709
|
let candidate = event;
|
|
703
710
|
if (!isRemotePatchesEvent(candidate) || candidate.origin !== "remote" || getPublishedDocumentId(candidate.documentId) !== getPublishedDocumentId(documentId)) return;
|
|
704
711
|
let patches;
|
|
@@ -708,9 +715,9 @@ function SDKValuePlugin(props) {
|
|
|
708
715
|
return;
|
|
709
716
|
}
|
|
710
717
|
patches && patches.length > 0 && callback(patches);
|
|
711
|
-
} }), $[
|
|
712
|
-
let onRemotePatches =
|
|
713
|
-
$[
|
|
718
|
+
} }), $[0] = documentId, $[1] = instance, $[2] = path, $[3] = t0) : t0 = $[3];
|
|
719
|
+
let onRemotePatches = t0, t1;
|
|
720
|
+
$[4] !== applyActions || $[5] !== documentId || $[6] !== documentType || $[7] !== path ? (t1 = (patches_0) => {
|
|
714
721
|
let sanityPatches = convertPatchesToSanity(patches_0, { prefix: path }), action = {
|
|
715
722
|
...editDocument({
|
|
716
723
|
documentId,
|
|
@@ -719,15 +726,15 @@ function SDKValuePlugin(props) {
|
|
|
719
726
|
preserveOperations: !0
|
|
720
727
|
};
|
|
721
728
|
applyActions(action);
|
|
722
|
-
}, $[
|
|
723
|
-
let pushPatches =
|
|
724
|
-
return $[
|
|
729
|
+
}, $[4] = applyActions, $[5] = documentId, $[6] = documentType, $[7] = path, $[8] = t1) : t1 = $[8];
|
|
730
|
+
let pushPatches = t1, t2;
|
|
731
|
+
return $[9] !== getCurrent || $[10] !== onRemotePatches || $[11] !== pushPatches || $[12] !== setSdkValue || $[13] !== subscribe ? (t2 = /* @__PURE__ */ jsx(ValueSyncPlugin, {
|
|
725
732
|
getRemoteValue: getCurrent,
|
|
726
733
|
pushValue: setSdkValue,
|
|
727
734
|
onRemoteValueChange: subscribe,
|
|
728
735
|
onRemotePatches,
|
|
729
736
|
pushPatches
|
|
730
|
-
}), $[
|
|
737
|
+
}), $[9] = getCurrent, $[10] = onRemotePatches, $[11] = pushPatches, $[12] = setSdkValue, $[13] = subscribe, $[14] = t2) : t2 = $[14], t2;
|
|
731
738
|
}
|
|
732
739
|
/**
|
|
733
740
|
* NOTE: You are probably looking for SDKValuePlugin instead of this.
|
|
@@ -775,14 +782,16 @@ function ValueSyncPlugin(props) {
|
|
|
775
782
|
* @public
|
|
776
783
|
*/
|
|
777
784
|
function SDKPresencePlugin(props) {
|
|
778
|
-
let $ = c(
|
|
779
|
-
$[0] === props ?
|
|
780
|
-
let
|
|
781
|
-
|
|
785
|
+
let $ = c(9), t0;
|
|
786
|
+
$[0] === props ? t0 = $[1] : (t0 = normalizeDocumentHandle(props), $[0] = props, $[1] = t0);
|
|
787
|
+
let handle, path;
|
|
788
|
+
$[2] === t0 ? (handle = $[3], path = $[4]) : ({path, ...handle} = t0, $[2] = t0, $[3] = handle, $[4] = path);
|
|
789
|
+
let selection = useLocalSelection(), fieldPath = useFieldPath(path), t1;
|
|
790
|
+
return $[5] !== fieldPath || $[6] !== handle || $[7] !== selection ? (t1 = {
|
|
782
791
|
...handle,
|
|
783
792
|
path: fieldPath,
|
|
784
793
|
selection
|
|
785
|
-
}, $[
|
|
794
|
+
}, $[5] = fieldPath, $[6] = handle, $[7] = selection, $[8] = t1) : t1 = $[8], useReportPresence(t1), null;
|
|
786
795
|
}
|
|
787
796
|
/**
|
|
788
797
|
* Other people's carets in a Portable Text field, as range decorations.
|
|
@@ -799,23 +808,25 @@ function SDKPresencePlugin(props) {
|
|
|
799
808
|
* @public
|
|
800
809
|
*/
|
|
801
810
|
function useSDKPresenceCursors(options) {
|
|
802
|
-
let $ = c(
|
|
803
|
-
$[0] === options ?
|
|
804
|
-
let
|
|
805
|
-
$[
|
|
811
|
+
let $ = c(14), t0;
|
|
812
|
+
$[0] === options ? t0 = $[1] : (t0 = normalizeDocumentHandle(options), $[0] = options, $[1] = t0);
|
|
813
|
+
let handle, path, renderCursor;
|
|
814
|
+
$[2] === t0 ? (handle = $[3], path = $[4], renderCursor = $[5]) : ({path, renderCursor, ...handle} = t0, $[2] = t0, $[3] = handle, $[4] = path, $[5] = renderCursor);
|
|
815
|
+
let fieldPath = useFieldPath(path), t1;
|
|
816
|
+
$[6] !== fieldPath || $[7] !== handle ? (t1 = {
|
|
806
817
|
...handle,
|
|
807
818
|
path: fieldPath,
|
|
808
819
|
excludeVersions: !0
|
|
809
|
-
}, $[
|
|
810
|
-
let { presence } = usePresenceForDocument(
|
|
811
|
-
$[
|
|
812
|
-
let cursors =
|
|
813
|
-
return $[
|
|
820
|
+
}, $[6] = fieldPath, $[7] = handle, $[8] = t1) : t1 = $[8];
|
|
821
|
+
let { presence } = usePresenceForDocument(t1), t2;
|
|
822
|
+
$[9] === presence ? t2 = $[10] : (t2 = presence.flatMap(_temp$1), $[9] = presence, $[10] = t2);
|
|
823
|
+
let cursors = t2, t3;
|
|
824
|
+
return $[11] !== cursors || $[12] !== renderCursor ? (t3 = {
|
|
814
825
|
cursors,
|
|
815
826
|
renderCursor
|
|
816
|
-
}, $[
|
|
827
|
+
}, $[11] = cursors, $[12] = renderCursor, $[13] = t3) : t3 = $[13], useRemoteCursors(t3);
|
|
817
828
|
}
|
|
818
|
-
function _temp(participant) {
|
|
829
|
+
function _temp$1(participant) {
|
|
819
830
|
return participant.selection ? [{
|
|
820
831
|
sessionId: participant.sessionId,
|
|
821
832
|
selection: participant.selection,
|
|
@@ -832,6 +843,339 @@ function useFieldPath(path) {
|
|
|
832
843
|
return $[0] === path ? t0 = $[1] : (t0 = arrayifyPath(path), $[0] = path, $[1] = t0), t0;
|
|
833
844
|
}
|
|
834
845
|
/**
|
|
846
|
+
* How the Studio stores an inline comment's anchor: per Portable Text block the
|
|
847
|
+
* selection touches, the block's entire plain text with these two private-use
|
|
848
|
+
* characters inserted where the selection starts and ends. Text rather than
|
|
849
|
+
* offsets, so the anchor can be re-found after the text around it changes.
|
|
850
|
+
*/
|
|
851
|
+
const COMMENT_INDICATORS = ["", ""], COMMENT_INDICATORS_REGEX = RegExp(`[${COMMENT_INDICATORS.join("")}]`, "g");
|
|
852
|
+
function isTextBlock(node) {
|
|
853
|
+
return typeof node == "object" && !!node && Array.isArray(node.children) && typeof node._key == "string";
|
|
854
|
+
}
|
|
855
|
+
function isSpan$1(child) {
|
|
856
|
+
return child._type === "span" && typeof child.text == "string";
|
|
857
|
+
}
|
|
858
|
+
function getValueAtPath(value, path) {
|
|
859
|
+
let current = value;
|
|
860
|
+
for (let segment of path) {
|
|
861
|
+
if (typeof current != "object" || !current) return;
|
|
862
|
+
if (typeof segment == "string") current = current[segment];
|
|
863
|
+
else if (typeof segment == "number") current = Array.isArray(current) ? current[segment] : void 0;
|
|
864
|
+
else if (isKeyedSegment$1(segment)) current = Array.isArray(current) ? current.find((item) => typeof item == "object" && !!item && item._key === segment._key) : void 0;
|
|
865
|
+
else return;
|
|
866
|
+
}
|
|
867
|
+
return current;
|
|
868
|
+
}
|
|
869
|
+
function isKeyedSegment$1(segment) {
|
|
870
|
+
return typeof segment == "object" && !!segment && "_key" in segment;
|
|
871
|
+
}
|
|
872
|
+
function toPlainTextWithChildSeparators(block) {
|
|
873
|
+
return block.children.map((child) => isSpan$1(child) ? (child.text ?? "").replaceAll("", " ") : "").join("");
|
|
874
|
+
}
|
|
875
|
+
function diffText(current, next) {
|
|
876
|
+
let diff = makeDiff(current, next), diffs = cleanupEfficiency(diff);
|
|
877
|
+
return {
|
|
878
|
+
patches: makePatches(current, diffs, { margin: 15 }),
|
|
879
|
+
levenshtein: diffsLevenshtein(diffs)
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function diffApply(current, patches) {
|
|
883
|
+
return applyPatches(patches, current, {
|
|
884
|
+
allowExceedingIndices: !0,
|
|
885
|
+
margin: 15
|
|
886
|
+
})[0];
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Finds each stored anchor in the current editor value.
|
|
890
|
+
*
|
|
891
|
+
* A direct port of the Studio's `buildRangeDecorationSelectionsFromComments`,
|
|
892
|
+
* minus its Studio-only inputs. The stored text is diffed against the block's
|
|
893
|
+
* current text, so an anchor survives edits around it and inside it up to a
|
|
894
|
+
* similarity threshold. An anchor whose text is gone, or changed beyond
|
|
895
|
+
* recognition, is dropped rather than drawn somewhere wrong.
|
|
896
|
+
*
|
|
897
|
+
* Live tracking while the user types is not this function's job: the editor's
|
|
898
|
+
* `RangeDecoration.onMoved` does that. This runs when comments load or change.
|
|
899
|
+
*/
|
|
900
|
+
function resolveCommentSelections(options) {
|
|
901
|
+
let { value, comments } = options, anchored = [];
|
|
902
|
+
for (let { commentId, relativePath, selection } of comments) for (let selectionMember of selection.value) {
|
|
903
|
+
let container = relativePath.length > 0 ? getValueAtPath(value, relativePath) : value, matchedBlock = Array.isArray(container) ? container.find((block) => isTextBlock(block) && block._key === selectionMember._key) : void 0;
|
|
904
|
+
if (!matchedBlock || !isTextBlock(matchedBlock)) continue;
|
|
905
|
+
let selectionText = selectionMember.text.replaceAll(COMMENT_INDICATORS_REGEX, ""), textWithChildSeparators = toPlainTextWithChildSeparators(matchedBlock), { patches } = diffText(selectionText, selectionMember.text), diffedText = diffApply(textWithChildSeparators, patches), startIndex = diffedText.indexOf(COMMENT_INDICATORS[0]), endIndex = diffedText.replaceAll(COMMENT_INDICATORS[0], "").indexOf(COMMENT_INDICATORS[1]), textWithoutCommentTags = diffedText.replaceAll(COMMENT_INDICATORS_REGEX, "");
|
|
906
|
+
if (startIndex === -1 || endIndex === -1) continue;
|
|
907
|
+
let oldCommentedText = selectionMember.text.slice(selectionMember.text.indexOf(COMMENT_INDICATORS[0]) + 1, selectionMember.text.indexOf(COMMENT_INDICATORS[1])), newCommentedText = textWithoutCommentTags.slice(startIndex, endIndex), { levenshtein } = diffText(newCommentedText, oldCommentedText), threshold = Math.round(newCommentedText.length + oldCommentedText.length / 2);
|
|
908
|
+
if (newCommentedText.length === 0 || levenshtein > threshold || startIndex + 1 === endIndex) continue;
|
|
909
|
+
let childIndexAnchor = 0, anchorOffset = 0, childIndexFocus = 0, focusOffset = 0;
|
|
910
|
+
for (let i = 0; i < textWithoutCommentTags.length && (textWithoutCommentTags[i] === "" && (i <= startIndex && (anchorOffset = -1, childIndexAnchor++), focusOffset = -1, childIndexFocus++), i < startIndex && anchorOffset++, i < startIndex + newCommentedText.length && focusOffset++, i !== startIndex + newCommentedText.length); i++);
|
|
911
|
+
anchored.push({
|
|
912
|
+
commentId,
|
|
913
|
+
selection: {
|
|
914
|
+
anchor: {
|
|
915
|
+
path: [
|
|
916
|
+
...relativePath,
|
|
917
|
+
{ _key: matchedBlock._key },
|
|
918
|
+
"children",
|
|
919
|
+
{ _key: matchedBlock.children[childIndexAnchor]._key }
|
|
920
|
+
],
|
|
921
|
+
offset: anchorOffset
|
|
922
|
+
},
|
|
923
|
+
focus: {
|
|
924
|
+
path: [
|
|
925
|
+
...relativePath,
|
|
926
|
+
{ _key: matchedBlock._key },
|
|
927
|
+
"children",
|
|
928
|
+
{ _key: matchedBlock.children[childIndexFocus]._key }
|
|
929
|
+
],
|
|
930
|
+
offset: focusOffset
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return anchored;
|
|
936
|
+
}
|
|
937
|
+
function diffsLevenshtein(diffs) {
|
|
938
|
+
let levenshtein = 0, insertions = 0, deletions = 0;
|
|
939
|
+
for (let [op, data] of diffs) switch (op) {
|
|
940
|
+
case DIFF_INSERT:
|
|
941
|
+
insertions += data.length;
|
|
942
|
+
break;
|
|
943
|
+
case DIFF_DELETE:
|
|
944
|
+
deletions += data.length;
|
|
945
|
+
break;
|
|
946
|
+
case DIFF_EQUAL: levenshtein += Math.max(insertions, deletions), insertions = 0, deletions = 0;
|
|
947
|
+
}
|
|
948
|
+
return levenshtein += Math.max(insertions, deletions), levenshtein;
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Reduces a comment's stored field path to a path inside this editor.
|
|
952
|
+
*
|
|
953
|
+
* `undefined` means the comment belongs to some other editor: a different
|
|
954
|
+
* field, a sibling container, or a stored path that does not parse. Skipping
|
|
955
|
+
* those is what lets several editors on one document each decorate only their
|
|
956
|
+
* own comments.
|
|
957
|
+
*/
|
|
958
|
+
function relativeCommentPath(basePath, fieldPath) {
|
|
959
|
+
let parsed;
|
|
960
|
+
try {
|
|
961
|
+
parsed = arrayifyPath(fieldPath);
|
|
962
|
+
} catch {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (!(parsed.length < basePath.length)) {
|
|
966
|
+
for (let i = 0; i < basePath.length; i++) if (!segmentsEqual(basePath[i], parsed[i])) return;
|
|
967
|
+
return parsed.slice(basePath.length);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function segmentsEqual(a, b) {
|
|
971
|
+
return isKeyedSegment$1(a) && isKeyedSegment$1(b) ? a._key === b._key : a === b;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Turns the current editor selection into the stored comment anchor.
|
|
975
|
+
*
|
|
976
|
+
* Per selected block, the block's entire plain text with the selection
|
|
977
|
+
* boundaries marked by the two indicator characters. The write-time mirror of
|
|
978
|
+
* `resolveCommentSelections`, and deliberately built the way the Studio builds
|
|
979
|
+
* it so a comment written here re-anchors there and back.
|
|
980
|
+
*
|
|
981
|
+
* Returns `null` when there is nothing commentable: a collapsed selection,
|
|
982
|
+
* no selected text, or a selection spanning blocks from different containing
|
|
983
|
+
* arrays, which a single stored path cannot describe.
|
|
984
|
+
*/
|
|
985
|
+
function buildStoredSelection(options) {
|
|
986
|
+
let { selection, selectedBlocks } = options;
|
|
987
|
+
if (!selection || selectedBlocks.length === 0) return null;
|
|
988
|
+
let [start, end] = selection.backward ? [selection.focus, selection.anchor] : [selection.anchor, selection.focus], containerPath = selectedBlocks[0].path.slice(0, -1);
|
|
989
|
+
if (!selectedBlocks.every((selected) => pathsEqual(selected.path.slice(0, -1), containerPath))) return null;
|
|
990
|
+
let selectedCharacters = 0, value = selectedBlocks.map((selected, index) => {
|
|
991
|
+
let isFirst = index === 0, isLast = index === selectedBlocks.length - 1, plain = plainText(selected.node), from = isFirst ? plainOffset(selected.node, start.path, start.offset) : 0, to = isLast ? plainOffset(selected.node, end.path, end.offset) : plain.length;
|
|
992
|
+
return selectedCharacters += Math.max(0, to - from), {
|
|
993
|
+
_key: selected.node._key,
|
|
994
|
+
text: `${plain.slice(0, from)}${COMMENT_INDICATORS[0]}${plain.slice(from, to)}${COMMENT_INDICATORS[1]}${plain.slice(to)}`
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
return selectedCharacters === 0 ? null : {
|
|
998
|
+
containerPath,
|
|
999
|
+
selection: {
|
|
1000
|
+
type: "text",
|
|
1001
|
+
value
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
function plainText(block) {
|
|
1006
|
+
return block.children.map((child) => isSpan(child) ? child.text ?? "" : "").join("");
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Converts a selection point into an offset within the block's plain text: the
|
|
1010
|
+
* text of every span before the point's child, plus the offset inside it. A
|
|
1011
|
+
* point whose child is not in this block clamps to the block edge, which is
|
|
1012
|
+
* where a cross-block selection boundary lands.
|
|
1013
|
+
*/
|
|
1014
|
+
function plainOffset(block, pointPath, offset) {
|
|
1015
|
+
let childSegment = pointPath[pointPath.length - 1];
|
|
1016
|
+
if (!isKeyedSegment(childSegment)) return offset;
|
|
1017
|
+
let total = 0;
|
|
1018
|
+
for (let child of block.children) {
|
|
1019
|
+
if (child._key === childSegment._key) return total + (isSpan(child) ? offset : 0);
|
|
1020
|
+
isSpan(child) && (total += (child.text ?? "").length);
|
|
1021
|
+
}
|
|
1022
|
+
return total;
|
|
1023
|
+
}
|
|
1024
|
+
function isSpan(child) {
|
|
1025
|
+
return child._type === "span" && typeof child.text == "string";
|
|
1026
|
+
}
|
|
1027
|
+
function isKeyedSegment(segment) {
|
|
1028
|
+
return typeof segment == "object" && !!segment && "_key" in segment;
|
|
1029
|
+
}
|
|
1030
|
+
function pathsEqual(a, b) {
|
|
1031
|
+
return a.length === b.length && a.every((segment, index) => {
|
|
1032
|
+
let other = b[index];
|
|
1033
|
+
return isKeyedSegment(segment) && isKeyedSegment(other) ? segment._key === other._key : segment === other;
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
const NO_MOVES = {};
|
|
1037
|
+
/**
|
|
1038
|
+
* Anchors count as unchanged when every comment still sits at the same spot,
|
|
1039
|
+
* so resolving on each editor emission only re-renders when a highlight
|
|
1040
|
+
* actually needs to draw somewhere else.
|
|
1041
|
+
*/
|
|
1042
|
+
function sameAnchors(a, b) {
|
|
1043
|
+
return a.length === b.length && a.every((anchor, index) => {
|
|
1044
|
+
let other = b[index];
|
|
1045
|
+
return anchor.commentId === other.commentId && samePoint(anchor.selection.anchor, other.selection.anchor) && samePoint(anchor.selection.focus, other.selection.focus);
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
function samePoint(a, b) {
|
|
1049
|
+
return a.offset !== b.offset || a.path.length !== b.path.length ? !1 : a.path.every((segment, index) => {
|
|
1050
|
+
let other = b.path[index];
|
|
1051
|
+
return typeof segment == "object" && segment && typeof other == "object" && other ? segment._key === other._key : segment === other;
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Inline comment highlights for a Portable Text field, as range decorations.
|
|
1056
|
+
*
|
|
1057
|
+
* Pass the result to `<PortableTextEditable rangeDecorations={...} />`. Each
|
|
1058
|
+
* thread's first comment that carries a text anchor on this field gets one
|
|
1059
|
+
* decoration. Highlights stay put while the local user types, and a highlight
|
|
1060
|
+
* whose text has been deleted or rewritten beyond recognition is dropped
|
|
1061
|
+
* rather than drawn on the wrong words.
|
|
1062
|
+
*
|
|
1063
|
+
* Suspends while the document's comments load, like every SDK read hook.
|
|
1064
|
+
*
|
|
1065
|
+
* Resolved threads draw nothing: this mirrors the Studio, where resolving a
|
|
1066
|
+
* thread removes its highlight from the text.
|
|
1067
|
+
*
|
|
1068
|
+
* @public
|
|
1069
|
+
*/
|
|
1070
|
+
function useSDKCommentDecorations(options) {
|
|
1071
|
+
let $ = c(19), handle, path, renderDecoration;
|
|
1072
|
+
$[0] === options ? (handle = $[1], path = $[2], renderDecoration = $[3]) : ({path, renderDecoration, ...handle} = options, $[0] = options, $[1] = handle, $[2] = path, $[3] = renderDecoration);
|
|
1073
|
+
let editor = useEditor(), t0;
|
|
1074
|
+
$[4] === handle ? t0 = $[5] : (t0 = { ...handle }, $[4] = handle, $[5] = t0);
|
|
1075
|
+
let { comments } = useComments(t0), t1;
|
|
1076
|
+
if ($[6] !== comments || $[7] !== path) {
|
|
1077
|
+
let basePath = arrayifyPath(path);
|
|
1078
|
+
t1 = comments.flatMap((comment) => {
|
|
1079
|
+
if (comment.parentCommentId || !comment.selection || comment.status !== "open") return [];
|
|
1080
|
+
let relativePath = relativeCommentPath(basePath, comment.fieldPath);
|
|
1081
|
+
return relativePath === void 0 ? [] : [{
|
|
1082
|
+
comment,
|
|
1083
|
+
relativePath,
|
|
1084
|
+
selection: comment.selection
|
|
1085
|
+
}];
|
|
1086
|
+
}), $[6] = comments, $[7] = path, $[8] = t1;
|
|
1087
|
+
} else t1 = $[8];
|
|
1088
|
+
let inline = t1, t2;
|
|
1089
|
+
$[9] === inline ? t2 = $[10] : (t2 = (snapshot) => resolveCommentSelections({
|
|
1090
|
+
value: snapshot.context.value,
|
|
1091
|
+
comments: inline.map(_temp)
|
|
1092
|
+
}), $[9] = inline, $[10] = t2);
|
|
1093
|
+
let anchored = useEditorSelector(editor, t2, sameAnchors), t3;
|
|
1094
|
+
$[11] === Symbol.for("react.memo_cache_sentinel") ? (t3 = {}, $[11] = t3) : t3 = $[11];
|
|
1095
|
+
let t4;
|
|
1096
|
+
$[12] === inline ? t4 = $[13] : (t4 = {
|
|
1097
|
+
forComments: inline,
|
|
1098
|
+
selections: t3
|
|
1099
|
+
}, $[12] = inline, $[13] = t4);
|
|
1100
|
+
let [moved, setMoved] = useState(t4), movedSelections = moved.forComments === inline ? moved.selections : NO_MOVES, t5;
|
|
1101
|
+
if ($[14] !== anchored || $[15] !== inline || $[16] !== movedSelections || $[17] !== renderDecoration) {
|
|
1102
|
+
let commentsById = new Map(inline.map(_temp2));
|
|
1103
|
+
t5 = anchored.flatMap((anchor) => {
|
|
1104
|
+
let comment_2 = commentsById.get(anchor.commentId);
|
|
1105
|
+
if (!comment_2) return [];
|
|
1106
|
+
let movedSelection = movedSelections[anchor.commentId], selection_0 = movedSelection === void 0 ? anchor.selection : movedSelection;
|
|
1107
|
+
return selection_0 === null ? [] : [{
|
|
1108
|
+
component: renderDecoration(comment_2),
|
|
1109
|
+
selection: selection_0,
|
|
1110
|
+
onMoved: (t6) => {
|
|
1111
|
+
let { newSelection } = t6;
|
|
1112
|
+
setMoved((previous) => ({
|
|
1113
|
+
forComments: inline,
|
|
1114
|
+
selections: {
|
|
1115
|
+
...previous.forComments === inline ? previous.selections : {},
|
|
1116
|
+
[anchor.commentId]: newSelection
|
|
1117
|
+
}
|
|
1118
|
+
}));
|
|
1119
|
+
},
|
|
1120
|
+
payload: { commentId: anchor.commentId }
|
|
1121
|
+
}];
|
|
1122
|
+
}), $[14] = anchored, $[15] = inline, $[16] = movedSelections, $[17] = renderDecoration, $[18] = t5;
|
|
1123
|
+
} else t5 = $[18];
|
|
1124
|
+
return t5;
|
|
1125
|
+
}
|
|
1126
|
+
function _temp2(t0) {
|
|
1127
|
+
let { comment: comment_1 } = t0;
|
|
1128
|
+
return [comment_1.id, comment_1];
|
|
1129
|
+
}
|
|
1130
|
+
function _temp(t0) {
|
|
1131
|
+
let { comment: comment_0, relativePath: relativePath_0, selection } = t0;
|
|
1132
|
+
return {
|
|
1133
|
+
commentId: comment_0.id,
|
|
1134
|
+
relativePath: relativePath_0,
|
|
1135
|
+
selection
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
/** Reports the selection when it can take a comment, `null` otherwise. */
|
|
1139
|
+
function getCommentableSelection(snapshot) {
|
|
1140
|
+
let selection = getSelection(snapshot);
|
|
1141
|
+
return buildStoredSelection({
|
|
1142
|
+
selection,
|
|
1143
|
+
selectedBlocks: getSelectedTextBlocks(snapshot)
|
|
1144
|
+
}) ? selection : null;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Lets the app author inline comments on a Portable Text field.
|
|
1148
|
+
*
|
|
1149
|
+
* The plugin captures the selection and writes the comment; the composer UI is
|
|
1150
|
+
* the app's, the same split the Studio and Canvas use. Comments are written in
|
|
1151
|
+
* the shape the Studio stores, so a thread started here shows up there.
|
|
1152
|
+
*
|
|
1153
|
+
* @public
|
|
1154
|
+
*/
|
|
1155
|
+
function useSDKCommentAuthoring(options) {
|
|
1156
|
+
let $ = c(11), handle, path;
|
|
1157
|
+
$[0] === options ? (handle = $[1], path = $[2]) : ({path, ...handle} = options, $[0] = options, $[1] = handle, $[2] = path);
|
|
1158
|
+
let editor = useEditor(), { createComment } = useCommentActions(), commentableSelection = useEditorSelector(editor, getCommentableSelection, isEqualSelections), t0;
|
|
1159
|
+
$[3] !== createComment || $[4] !== editor || $[5] !== handle || $[6] !== path ? (t0 = (t1) => {
|
|
1160
|
+
let { message, commentId } = t1, snapshot = editor.getSnapshot(), built = buildStoredSelection({
|
|
1161
|
+
selection: getSelection(snapshot),
|
|
1162
|
+
selectedBlocks: getSelectedTextBlocks(snapshot)
|
|
1163
|
+
});
|
|
1164
|
+
return built ? createComment({
|
|
1165
|
+
...handle,
|
|
1166
|
+
fieldPath: stringifyPath([...arrayifyPath(path), ...built.containerPath]),
|
|
1167
|
+
selection: built.selection,
|
|
1168
|
+
message,
|
|
1169
|
+
...commentId === void 0 ? {} : { commentId }
|
|
1170
|
+
}) : Promise.reject(/* @__PURE__ */ Error("Nothing commentable is selected, so there is nothing to anchor the comment to."));
|
|
1171
|
+
}, $[3] = createComment, $[4] = editor, $[5] = handle, $[6] = path, $[7] = t0) : t0 = $[7];
|
|
1172
|
+
let t1;
|
|
1173
|
+
return $[8] !== commentableSelection || $[9] !== t0 ? (t1 = {
|
|
1174
|
+
commentableSelection,
|
|
1175
|
+
createInlineComment: t0
|
|
1176
|
+
}, $[8] = commentableSelection, $[9] = t0, $[10] = t1) : t1 = $[10], t1;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
835
1179
|
* Mid-tone hues, so a caret stays legible whether the app is light or dark. This
|
|
836
1180
|
* package cannot read the app's theme, so it does not try.
|
|
837
1181
|
*/
|
|
@@ -977,16 +1321,15 @@ function SDKPortableTextEditable(props) {
|
|
|
977
1321
|
* @internal
|
|
978
1322
|
*/
|
|
979
1323
|
function splitEditableProps(props) {
|
|
980
|
-
let { documentId, documentType, projectId, dataset, resource, resourceName,
|
|
1324
|
+
let normalizedProps = normalizeDocumentHandle(props), { documentId, documentType, projectId, dataset, resource, resourceName, liveEdit, perspective, path, renderCursor, rangeDecorations, ...editableProps } = normalizedProps;
|
|
981
1325
|
return {
|
|
982
1326
|
handle: {
|
|
983
1327
|
documentId,
|
|
984
1328
|
documentType,
|
|
985
1329
|
..."projectId" in props && { projectId },
|
|
986
1330
|
..."dataset" in props && { dataset },
|
|
987
|
-
..."resource" in
|
|
1331
|
+
..."resource" in normalizedProps && { resource },
|
|
988
1332
|
..."resourceName" in props && { resourceName },
|
|
989
|
-
..."source" in props && { source },
|
|
990
1333
|
..."liveEdit" in props && { liveEdit },
|
|
991
1334
|
..."perspective" in props && { perspective }
|
|
992
1335
|
},
|
|
@@ -1021,6 +1364,6 @@ function resolveCursorRenderer(renderCursor) {
|
|
|
1021
1364
|
function mergePresenceDecorations(rangeDecorations, cursors, drawCursors) {
|
|
1022
1365
|
return drawCursors ? [...rangeDecorations ?? [], ...cursors] : rangeDecorations;
|
|
1023
1366
|
}
|
|
1024
|
-
export { SDKPortableTextEditable, SDKPresencePlugin, SDKValuePlugin, ValueSyncPlugin, useSDKPresenceCursors };
|
|
1367
|
+
export { SDKPortableTextEditable, SDKPresencePlugin, SDKValuePlugin, ValueSyncPlugin, useSDKCommentAuthoring, useSDKCommentDecorations, useSDKPresenceCursors };
|
|
1025
1368
|
|
|
1026
1369
|
//# sourceMappingURL=index.js.map
|