@portabletext/plugin-dnd 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2016 - 2026 Sanity.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # `@portabletext/plugin-dnd`
2
+
3
+ A helper plugin for tracking the drop position during drag and drop.
4
+
5
+ When a consumer takes over block rendering through the `defineX` pipeline, the engine renders no drop-indicator chrome: where a dragged block would land is deliberately left to the consumer, since drop indication is pointer-driven UI, not document structure. This plugin tracks the position for you, derived from the editor's public `drag.*` behavior events.
6
+
7
+ ```tsx
8
+ import {DndProvider, useDropPosition} from '@portabletext/plugin-dnd'
9
+
10
+ function MyEditor() {
11
+ return (
12
+ <EditorProvider initialConfig={...}>
13
+ <DndProvider>
14
+ <PortableTextEditable />
15
+ </DndProvider>
16
+ </EditorProvider>
17
+ )
18
+ }
19
+
20
+ function MyTextBlock(props: TextBlockRenderProps) {
21
+ // `'start' | 'end'` while a block drag hovers this block, `undefined`
22
+ // otherwise.
23
+ const dropPosition = useDropPosition(props.path)
24
+ return (
25
+ <div {...props.attributes} style={{position: 'relative'}}>
26
+ {props.children}
27
+ {dropPosition ? <DropIndicator edge={dropPosition} /> : null}
28
+ </div>
29
+ )
30
+ }
31
+
32
+ // A line across the top of the block for 'start', the bottom for 'end'.
33
+ // `position: absolute` keeps it out of flow so it doesn't shift the text.
34
+ function DropIndicator({edge}: {edge: 'start' | 'end'}) {
35
+ return (
36
+ <div
37
+ contentEditable={false}
38
+ style={{
39
+ position: 'absolute',
40
+ left: 0,
41
+ right: 0,
42
+ top: edge === 'start' ? 0 : 'auto',
43
+ bottom: edge === 'end' ? 0 : 'auto',
44
+ borderTop: '1px solid currentColor',
45
+ }}
46
+ />
47
+ )
48
+ }
49
+ ```
50
+
51
+ Call `useDropPosition` from a component the render returns, not inline in the `render` callback (it is a hook):
52
+
53
+ ```tsx
54
+ defineTextBlock({type: '*', render: (props) => <MyTextBlock {...props} />})
55
+ ```
56
+
57
+ The position only appears for block drags (dragging an entire block or a multi-block selection), never for text drags, and never over the dragged blocks themselves. The plugin observes the drag events and forwards them untouched, so the editor's own drag handling is unaffected.
58
+
59
+ `dragover` fires at mousemove frequency, so granularity matters: reads via `useDropPosition` re-render only when the position at their own path changes. Moving the drag from one block to another re-renders exactly two blocks, the one losing the indicator and the one gaining it.
@@ -0,0 +1,48 @@
1
+ import type {Path} from '@portabletext/editor'
2
+ import {JSX, ReactNode} from 'react'
3
+
4
+ /**
5
+ * Tracks the drop position during drag and drop and serves it through
6
+ * context. Mount inside `EditorProvider`, wrapping whatever reads the
7
+ * position:
8
+ *
9
+ * ```tsx
10
+ * <EditorProvider initialConfig={...}>
11
+ * <DndProvider>
12
+ * <PortableTextEditable />
13
+ * </DndProvider>
14
+ * </EditorProvider>
15
+ * ```
16
+ *
17
+ * Reads via {@link useDropPosition} only re-render when the drop position
18
+ * at their own path changes.
19
+ *
20
+ * @beta
21
+ */
22
+ export declare function DndProvider(props: {children?: ReactNode}): JSX.Element
23
+
24
+ /**
25
+ * The block-relative position a drop would land at.
26
+ *
27
+ * @beta
28
+ */
29
+ export declare type DropPosition = {
30
+ path: Path
31
+ position: 'start' | 'end'
32
+ }
33
+
34
+ /**
35
+ * Read where a drop would land relative to the block at `path`: `'start'`,
36
+ * `'end'`, or `undefined` when no drag is hovering this block. Re-renders
37
+ * only when the position at this path changes.
38
+ *
39
+ * `path` is the keyed block path render callbacks receive, e.g.
40
+ * `[{_key: 'b0'}]`.
41
+ *
42
+ * @beta
43
+ */
44
+ export declare function useDropPosition(
45
+ path: Path,
46
+ ): DropPosition['position'] | undefined
47
+
48
+ export {}
package/dist/index.js ADDED
@@ -0,0 +1,210 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { c } from "react/compiler-runtime";
3
+ import { defineBehavior, effect, forward } from "@portabletext/editor/behaviors";
4
+ import { BehaviorPlugin } from "@portabletext/editor/plugins";
5
+ import { getFocusInlineObject, isSelectionCollapsed, getFocusTextBlock, getFocusSpan, getFragment, isSelectionExpanded, getSelectionStartBlock, getSelectionEndBlock, isOverlappingSelection, getFocusBlock, getSelectedBlocks, isSelectingEntireBlocks } from "@portabletext/editor/selectors";
6
+ import { getBlockEndPoint, getBlockStartPoint, isKeyedSegment } from "@portabletext/editor/utils";
7
+ import { createContext, useEffect, useContext, useSyncExternalStore } from "react";
8
+ function getDragSelection({
9
+ eventSelection,
10
+ snapshot
11
+ }) {
12
+ let dragSelection = eventSelection;
13
+ if (getFocusInlineObject({
14
+ ...snapshot,
15
+ context: {
16
+ ...snapshot.context,
17
+ selection: eventSelection
18
+ }
19
+ }))
20
+ return dragSelection;
21
+ const draggingCollapsedSelection = isSelectionCollapsed({
22
+ ...snapshot,
23
+ context: {
24
+ ...snapshot.context,
25
+ selection: eventSelection
26
+ }
27
+ }), draggedTextBlock = getFocusTextBlock({
28
+ ...snapshot,
29
+ context: {
30
+ ...snapshot.context,
31
+ selection: eventSelection
32
+ }
33
+ }), draggedSpan = getFocusSpan({
34
+ ...snapshot,
35
+ context: {
36
+ ...snapshot.context,
37
+ selection: eventSelection
38
+ }
39
+ });
40
+ draggingCollapsedSelection && draggedTextBlock && draggedSpan && (dragSelection = {
41
+ anchor: getBlockStartPoint({
42
+ context: snapshot.context,
43
+ block: draggedTextBlock
44
+ }),
45
+ focus: getBlockEndPoint({
46
+ context: snapshot.context,
47
+ block: draggedTextBlock
48
+ })
49
+ });
50
+ const selectedBlocks = getFragment(snapshot);
51
+ if (snapshot.context.selection && isSelectionExpanded(snapshot) && selectedBlocks.length > 1) {
52
+ const selectionStartBlock = getSelectionStartBlock(snapshot), selectionEndBlock = getSelectionEndBlock(snapshot);
53
+ if (!selectionStartBlock || !selectionEndBlock)
54
+ return dragSelection;
55
+ const selectionStartPoint = getBlockStartPoint({
56
+ context: snapshot.context,
57
+ block: selectionStartBlock
58
+ }), selectionEndPoint = getBlockEndPoint({
59
+ context: snapshot.context,
60
+ block: selectionEndBlock
61
+ });
62
+ isOverlappingSelection(eventSelection)({
63
+ ...snapshot,
64
+ context: {
65
+ ...snapshot.context,
66
+ selection: {
67
+ anchor: selectionStartPoint,
68
+ focus: selectionEndPoint
69
+ }
70
+ }
71
+ }) && (dragSelection = {
72
+ anchor: selectionStartPoint,
73
+ focus: selectionEndPoint
74
+ });
75
+ }
76
+ return dragSelection;
77
+ }
78
+ function createDropPositionStore() {
79
+ let current;
80
+ const subscribers = /* @__PURE__ */ new Map();
81
+ function notify(serializedPath) {
82
+ if (serializedPath === void 0)
83
+ return;
84
+ const bucket = subscribers.get(serializedPath);
85
+ if (bucket !== void 0)
86
+ for (const callback of bucket)
87
+ callback();
88
+ }
89
+ return {
90
+ get: (serializedPath) => current?.serializedPath === serializedPath ? current.position : void 0,
91
+ subscribeKey: (serializedPath, callback) => {
92
+ let bucket = subscribers.get(serializedPath);
93
+ return bucket === void 0 && (bucket = /* @__PURE__ */ new Set(), subscribers.set(serializedPath, bucket)), bucket.add(callback), () => {
94
+ bucket.delete(callback), bucket.size === 0 && subscribers.delete(serializedPath);
95
+ };
96
+ },
97
+ set: (next) => {
98
+ const previous = current;
99
+ current = next ? {
100
+ serializedPath: serializePath(next.path),
101
+ position: next.position
102
+ } : void 0, !(previous?.serializedPath === current?.serializedPath && previous?.position === current?.position) && (previous?.serializedPath !== current?.serializedPath && notify(previous?.serializedPath), notify(current?.serializedPath));
103
+ }
104
+ };
105
+ }
106
+ function createDropPositionBehaviors(setDropPosition) {
107
+ return [defineBehavior({
108
+ on: "drag.dragover",
109
+ guard: ({
110
+ snapshot,
111
+ event
112
+ }) => {
113
+ const dropFocusBlock = getFocusBlock({
114
+ ...snapshot,
115
+ context: {
116
+ ...snapshot.context,
117
+ selection: event.position.selection
118
+ }
119
+ });
120
+ if (!dropFocusBlock)
121
+ return !1;
122
+ const dragOrigin = event.dragOrigin;
123
+ if (!dragOrigin)
124
+ return !1;
125
+ const dragSelection = getDragSelection({
126
+ eventSelection: dragOrigin.selection,
127
+ snapshot
128
+ });
129
+ return getSelectedBlocks({
130
+ ...snapshot,
131
+ context: {
132
+ ...snapshot.context,
133
+ selection: dragSelection
134
+ }
135
+ }).some((draggedBlock) => draggedBlock.node._key === dropFocusBlock.node._key) || !isSelectingEntireBlocks({
136
+ ...snapshot,
137
+ context: {
138
+ ...snapshot.context,
139
+ selection: dragSelection
140
+ }
141
+ }) ? !1 : {
142
+ dropFocusBlock
143
+ };
144
+ },
145
+ actions: [({
146
+ event
147
+ }, {
148
+ dropFocusBlock
149
+ }) => [effect(() => {
150
+ setDropPosition({
151
+ path: dropFocusBlock.path,
152
+ position: event.position.block
153
+ });
154
+ }), forward(event)]]
155
+ }), defineBehavior({
156
+ on: "drag.*",
157
+ guard: ({
158
+ event
159
+ }) => event.type !== "drag.dragover",
160
+ actions: [({
161
+ event
162
+ }) => [effect(() => {
163
+ setDropPosition(void 0);
164
+ }), forward(event)]]
165
+ })];
166
+ }
167
+ const DndContext = createContext(void 0);
168
+ function DndProvider(props) {
169
+ const $ = c(7);
170
+ let t0;
171
+ $[0] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (t0 = createDropPositionStore(), $[0] = t0) : t0 = $[0];
172
+ const store = t0;
173
+ let t1;
174
+ $[1] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (t1 = createDropPositionBehaviors(store.set), $[1] = t1) : t1 = $[1];
175
+ const behaviors = t1;
176
+ let t2, t3;
177
+ $[2] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (t2 = () => () => {
178
+ store.set(void 0);
179
+ }, t3 = [store], $[2] = t2, $[3] = t3) : (t2 = $[2], t3 = $[3]), useEffect(t2, t3);
180
+ let t4;
181
+ $[4] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (t4 = /* @__PURE__ */ jsx(BehaviorPlugin, { behaviors }), $[4] = t4) : t4 = $[4];
182
+ let t5;
183
+ return $[5] !== props.children ? (t5 = /* @__PURE__ */ jsxs(DndContext.Provider, { value: store, children: [
184
+ t4,
185
+ props.children
186
+ ] }), $[5] = props.children, $[6] = t5) : t5 = $[6], t5;
187
+ }
188
+ function useDropPosition(path) {
189
+ const $ = c(8), store = useContext(DndContext);
190
+ if (store === void 0)
191
+ throw new Error("useDropPosition must be used below a <DndProvider>");
192
+ let t0;
193
+ $[0] !== path ? (t0 = serializePath(path), $[0] = path, $[1] = t0) : t0 = $[1];
194
+ const serializedPath = t0;
195
+ let t1;
196
+ $[2] !== serializedPath || $[3] !== store ? (t1 = (callback) => store.subscribeKey(serializedPath, callback), $[2] = serializedPath, $[3] = store, $[4] = t1) : t1 = $[4];
197
+ const subscribe = t1;
198
+ let t2;
199
+ $[5] !== serializedPath || $[6] !== store ? (t2 = () => store.get(serializedPath), $[5] = serializedPath, $[6] = store, $[7] = t2) : t2 = $[7];
200
+ const getSnapshot = t2;
201
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
202
+ }
203
+ function serializePath(path) {
204
+ return path.reduce((result, segment, index) => isKeyedSegment(segment) ? `${result}[_key=="${segment._key}"]` : `${result}${index === 0 ? "" : "."}${segment}`, "");
205
+ }
206
+ export {
207
+ DndProvider,
208
+ useDropPosition
209
+ };
210
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/drag-selection.ts","../src/plugin.dnd.tsx"],"sourcesContent":["import type {EditorSelection, EditorSnapshot} from '@portabletext/editor'\nimport {\n getFocusInlineObject,\n getFocusSpan,\n getFocusTextBlock,\n getFragment,\n getSelectionEndBlock,\n getSelectionStartBlock,\n isOverlappingSelection,\n isSelectionCollapsed,\n isSelectionExpanded,\n} from '@portabletext/editor/selectors'\nimport {getBlockEndPoint, getBlockStartPoint} from '@portabletext/editor/utils'\n\n/**\n * Given the current editor `snapshot` and an `eventSelection` representing\n * where the drag event originates from, calculate the selection in the\n * editor that should be dragged.\n *\n * Duplicated from the editor's internal `getDragSelection` rather than\n * imported: exporting it from core would add permanent public API for what\n * is an implementation detail of this plugin. Built on public selectors\n * only. Keep in sync with\n * `packages/editor/src/selectors/drag-selection.ts`.\n */\nexport function getDragSelection({\n eventSelection,\n snapshot,\n}: {\n eventSelection: NonNullable<EditorSelection>\n snapshot: EditorSnapshot\n}): NonNullable<EditorSelection> {\n let dragSelection = eventSelection\n\n const draggedInlineObject = getFocusInlineObject({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: eventSelection,\n },\n })\n\n if (draggedInlineObject) {\n return dragSelection\n }\n\n const draggingCollapsedSelection = isSelectionCollapsed({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: eventSelection,\n },\n })\n const draggedTextBlock = getFocusTextBlock({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: eventSelection,\n },\n })\n const draggedSpan = getFocusSpan({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: eventSelection,\n },\n })\n\n if (draggingCollapsedSelection && draggedTextBlock && draggedSpan) {\n // Looks like we are dragging an empty span\n // Let's drag the entire block instead\n dragSelection = {\n anchor: getBlockStartPoint({\n context: snapshot.context,\n block: draggedTextBlock,\n }),\n focus: getBlockEndPoint({\n context: snapshot.context,\n block: draggedTextBlock,\n }),\n }\n }\n\n const selectedBlocks = getFragment(snapshot)\n\n if (\n snapshot.context.selection &&\n isSelectionExpanded(snapshot) &&\n selectedBlocks.length > 1\n ) {\n const selectionStartBlock = getSelectionStartBlock(snapshot)\n const selectionEndBlock = getSelectionEndBlock(snapshot)\n\n if (!selectionStartBlock || !selectionEndBlock) {\n return dragSelection\n }\n\n const selectionStartPoint = getBlockStartPoint({\n context: snapshot.context,\n block: selectionStartBlock,\n })\n const selectionEndPoint = getBlockEndPoint({\n context: snapshot.context,\n block: selectionEndBlock,\n })\n\n const eventSelectionInsideBlocks = isOverlappingSelection(eventSelection)({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: {anchor: selectionStartPoint, focus: selectionEndPoint},\n },\n })\n\n if (eventSelectionInsideBlocks) {\n dragSelection = {\n anchor: selectionStartPoint,\n focus: selectionEndPoint,\n }\n }\n }\n\n return dragSelection\n}\n","import type {Path} from '@portabletext/editor'\nimport {\n defineBehavior,\n effect,\n forward,\n type Behavior,\n} from '@portabletext/editor/behaviors'\nimport {BehaviorPlugin} from '@portabletext/editor/plugins'\nimport {\n getFocusBlock,\n getSelectedBlocks,\n isSelectingEntireBlocks,\n} from '@portabletext/editor/selectors'\nimport {isKeyedSegment} from '@portabletext/editor/utils'\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from 'react'\nimport {getDragSelection} from './drag-selection'\n\n/**\n * The block-relative position a drop would land at.\n *\n * @beta\n */\nexport type DropPosition = {\n path: Path\n position: 'start' | 'end'\n}\n\n/**\n * One store per editor: the drag behaviors below maintain the current drop\n * position, and per-path subscriber buckets make notification O(changed\n * paths) rather than O(subscribers). `dragover` fires at mousemove\n * frequency, so only the block losing and the block gaining the indicator\n * re-render.\n */\ntype DropPositionStore = {\n get: (serializedPath: string) => DropPosition['position'] | undefined\n subscribeKey: (serializedPath: string, callback: () => void) => () => void\n set: (next: DropPosition | undefined) => void\n}\n\nfunction createDropPositionStore(): DropPositionStore {\n let current:\n | {serializedPath: string; position: DropPosition['position']}\n | undefined\n const subscribers = new Map<string, Set<() => void>>()\n\n function notify(serializedPath: string | undefined) {\n if (serializedPath === undefined) {\n return\n }\n\n const bucket = subscribers.get(serializedPath)\n\n if (bucket === undefined) {\n return\n }\n\n for (const callback of bucket) {\n callback()\n }\n }\n\n return {\n get: (serializedPath) =>\n current?.serializedPath === serializedPath ? current.position : undefined,\n subscribeKey: (serializedPath, callback) => {\n let bucket = subscribers.get(serializedPath)\n\n if (bucket === undefined) {\n bucket = new Set()\n subscribers.set(serializedPath, bucket)\n }\n\n bucket.add(callback)\n\n return () => {\n bucket.delete(callback)\n\n if (bucket.size === 0) {\n subscribers.delete(serializedPath)\n }\n }\n },\n set: (next) => {\n const previous = current\n\n // Swap before notifying: `useSyncExternalStore` re-reads the snapshot\n // synchronously on notification and skips the re-render when it reads\n // an unchanged (stale) value.\n current = next\n ? {serializedPath: serializePath(next.path), position: next.position}\n : undefined\n\n if (\n previous?.serializedPath === current?.serializedPath &&\n previous?.position === current?.position\n ) {\n return\n }\n\n if (previous?.serializedPath !== current?.serializedPath) {\n notify(previous?.serializedPath)\n }\n\n notify(current?.serializedPath)\n },\n }\n}\n\n/**\n * The behaviors observe the public `drag.*` events and `forward` every\n * event they handle: consumer behaviors run before the editor's own drag\n * handling, so omitting the forward would swallow the event and break the\n * drag itself. (The editor's internal drop-position tracking gets away\n * without forwarding because it registers below core priority.)\n */\nfunction createDropPositionBehaviors(\n setDropPosition: (next: DropPosition | undefined) => void,\n): Array<Behavior> {\n return [\n defineBehavior({\n on: 'drag.dragover',\n guard: ({snapshot, event}) => {\n const dropFocusBlock = getFocusBlock({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: event.position.selection,\n },\n })\n\n if (!dropFocusBlock) {\n return false\n }\n\n const dragOrigin = event.dragOrigin\n\n if (!dragOrigin) {\n return false\n }\n\n const dragSelection = getDragSelection({\n eventSelection: dragOrigin.selection,\n snapshot,\n })\n\n const draggedBlocks = getSelectedBlocks({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: dragSelection,\n },\n })\n\n if (\n draggedBlocks.some(\n (draggedBlock) =>\n draggedBlock.node._key === dropFocusBlock.node._key,\n )\n ) {\n return false\n }\n\n const draggingEntireBlocks = isSelectingEntireBlocks({\n ...snapshot,\n context: {\n ...snapshot.context,\n selection: dragSelection,\n },\n })\n\n if (!draggingEntireBlocks) {\n return false\n }\n\n return {dropFocusBlock}\n },\n actions: [\n ({event}, {dropFocusBlock}) => [\n effect(() => {\n setDropPosition({\n path: dropFocusBlock.path,\n position: event.position.block,\n })\n }),\n forward(event),\n ],\n ],\n }),\n defineBehavior({\n on: 'drag.*',\n guard: ({event}) => event.type !== 'drag.dragover',\n actions: [\n ({event}) => [\n effect(() => {\n setDropPosition(undefined)\n }),\n forward(event),\n ],\n ],\n }),\n ]\n}\n\nconst DndContext = createContext<DropPositionStore | undefined>(undefined)\n\n/**\n * Tracks the drop position during drag and drop and serves it through\n * context. Mount inside `EditorProvider`, wrapping whatever reads the\n * position:\n *\n * ```tsx\n * <EditorProvider initialConfig={...}>\n * <DndProvider>\n * <PortableTextEditable />\n * </DndProvider>\n * </EditorProvider>\n * ```\n *\n * Reads via {@link useDropPosition} only re-render when the drop position\n * at their own path changes.\n *\n * @beta\n */\nexport function DndProvider(props: {children?: ReactNode}) {\n const store = useMemo(() => createDropPositionStore(), [])\n const behaviors = useMemo(\n () => createDropPositionBehaviors(store.set),\n [store],\n )\n\n useEffect(() => {\n return () => {\n // A drag in progress when the provider unmounts never delivers its\n // clearing event.\n store.set(undefined)\n }\n }, [store])\n\n return (\n <DndContext.Provider value={store}>\n <BehaviorPlugin behaviors={behaviors} />\n {props.children}\n </DndContext.Provider>\n )\n}\n\n/**\n * Read where a drop would land relative to the block at `path`: `'start'`,\n * `'end'`, or `undefined` when no drag is hovering this block. Re-renders\n * only when the position at this path changes.\n *\n * `path` is the keyed block path render callbacks receive, e.g.\n * `[{_key: 'b0'}]`.\n *\n * @beta\n */\nexport function useDropPosition(\n path: Path,\n): DropPosition['position'] | undefined {\n const store = useContext(DndContext)\n\n if (store === undefined) {\n throw new Error('useDropPosition must be used below a <DndProvider>')\n }\n\n // Callers typically pass a fresh `path` array every render, so the\n // serialized string, not the array, is what keeps `subscribe` stable\n // below.\n const serializedPath = serializePath(path)\n\n const subscribe = useCallback(\n (callback: () => void) => store.subscribeKey(serializedPath, callback),\n [store, serializedPath],\n )\n\n const getSnapshot = () => store.get(serializedPath)\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\n/**\n * Serialize a keyed path to a string using Sanity's bracket notation.\n * Duplicated from the editor's internal `serializePath`; see\n * `drag-selection.ts` for the duplication rationale.\n */\nfunction serializePath(path: Path): string {\n return path.reduce<string>((result, segment, index) => {\n if (isKeyedSegment(segment)) {\n return `${result}[_key==\"${segment._key}\"]`\n }\n\n const separator = index === 0 ? '' : '.'\n return `${result}${separator}${segment}`\n }, '')\n}\n"],"names":["getDragSelection","eventSelection","snapshot","dragSelection","getFocusInlineObject","context","selection","draggingCollapsedSelection","isSelectionCollapsed","draggedTextBlock","getFocusTextBlock","draggedSpan","getFocusSpan","anchor","getBlockStartPoint","block","focus","getBlockEndPoint","selectedBlocks","getFragment","isSelectionExpanded","length","selectionStartBlock","getSelectionStartBlock","selectionEndBlock","getSelectionEndBlock","selectionStartPoint","selectionEndPoint","isOverlappingSelection","createDropPositionStore","current","subscribers","Map","notify","serializedPath","undefined","bucket","get","callback","position","subscribeKey","Set","set","add","delete","size","next","previous","serializePath","path","createDropPositionBehaviors","setDropPosition","defineBehavior","on","guard","event","dropFocusBlock","getFocusBlock","dragOrigin","getSelectedBlocks","some","draggedBlock","node","_key","isSelectingEntireBlocks","actions","effect","forward","type","DndContext","createContext","DndProvider","props","$","_c","t0","Symbol","for","store","t1","behaviors","t2","t3","useEffect","t4","t5","children","useDropPosition","useContext","Error","subscribe","getSnapshot","useSyncExternalStore","reduce","result","segment","index","isKeyedSegment"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@portabletext/plugin-dnd",
3
+ "version": "1.0.0",
4
+ "description": "A helper plugin for tracking the drop position during drag and drop",
5
+ "keywords": [
6
+ "portabletext",
7
+ "plugin",
8
+ "dnd",
9
+ "drag-and-drop",
10
+ "drop-indicator"
11
+ ],
12
+ "homepage": "https://portabletext.org",
13
+ "bugs": {
14
+ "url": "https://github.com/portabletext/editor/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/portabletext/editor.git",
19
+ "directory": "packages/plugin-dnd"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Sanity.io <hello@sanity.io>",
23
+ "sideEffects": false,
24
+ "type": "module",
25
+ "exports": {
26
+ ".": "./dist/index.js",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "devDependencies": {
35
+ "@sanity/tsconfig": "^2.1.0",
36
+ "@types/react": "^19.2.14",
37
+ "@types/react-dom": "^19.2.3",
38
+ "@vitejs/plugin-react": "^5.2.0",
39
+ "@vitest/browser": "^4.1.8",
40
+ "@vitest/browser-playwright": "^4.1.8",
41
+ "babel-plugin-react-compiler": "^1.0.0",
42
+ "eslint": "^9.39.1",
43
+ "eslint-plugin-react-hooks": "^7.1.1",
44
+ "react": "^19.2.5",
45
+ "typescript": "5.9.3",
46
+ "typescript-eslint": "^8.48.0",
47
+ "vitest": "^4.1.8",
48
+ "@portabletext/editor": "^7.4.0",
49
+ "@portabletext/schema": "^2.2.0",
50
+ "@portabletext/test": "^1.0.3"
51
+ },
52
+ "peerDependencies": {
53
+ "react": "^19.2",
54
+ "@portabletext/editor": "^7.4.0"
55
+ },
56
+ "engines": {
57
+ "node": ">=20.19 <22 || >=22.12"
58
+ },
59
+ "scripts": {
60
+ "build": "pkg-utils build --strict --check --clean",
61
+ "check:lint": "biome lint .",
62
+ "check:react-compiler": "eslint .",
63
+ "check:types": "tsc",
64
+ "check:types:watch": "tsc --watch",
65
+ "clean": "del .turbo && del dist && del node_modules",
66
+ "dev": "pkg-utils watch",
67
+ "lint:fix": "biome lint --write .",
68
+ "test:browser": "vitest --run --project browser",
69
+ "test:browser:chromium": "vitest --run --project \"browser (chromium)\"",
70
+ "test:unit": "vitest --run --project unit",
71
+ "test:watch": "vitest"
72
+ }
73
+ }