@portabletext/plugin-list-index 1.0.27 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,5 @@
1
- import {JSX} from 'react'
2
- import {Path} from '@portabletext/editor'
3
- import {ReactNode} from 'react'
4
-
1
+ import { Path } from "@portabletext/editor";
2
+ import { ReactNode } from "react";
5
3
  /**
6
4
  * Maintains a list index map for the editor and serves it through context.
7
5
  * Mount inside `EditorProvider`, wrapping whatever reads the indices:
@@ -22,10 +20,9 @@ import {ReactNode} from 'react'
22
20
  *
23
21
  * @beta
24
22
  */
25
- export declare function ListIndexProvider(props: {
26
- children?: ReactNode
27
- }): JSX.Element
28
-
23
+ declare function ListIndexProvider(props: {
24
+ children?: ReactNode;
25
+ }): import("react").JSX.Element;
29
26
  /**
30
27
  * Read the 1-based list index of the block at `path`, or `undefined` when
31
28
  * the block is not a list item. Re-renders only when the index at this
@@ -36,6 +33,6 @@ export declare function ListIndexProvider(props: {
36
33
  *
37
34
  * @beta
38
35
  */
39
- export declare function useListIndex(path: Path): number | undefined
40
-
41
- export {}
36
+ declare function useListIndex(path: Path): number | undefined;
37
+ export { ListIndexProvider, useListIndex };
38
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugin.list-index.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;iBAiIgB,kBAAkB;EAAQ,WAAW;oBAAU,IAAA;;;;;;;;;;;iBAyB/C,aAAa,MAAM"}
package/dist/index.js CHANGED
@@ -1,127 +1,172 @@
1
- import { jsx } from "react/jsx-runtime";
2
1
  import { c } from "react/compiler-runtime";
3
2
  import { useEditor } from "@portabletext/editor";
4
- import { createContext, useEffect, useContext, useSyncExternalStore } from "react";
3
+ import { createContext, useContext, useEffect, useSyncExternalStore } from "react";
5
4
  import { getContainerChildren } from "@portabletext/editor/traversal";
6
5
  import { isKeyedSegment, isTextBlock } from "@portabletext/editor/utils";
6
+ import { jsx } from "react/jsx-runtime";
7
+ /**
8
+ * Serialize a keyed path to a string using Sanity's bracket notation,
9
+ * e.g. `[{_key: 'k0'}]` becomes `[_key=="k0"]`.
10
+ *
11
+ * Duplicated from the editor's internal `serializePath` rather than
12
+ * imported: exporting it from core would add permanent public API for what
13
+ * is an implementation detail of this plugin.
14
+ */
7
15
  function serializePath(path) {
8
- return path.reduce((result, segment, index) => isKeyedSegment(segment) ? `${result}[_key=="${segment._key}"]` : `${result}${index === 0 ? "" : "."}${segment}`, "");
16
+ return path.reduce((result, segment, index) => isKeyedSegment(segment) ? `${result}[_key=="${segment._key}"]` : `${result}${index === 0 ? "" : "."}${segment}`, "");
9
17
  }
18
+ /**
19
+ * Compute the list index for every list item block in the value, at any
20
+ * depth.
21
+ *
22
+ * Returns a fresh `Map` keyed by the serialized full block path with the
23
+ * 1-based index of the block within its list, honoring list type and
24
+ * indentation level: same-type items count up across consecutive blocks on
25
+ * the same level, deeper levels restart at 1, and non-list blocks break the
26
+ * sequence. List items nested inside a container (e.g. a table cell) number
27
+ * within their own array, independently of siblings and the enclosing array.
28
+ *
29
+ * Duplicated from the editor's internal `buildIndexMaps` (the part that fills
30
+ * `listIndexMap`) rather than imported, for the same reason as `serializePath`
31
+ * above. Descends containers with the public `getContainerChildren`, seeding
32
+ * the document root from `context.value`. Keep the numbering semantics in sync
33
+ * with `packages/editor/src/internal-utils/build-index-maps.ts`.
34
+ */
10
35
  function buildListIndexMap(context) {
11
- const listIndexMap = /* @__PURE__ */ new Map(), traversalContext = {
12
- schema: context.schema,
13
- containers: context.containers ?? /* @__PURE__ */ new Map()
14
- };
15
- return collectListIndexes(traversalContext, context.value, [], void 0, listIndexMap), listIndexMap;
36
+ let listIndexMap = /* @__PURE__ */ new Map();
37
+ return collectListIndexes({
38
+ schema: context.schema,
39
+ containers: context.containers ?? /* @__PURE__ */ new Map()
40
+ }, context.value, [], void 0, listIndexMap), listIndexMap;
16
41
  }
42
+ /**
43
+ * Walk one block array, numbering its list items, then descend into any
44
+ * container blocks. List state is scoped per array: a list inside a container
45
+ * numbers independently, so each array starts fresh from 1.
46
+ */
17
47
  function collectListIndexes(context, blocks, basePath, parent, listIndexMap) {
18
- const levelIndexMaps = /* @__PURE__ */ new Map();
19
- let previousListItem;
20
- for (const block of blocks) {
21
- if (block === void 0 || block._key === void 0)
22
- continue;
23
- const blockPath = [...basePath, {
24
- _key: block._key
25
- }];
26
- if (!isTextBlock(context, block) || block.listItem === void 0 || block.level === void 0) {
27
- levelIndexMaps.clear(), previousListItem = void 0;
28
- const childResult = getContainerChildren(context.containers, block, parent);
29
- childResult && collectListIndexes(context, childResult.children, [...blockPath, childResult.container.field.name], childResult.container, listIndexMap);
30
- continue;
31
- }
32
- if (!previousListItem) {
33
- const levelIndexMap2 = levelIndexMaps.get(block.listItem) ?? /* @__PURE__ */ new Map();
34
- levelIndexMap2.set(block.level, 1), levelIndexMaps.set(block.listItem, levelIndexMap2), listIndexMap.set(serializePath(blockPath), 1), previousListItem = {
35
- listItem: block.listItem,
36
- level: block.level
37
- };
38
- continue;
39
- }
40
- if (previousListItem.listItem === block.listItem && previousListItem.level < block.level) {
41
- const levelIndexMap2 = levelIndexMaps.get(block.listItem) ?? /* @__PURE__ */ new Map();
42
- levelIndexMap2.set(block.level, 1), levelIndexMaps.set(block.listItem, levelIndexMap2), listIndexMap.set(serializePath(blockPath), 1), previousListItem = {
43
- listItem: block.listItem,
44
- level: block.level
45
- };
46
- continue;
47
- }
48
- levelIndexMaps.forEach((levelIndexMap2, listItem) => {
49
- if (listItem === block.listItem)
50
- return;
51
- const levelsToDelete = [];
52
- levelIndexMap2.forEach((_, level) => {
53
- block.level !== void 0 && level >= block.level && levelsToDelete.push(level);
54
- }), levelsToDelete.forEach((level) => {
55
- levelIndexMap2.delete(level);
56
- });
57
- });
58
- const levelIndexMap = levelIndexMaps.get(block.listItem) ?? /* @__PURE__ */ new Map(), levelCounter = levelIndexMap.get(block.level) ?? 0;
59
- levelIndexMap.set(block.level, levelCounter + 1), levelIndexMaps.set(block.listItem, levelIndexMap), listIndexMap.set(serializePath(blockPath), levelCounter + 1), previousListItem = {
60
- listItem: block.listItem,
61
- level: block.level
62
- };
63
- }
48
+ let levelIndexMaps = /* @__PURE__ */ new Map(), previousListItem;
49
+ for (let block of blocks) {
50
+ if (block === void 0 || block._key === void 0) continue;
51
+ let blockPath = [...basePath, { _key: block._key }];
52
+ if (!isTextBlock(context, block) || block.listItem === void 0 || block.level === void 0) {
53
+ levelIndexMaps.clear(), previousListItem = void 0;
54
+ let childResult = getContainerChildren(context.containers, block, parent);
55
+ childResult && collectListIndexes(context, childResult.children, [...blockPath, childResult.container.field.name], childResult.container, listIndexMap);
56
+ continue;
57
+ }
58
+ if (!previousListItem) {
59
+ let levelIndexMap = levelIndexMaps.get(block.listItem) ?? /* @__PURE__ */ new Map();
60
+ levelIndexMap.set(block.level, 1), levelIndexMaps.set(block.listItem, levelIndexMap), listIndexMap.set(serializePath(blockPath), 1), previousListItem = {
61
+ listItem: block.listItem,
62
+ level: block.level
63
+ };
64
+ continue;
65
+ }
66
+ if (previousListItem.listItem === block.listItem && previousListItem.level < block.level) {
67
+ let levelIndexMap = levelIndexMaps.get(block.listItem) ?? /* @__PURE__ */ new Map();
68
+ levelIndexMap.set(block.level, 1), levelIndexMaps.set(block.listItem, levelIndexMap), listIndexMap.set(serializePath(blockPath), 1), previousListItem = {
69
+ listItem: block.listItem,
70
+ level: block.level
71
+ };
72
+ continue;
73
+ }
74
+ levelIndexMaps.forEach((levelIndexMap, listItem) => {
75
+ if (listItem === block.listItem) return;
76
+ let levelsToDelete = [];
77
+ levelIndexMap.forEach((_, level) => {
78
+ block.level !== void 0 && level >= block.level && levelsToDelete.push(level);
79
+ }), levelsToDelete.forEach((level) => {
80
+ levelIndexMap.delete(level);
81
+ });
82
+ });
83
+ let levelIndexMap = levelIndexMaps.get(block.listItem) ?? /* @__PURE__ */ new Map(), levelCounter = levelIndexMap.get(block.level) ?? 0;
84
+ levelIndexMap.set(block.level, levelCounter + 1), levelIndexMaps.set(block.listItem, levelIndexMap), listIndexMap.set(serializePath(blockPath), levelCounter + 1), previousListItem = {
85
+ listItem: block.listItem,
86
+ level: block.level
87
+ };
88
+ }
64
89
  }
65
90
  function createListIndexStore(editor) {
66
- let listIndexMap = buildListIndexMap(editor.getSnapshot().context);
67
- const subscribers = /* @__PURE__ */ new Map();
68
- function rebuild() {
69
- const previousListIndexMap = listIndexMap;
70
- listIndexMap = buildListIndexMap(editor.getSnapshot().context);
71
- for (const [serializedPath, callbacks] of subscribers)
72
- if (previousListIndexMap.get(serializedPath) !== listIndexMap.get(serializedPath))
73
- for (const callback of callbacks)
74
- callback();
75
- }
76
- return {
77
- get: (serializedPath) => listIndexMap.get(serializedPath),
78
- subscribeKey: (serializedPath, callback) => {
79
- let bucket = subscribers.get(serializedPath);
80
- return bucket === void 0 && (bucket = /* @__PURE__ */ new Set(), subscribers.set(serializedPath, bucket)), bucket.add(callback), () => {
81
- bucket.delete(callback), bucket.size === 0 && subscribers.delete(serializedPath);
82
- };
83
- },
84
- subscribe: () => {
85
- rebuild();
86
- const subscription = editor.on("operation", (events) => {
87
- events.some((event) => event.operation.type !== "insert.text" && event.operation.type !== "remove.text") && rebuild();
88
- }, {
89
- batch: !0
90
- });
91
- return () => {
92
- subscription.unsubscribe();
93
- };
94
- }
95
- };
91
+ let listIndexMap = buildListIndexMap(editor.getSnapshot().context), subscribers = /* @__PURE__ */ new Map();
92
+ function rebuild() {
93
+ let previousListIndexMap = listIndexMap;
94
+ listIndexMap = buildListIndexMap(editor.getSnapshot().context);
95
+ for (let [serializedPath, callbacks] of subscribers) if (previousListIndexMap.get(serializedPath) !== listIndexMap.get(serializedPath)) for (let callback of callbacks) callback();
96
+ }
97
+ return {
98
+ get: (serializedPath) => listIndexMap.get(serializedPath),
99
+ subscribeKey: (serializedPath, callback) => {
100
+ let bucket = subscribers.get(serializedPath);
101
+ return bucket === void 0 && (bucket = /* @__PURE__ */ new Set(), subscribers.set(serializedPath, bucket)), bucket.add(callback), () => {
102
+ bucket.delete(callback), bucket.size === 0 && subscribers.delete(serializedPath);
103
+ };
104
+ },
105
+ subscribe: () => {
106
+ rebuild();
107
+ let subscription = editor.on("operation", (events) => {
108
+ events.some((event) => event.operation.type !== "insert.text" && event.operation.type !== "remove.text") && rebuild();
109
+ }, { batch: !0 });
110
+ return () => {
111
+ subscription.unsubscribe();
112
+ };
113
+ }
114
+ };
96
115
  }
97
116
  const ListIndexContext = createContext(void 0);
117
+ /**
118
+ * Maintains a list index map for the editor and serves it through context.
119
+ * Mount inside `EditorProvider`, wrapping whatever reads the indices:
120
+ *
121
+ * ```tsx
122
+ * <EditorProvider initialConfig={...}>
123
+ * <ListIndexProvider>
124
+ * <PortableTextEditable />
125
+ * </ListIndexProvider>
126
+ * </EditorProvider>
127
+ * ```
128
+ *
129
+ * The map is rebuilt at most once per microtask burst of operations,
130
+ * regardless of how many operations the burst contains or how many
131
+ * components read it, and only when the burst contains an operation that can
132
+ * affect list indices. Reads via {@link useListIndex} only re-render when the
133
+ * index at their own path changes.
134
+ *
135
+ * @beta
136
+ */
98
137
  function ListIndexProvider(props) {
99
- const $ = c(8), editor = useEditor();
100
- let t0;
101
- $[0] !== editor ? (t0 = createListIndexStore(editor), $[0] = editor, $[1] = t0) : t0 = $[1];
102
- const store = t0;
103
- let t1, t2;
104
- $[2] !== store ? (t1 = () => store.subscribe(), t2 = [store], $[2] = store, $[3] = t1, $[4] = t2) : (t1 = $[3], t2 = $[4]), useEffect(t1, t2);
105
- let t3;
106
- return $[5] !== props.children || $[6] !== store ? (t3 = /* @__PURE__ */ jsx(ListIndexContext.Provider, { value: store, children: props.children }), $[5] = props.children, $[6] = store, $[7] = t3) : t3 = $[7], t3;
138
+ let $ = c(8), editor = useEditor(), t0;
139
+ $[0] === editor ? t0 = $[1] : (t0 = createListIndexStore(editor), $[0] = editor, $[1] = t0);
140
+ let store = t0, t1, t2;
141
+ $[2] === store ? (t1 = $[3], t2 = $[4]) : (t1 = () => store.subscribe(), t2 = [store], $[2] = store, $[3] = t1, $[4] = t2), useEffect(t1, t2);
142
+ let t3;
143
+ return $[5] !== props.children || $[6] !== store ? (t3 = /* @__PURE__ */ jsx(ListIndexContext.Provider, {
144
+ value: store,
145
+ children: props.children
146
+ }), $[5] = props.children, $[6] = store, $[7] = t3) : t3 = $[7], t3;
107
147
  }
148
+ /**
149
+ * Read the 1-based list index of the block at `path`, or `undefined` when
150
+ * the block is not a list item. Re-renders only when the index at this
151
+ * path changes.
152
+ *
153
+ * `path` is the keyed block path render callbacks receive, e.g.
154
+ * `[{_key: 'b0'}]`.
155
+ *
156
+ * @beta
157
+ */
108
158
  function useListIndex(path) {
109
- const $ = c(8), store = useContext(ListIndexContext);
110
- if (store === void 0)
111
- throw new Error("useListIndex must be used below a <ListIndexProvider>");
112
- let t0;
113
- $[0] !== path ? (t0 = serializePath(path), $[0] = path, $[1] = t0) : t0 = $[1];
114
- const serializedPath = t0;
115
- let t1;
116
- $[2] !== serializedPath || $[3] !== store ? (t1 = (callback) => store.subscribeKey(serializedPath, callback), $[2] = serializedPath, $[3] = store, $[4] = t1) : t1 = $[4];
117
- const subscribe = t1;
118
- let t2;
119
- $[5] !== serializedPath || $[6] !== store ? (t2 = () => store.get(serializedPath), $[5] = serializedPath, $[6] = store, $[7] = t2) : t2 = $[7];
120
- const getSnapshot = t2;
121
- return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
159
+ let $ = c(8), store = useContext(ListIndexContext);
160
+ if (store === void 0) throw Error("useListIndex must be used below a <ListIndexProvider>");
161
+ let t0;
162
+ $[0] === path ? t0 = $[1] : (t0 = serializePath(path), $[0] = path, $[1] = t0);
163
+ let serializedPath = t0, t1;
164
+ $[2] !== serializedPath || $[3] !== store ? (t1 = (callback) => store.subscribeKey(serializedPath, callback), $[2] = serializedPath, $[3] = store, $[4] = t1) : t1 = $[4];
165
+ let subscribe = t1, t2;
166
+ $[5] !== serializedPath || $[6] !== store ? (t2 = () => store.get(serializedPath), $[5] = serializedPath, $[6] = store, $[7] = t2) : t2 = $[7];
167
+ let getSnapshot = t2;
168
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
122
169
  }
123
- export {
124
- ListIndexProvider,
125
- useListIndex
126
- };
127
- //# sourceMappingURL=index.js.map
170
+ export { ListIndexProvider, useListIndex };
171
+
172
+ //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/build-list-index-map.ts","../src/plugin.list-index.tsx"],"sourcesContent":["import type {\n EditorContext,\n Path,\n RegisteredContainer,\n} from '@portabletext/editor'\nimport {getContainerChildren} from '@portabletext/editor/traversal'\nimport {isKeyedSegment, isTextBlock} from '@portabletext/editor/utils'\n\n/**\n * Serialize a keyed path to a string using Sanity's bracket notation,\n * e.g. `[{_key: 'k0'}]` becomes `[_key==\"k0\"]`.\n *\n * Duplicated from the editor's internal `serializePath` rather than\n * imported: exporting it from core would add permanent public API for what\n * is an implementation detail of this plugin.\n */\nexport function 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\n// `containers` is optional: the production caller passes the full snapshot\n// context, while the flat (top-level only) test scenarios omit it. Without\n// containers, container blocks resolve to no children and are not descended,\n// the pre-container behavior.\ntype ListIndexInput = Pick<EditorContext, 'schema' | 'value'> &\n Partial<Pick<EditorContext, 'containers'>>\n\ntype TraversalContext = Pick<EditorContext, 'schema' | 'containers'>\n\n// The block type `getContainerChildren` yields. `Node` is not public, so\n// derive it from the (public) traversal primitive rather than naming it.\ntype Block = NonNullable<\n ReturnType<typeof getContainerChildren>\n>['children'][number]\n\n/**\n * Compute the list index for every list item block in the value, at any\n * depth.\n *\n * Returns a fresh `Map` keyed by the serialized full block path with the\n * 1-based index of the block within its list, honoring list type and\n * indentation level: same-type items count up across consecutive blocks on\n * the same level, deeper levels restart at 1, and non-list blocks break the\n * sequence. List items nested inside a container (e.g. a table cell) number\n * within their own array, independently of siblings and the enclosing array.\n *\n * Duplicated from the editor's internal `buildIndexMaps` (the part that fills\n * `listIndexMap`) rather than imported, for the same reason as `serializePath`\n * above. Descends containers with the public `getContainerChildren`, seeding\n * the document root from `context.value`. Keep the numbering semantics in sync\n * with `packages/editor/src/internal-utils/build-index-maps.ts`.\n */\nexport function buildListIndexMap(\n context: ListIndexInput,\n): Map<string, number> {\n const listIndexMap = new Map<string, number>()\n const traversalContext: TraversalContext = {\n schema: context.schema,\n containers: context.containers ?? new Map<string, RegisteredContainer>(),\n }\n collectListIndexes(\n traversalContext,\n context.value,\n [],\n undefined,\n listIndexMap,\n )\n return listIndexMap\n}\n\n/**\n * Walk one block array, numbering its list items, then descend into any\n * container blocks. List state is scoped per array: a list inside a container\n * numbers independently, so each array starts fresh from 1.\n */\nfunction collectListIndexes(\n context: TraversalContext,\n blocks: ReadonlyArray<Block | undefined>,\n basePath: Path,\n parent: RegisteredContainer | undefined,\n listIndexMap: Map<string, number>,\n): void {\n const levelIndexMaps = new Map<string, Map<number, number>>()\n\n let previousListItem:\n | {\n listItem: string\n level: number\n }\n | undefined\n\n for (const block of blocks) {\n if (block === undefined || block._key === undefined) {\n continue\n }\n\n const blockPath: Path = [...basePath, {_key: block._key}]\n\n // A non-list block breaks the list run for this array. Containers are\n // descended into; each nested array numbers independently.\n //\n // Unlike the engine's internal `isTextBlockNode`, `isTextBlock` also\n // requires `children`, which holds for the post-apply snapshot values\n // this plugin reads.\n if (\n !isTextBlock(context, block) ||\n block.listItem === undefined ||\n block.level === undefined\n ) {\n levelIndexMaps.clear()\n previousListItem = undefined\n\n const childResult = getContainerChildren(\n context.containers,\n block,\n parent,\n )\n if (childResult) {\n collectListIndexes(\n context,\n childResult.children,\n [...blockPath, childResult.container.field.name],\n childResult.container,\n listIndexMap,\n )\n }\n\n continue\n }\n\n // If we encounter a new list item, we set the initial index to 1 for the\n // list type on that level.\n if (!previousListItem) {\n const listIndex = 1\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n levelIndexMap.set(block.level, listIndex)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), listIndex)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n\n continue\n }\n\n // If the previous list item is of the same type but on a lower level, we\n // need to reset the level index map for that type.\n if (\n previousListItem.listItem === block.listItem &&\n previousListItem.level < block.level\n ) {\n const listIndex = 1\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n levelIndexMap.set(block.level, listIndex)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), listIndex)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n\n continue\n }\n\n // Reset other list types at current level and deeper\n levelIndexMaps.forEach((levelIndexMap, listItem) => {\n if (listItem === block.listItem) {\n return\n }\n\n // Reset all levels that are >= current level\n const levelsToDelete: number[] = []\n\n levelIndexMap.forEach((_, level) => {\n if (block.level !== undefined && level >= block.level) {\n levelsToDelete.push(level)\n }\n })\n\n levelsToDelete.forEach((level) => {\n levelIndexMap.delete(level)\n })\n })\n\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n const levelCounter = levelIndexMap.get(block.level) ?? 0\n levelIndexMap.set(block.level, levelCounter + 1)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), levelCounter + 1)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n }\n}\n","import {useEditor, type Editor, type Path} from '@portabletext/editor'\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from 'react'\nimport {buildListIndexMap, serializePath} from './build-list-index-map'\n\n/**\n * One store per editor: a single `operation` subscription maintains the\n * list index map, and per-path subscriber buckets make notification\n * O(changed paths) rather than O(subscribers).\n */\ntype ListIndexStore = {\n get: (serializedPath: string) => number | undefined\n subscribeKey: (serializedPath: string, callback: () => void) => () => void\n /**\n * Starts the `operation` subscription. Returns the unsubscribe.\n */\n subscribe: () => () => void\n}\n\nfunction createListIndexStore(editor: Editor): ListIndexStore {\n let listIndexMap = buildListIndexMap(editor.getSnapshot().context)\n const subscribers = new Map<string, Set<() => void>>()\n\n function rebuild() {\n const previousListIndexMap = listIndexMap\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 listIndexMap = buildListIndexMap(editor.getSnapshot().context)\n\n for (const [serializedPath, callbacks] of subscribers) {\n if (\n previousListIndexMap.get(serializedPath) !==\n listIndexMap.get(serializedPath)\n ) {\n for (const callback of callbacks) {\n callback()\n }\n }\n }\n }\n\n return {\n get: (serializedPath) => listIndexMap.get(serializedPath),\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 subscribe: () => {\n // Operations applied between store creation (render) and subscription\n // (effect) are not observed, so reconcile once up front.\n rebuild()\n\n // Batched delivery coalesces a burst (e.g. one operation per block\n // during a large delete, insert, or undo) into a single call carrying\n // the whole burst, so the map rebuilds at most once per burst instead\n // of once per operation. Text ops never change list structure; every\n // other document-changing op can (including deep edits inside a\n // container), so rebuild only when the burst contains one. Inspecting\n // the whole burst is why this needs `batch` rather than a last-event\n // delivery, which would drop a structural op whenever a text op\n // happened to be last in the burst.\n const subscription = editor.on(\n 'operation',\n (events) => {\n if (\n events.some(\n (event) =>\n event.operation.type !== 'insert.text' &&\n event.operation.type !== 'remove.text',\n )\n ) {\n rebuild()\n }\n },\n {batch: true},\n )\n\n return () => {\n subscription.unsubscribe()\n }\n },\n }\n}\n\nconst ListIndexContext = createContext<ListIndexStore | undefined>(undefined)\n\n/**\n * Maintains a list index map for the editor and serves it through context.\n * Mount inside `EditorProvider`, wrapping whatever reads the indices:\n *\n * ```tsx\n * <EditorProvider initialConfig={...}>\n * <ListIndexProvider>\n * <PortableTextEditable />\n * </ListIndexProvider>\n * </EditorProvider>\n * ```\n *\n * The map is rebuilt at most once per microtask burst of operations,\n * regardless of how many operations the burst contains or how many\n * components read it, and only when the burst contains an operation that can\n * affect list indices. Reads via {@link useListIndex} only re-render when the\n * index at their own path changes.\n *\n * @beta\n */\nexport function ListIndexProvider(props: {children?: ReactNode}) {\n const editor = useEditor()\n const store = useMemo(() => createListIndexStore(editor), [editor])\n\n useEffect(() => {\n return store.subscribe()\n }, [store])\n\n return (\n <ListIndexContext.Provider value={store}>\n {props.children}\n </ListIndexContext.Provider>\n )\n}\n\n/**\n * Read the 1-based list index of the block at `path`, or `undefined` when\n * the block is not a list item. Re-renders only when the index at this\n * path changes.\n *\n * `path` is the keyed block path render callbacks receive, e.g.\n * `[{_key: 'b0'}]`.\n *\n * @beta\n */\nexport function useListIndex(path: Path): number | undefined {\n const store = useContext(ListIndexContext)\n\n if (store === undefined) {\n throw new Error('useListIndex must be used below a <ListIndexProvider>')\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. Memoizing on the array would never hit.\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"],"names":["serializePath","path","reduce","result","segment","index","isKeyedSegment","_key","buildListIndexMap","context","listIndexMap","Map","traversalContext","schema","containers","collectListIndexes","value","undefined","blocks","basePath","parent","levelIndexMaps","previousListItem","block","blockPath","isTextBlock","listItem","level","clear","childResult","getContainerChildren","children","container","field","name","levelIndexMap","get","set","listIndex","forEach","levelsToDelete","_","push","delete","levelCounter","createListIndexStore","editor","getSnapshot","subscribers","rebuild","previousListIndexMap","serializedPath","callbacks","callback","subscribeKey","bucket","Set","add","size","subscribe","subscription","on","events","some","event","operation","type","batch","unsubscribe","ListIndexContext","createContext","ListIndexProvider","props","$","_c","useEditor","t0","store","t1","t2","useEffect","t3","useListIndex","useContext","Error","useSyncExternalStore"],"mappings":";;;;;;AAgBO,SAASA,cAAcC,MAAoB;AAChD,SAAOA,KAAKC,OAAe,CAACC,QAAQC,SAASC,UACvCC,eAAeF,OAAO,IACjB,GAAGD,MAAM,WAAWC,QAAQG,IAAI,OAIlC,GAAGJ,MAAM,GADEE,UAAU,IAAI,KAAK,GACT,GAAGD,OAAO,IACrC,EAAE;AACP;AAkCO,SAASI,kBACdC,SACqB;AACrB,QAAMC,eAAe,oBAAIC,IAAAA,GACnBC,mBAAqC;AAAA,IACzCC,QAAQJ,QAAQI;AAAAA,IAChBC,YAAYL,QAAQK,cAAc,oBAAIH,IAAAA;AAAAA,EAAiC;AAEzEI,SAAAA,mBACEH,kBACAH,QAAQO,OACR,CAAA,GACAC,QACAP,YACF,GACOA;AACT;AAOA,SAASK,mBACPN,SACAS,QACAC,UACAC,QACAV,cACM;AACN,QAAMW,qCAAqBV,IAAAA;AAE3B,MAAIW;AAOJ,aAAWC,SAASL,QAAQ;AAC1B,QAAIK,UAAUN,UAAaM,MAAMhB,SAASU;AACxC;AAGF,UAAMO,YAAkB,CAAC,GAAGL,UAAU;AAAA,MAACZ,MAAMgB,MAAMhB;AAAAA,IAAAA,CAAK;AAQxD,QACE,CAACkB,YAAYhB,SAASc,KAAK,KAC3BA,MAAMG,aAAaT,UACnBM,MAAMI,UAAUV,QAChB;AACAI,qBAAeO,SACfN,mBAAmBL;AAEnB,YAAMY,cAAcC,qBAClBrB,QAAQK,YACRS,OACAH,MACF;AACIS,qBACFd,mBACEN,SACAoB,YAAYE,UACZ,CAAC,GAAGP,WAAWK,YAAYG,UAAUC,MAAMC,IAAI,GAC/CL,YAAYG,WACZtB,YACF;AAGF;AAAA,IACF;AAIA,QAAI,CAACY,kBAAkB;AAErB,YAAMa,iBACJd,eAAee,IAAIb,MAAMG,QAAQ,yBAASf,IAAAA;AAC5CwB,qBAAcE,IAAId,MAAMI,OAAOW,CAAS,GACxCjB,eAAegB,IAAId,MAAMG,UAAUS,cAAa,GAEhDzB,aAAa2B,IAAIrC,cAAcwB,SAAS,GAAGc,CAAS,GAEpDhB,mBAAmB;AAAA,QACjBI,UAAUH,MAAMG;AAAAA,QAChBC,OAAOJ,MAAMI;AAAAA,MAAAA;AAGf;AAAA,IACF;AAIA,QACEL,iBAAiBI,aAAaH,MAAMG,YACpCJ,iBAAiBK,QAAQJ,MAAMI,OAC/B;AAEA,YAAMQ,iBACJd,eAAee,IAAIb,MAAMG,QAAQ,yBAASf,IAAAA;AAC5CwB,qBAAcE,IAAId,MAAMI,OAAOW,CAAS,GACxCjB,eAAegB,IAAId,MAAMG,UAAUS,cAAa,GAEhDzB,aAAa2B,IAAIrC,cAAcwB,SAAS,GAAGc,CAAS,GAEpDhB,mBAAmB;AAAA,QACjBI,UAAUH,MAAMG;AAAAA,QAChBC,OAAOJ,MAAMI;AAAAA,MAAAA;AAGf;AAAA,IACF;AAGAN,mBAAekB,QAAQ,CAACJ,gBAAeT,aAAa;AAClD,UAAIA,aAAaH,MAAMG;AACrB;AAIF,YAAMc,iBAA2B,CAAA;AAEjCL,qBAAcI,QAAQ,CAACE,GAAGd,UAAU;AAC9BJ,cAAMI,UAAUV,UAAaU,SAASJ,MAAMI,SAC9Ca,eAAeE,KAAKf,KAAK;AAAA,MAE7B,CAAC,GAEDa,eAAeD,QAASZ,CAAAA,UAAU;AAChCQ,uBAAcQ,OAAOhB,KAAK;AAAA,MAC5B,CAAC;AAAA,IACH,CAAC;AAED,UAAMQ,gBACJd,eAAee,IAAIb,MAAMG,QAAQ,KAAK,oBAAIf,IAAAA,GACtCiC,eAAeT,cAAcC,IAAIb,MAAMI,KAAK,KAAK;AACvDQ,kBAAcE,IAAId,MAAMI,OAAOiB,eAAe,CAAC,GAC/CvB,eAAegB,IAAId,MAAMG,UAAUS,aAAa,GAEhDzB,aAAa2B,IAAIrC,cAAcwB,SAAS,GAAGoB,eAAe,CAAC,GAE3DtB,mBAAmB;AAAA,MACjBI,UAAUH,MAAMG;AAAAA,MAChBC,OAAOJ,MAAMI;AAAAA,IAAAA;AAAAA,EAEjB;AACF;ACzLA,SAASkB,qBAAqBC,QAAgC;AAC5D,MAAIpC,eAAeF,kBAAkBsC,OAAOC,YAAAA,EAActC,OAAO;AACjE,QAAMuC,kCAAkBrC,IAAAA;AAExB,WAASsC,UAAU;AACjB,UAAMC,uBAAuBxC;AAK7BA,mBAAeF,kBAAkBsC,OAAOC,YAAAA,EAActC,OAAO;AAE7D,eAAW,CAAC0C,gBAAgBC,SAAS,KAAKJ;AACxC,UACEE,qBAAqBd,IAAIe,cAAc,MACvCzC,aAAa0B,IAAIe,cAAc;AAE/B,mBAAWE,YAAYD;AACrBC,mBAAAA;AAAAA,EAIR;AAEA,SAAO;AAAA,IACLjB,KAAMe,CAAAA,mBAAmBzC,aAAa0B,IAAIe,cAAc;AAAA,IACxDG,cAAcA,CAACH,gBAAgBE,aAAa;AAC1C,UAAIE,SAASP,YAAYZ,IAAIe,cAAc;AAE3C,aAAII,WAAWtC,WACbsC,SAAS,oBAAIC,OACbR,YAAYX,IAAIc,gBAAgBI,MAAM,IAGxCA,OAAOE,IAAIJ,QAAQ,GAEZ,MAAM;AACXE,eAAOZ,OAAOU,QAAQ,GAElBE,OAAOG,SAAS,KAClBV,YAAYL,OAAOQ,cAAc;AAAA,MAErC;AAAA,IACF;AAAA,IACAQ,WAAWA,MAAM;AAGfV,cAAAA;AAWA,YAAMW,eAAed,OAAOe,GAC1B,aACCC,CAAAA,WAAW;AAERA,eAAOC,KACJC,CAAAA,UACCA,MAAMC,UAAUC,SAAS,iBACzBF,MAAMC,UAAUC,SAAS,aAC7B,KAEAjB,QAAAA;AAAAA,MAEJ,GACA;AAAA,QAACkB,OAAO;AAAA,MAAA,CACV;AAEA,aAAO,MAAM;AACXP,qBAAaQ,YAAAA;AAAAA,MACf;AAAA,IACF;AAAA,EAAA;AAEJ;AAEA,MAAMC,mBAAmBC,cAA0CrD,MAAS;AAsBrE,SAAAsD,kBAAAC,OAAA;AAAA,QAAAC,IAAAC,EAAA,CAAA,GACL5B,SAAe6B,UAAAA;AAAW,MAAAC;AAAAH,WAAA3B,UACE8B,KAAA/B,qBAAqBC,MAAM,GAAC2B,OAAA3B,QAAA2B,OAAAG,MAAAA,KAAAH,EAAA,CAAA;AAAxD,QAAAI,QAA4BD;AAAuC,MAAAE,IAAAC;AAAAN,WAAAI,SAEzDC,KAAAA,MACDD,MAAKlB,UAAAA,GACXoB,KAAA,CAACF,KAAK,GAACJ,OAAAI,OAAAJ,OAAAK,IAAAL,OAAAM,OAAAD,KAAAL,EAAA,CAAA,GAAAM,KAAAN,EAAA,CAAA,IAFVO,UAAUF,IAEPC,EAAO;AAAC,MAAAE;AAAA,SAAAR,SAAAD,MAAAzC,YAAA0C,SAAAI,SAGTI,KAAA,oBAAA,iBAAA,UAAA,EAAkCJ,OAAAA,OAC/BL,UAAAA,MAAKzC,SAAAA,CACR,GAA4B0C,EAAA,CAAA,IAAAD,MAAAzC,UAAA0C,OAAAI,OAAAJ,OAAAQ,MAAAA,KAAAR,EAAA,CAAA,GAF5BQ;AAE4B;AAczB,SAAAC,aAAAjF,MAAA;AAAA,QAAAwE,IAAAC,EAAA,CAAA,GACLG,QAAcM,WAAWd,gBAAgB;AAEzC,MAAIQ,UAAU5D;AACZ,UAAM,IAAImE,MAAM,uDAAuD;AACxE,MAAAR;AAAAH,WAAAxE,QAKsB2E,KAAA5E,cAAcC,IAAI,GAACwE,OAAAxE,MAAAwE,OAAAG,MAAAA,KAAAH,EAAA,CAAA;AAA1C,QAAAtB,iBAAuByB;AAAmB,MAAAE;AAAAL,IAAA,CAAA,MAAAtB,kBAAAsB,SAAAI,SAGxCC,KAAAzB,CAAAA,aAA0BwB,MAAKvB,aAAcH,gBAAgBE,QAAQ,GAACoB,OAAAtB,gBAAAsB,OAAAI,OAAAJ,OAAAK,MAAAA,KAAAL,EAAA,CAAA;AADxE,QAAAd,YAAkBmB;AAGjB,MAAAC;AAAAN,IAAA,CAAA,MAAAtB,kBAAAsB,SAAAI,SAEmBE,KAAAA,MAAMF,MAAKzC,IAAKe,cAAc,GAACsB,OAAAtB,gBAAAsB,OAAAI,OAAAJ,OAAAM,MAAAA,KAAAN,EAAA,CAAA;AAAnD,QAAA1B,cAAoBgC;AAA+B,SAE5CM,qBAAqB1B,WAAWZ,aAAaA,WAAW;AAAC;"}
1
+ {"version":3,"file":"index.js","names":["EditorContext","Path","RegisteredContainer","getContainerChildren","isKeyedSegment","isTextBlock","serializePath","path","reduce","result","segment","index","_key","separator","ListIndexInput","Pick","Partial","TraversalContext","Block","NonNullable","ReturnType","buildListIndexMap","context","Map","listIndexMap","traversalContext","schema","containers","collectListIndexes","value","undefined","blocks","ReadonlyArray","basePath","parent","levelIndexMaps","previousListItem","listItem","level","block","blockPath","clear","childResult","children","container","field","name","listIndex","levelIndexMap","get","set","forEach","levelsToDelete","_","push","delete","levelCounter","useEditor","Editor","Path","createContext","useCallback","useContext","useEffect","useMemo","useSyncExternalStore","ReactNode","buildListIndexMap","serializePath","ListIndexStore","get","serializedPath","subscribeKey","callback","subscribe","createListIndexStore","editor","listIndexMap","getSnapshot","context","subscribers","Map","Set","rebuild","previousListIndexMap","callbacks","bucket","undefined","set","add","delete","size","subscription","on","events","some","event","operation","type","batch","unsubscribe","ListIndexContext","ListIndexProvider","props","$","_c","t0","store","t1","t2","t3","children","useListIndex","path","Error"],"sources":["../src/build-list-index-map.ts","../src/plugin.list-index.tsx"],"sourcesContent":["import type {\n EditorContext,\n Path,\n RegisteredContainer,\n} from '@portabletext/editor'\nimport {getContainerChildren} from '@portabletext/editor/traversal'\nimport {isKeyedSegment, isTextBlock} from '@portabletext/editor/utils'\n\n/**\n * Serialize a keyed path to a string using Sanity's bracket notation,\n * e.g. `[{_key: 'k0'}]` becomes `[_key==\"k0\"]`.\n *\n * Duplicated from the editor's internal `serializePath` rather than\n * imported: exporting it from core would add permanent public API for what\n * is an implementation detail of this plugin.\n */\nexport function 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\n// `containers` is optional: the production caller passes the full snapshot\n// context, while the flat (top-level only) test scenarios omit it. Without\n// containers, container blocks resolve to no children and are not descended,\n// the pre-container behavior.\ntype ListIndexInput = Pick<EditorContext, 'schema' | 'value'> &\n Partial<Pick<EditorContext, 'containers'>>\n\ntype TraversalContext = Pick<EditorContext, 'schema' | 'containers'>\n\n// The block type `getContainerChildren` yields. `Node` is not public, so\n// derive it from the (public) traversal primitive rather than naming it.\ntype Block = NonNullable<\n ReturnType<typeof getContainerChildren>\n>['children'][number]\n\n/**\n * Compute the list index for every list item block in the value, at any\n * depth.\n *\n * Returns a fresh `Map` keyed by the serialized full block path with the\n * 1-based index of the block within its list, honoring list type and\n * indentation level: same-type items count up across consecutive blocks on\n * the same level, deeper levels restart at 1, and non-list blocks break the\n * sequence. List items nested inside a container (e.g. a table cell) number\n * within their own array, independently of siblings and the enclosing array.\n *\n * Duplicated from the editor's internal `buildIndexMaps` (the part that fills\n * `listIndexMap`) rather than imported, for the same reason as `serializePath`\n * above. Descends containers with the public `getContainerChildren`, seeding\n * the document root from `context.value`. Keep the numbering semantics in sync\n * with `packages/editor/src/internal-utils/build-index-maps.ts`.\n */\nexport function buildListIndexMap(\n context: ListIndexInput,\n): Map<string, number> {\n const listIndexMap = new Map<string, number>()\n const traversalContext: TraversalContext = {\n schema: context.schema,\n containers: context.containers ?? new Map<string, RegisteredContainer>(),\n }\n collectListIndexes(\n traversalContext,\n context.value,\n [],\n undefined,\n listIndexMap,\n )\n return listIndexMap\n}\n\n/**\n * Walk one block array, numbering its list items, then descend into any\n * container blocks. List state is scoped per array: a list inside a container\n * numbers independently, so each array starts fresh from 1.\n */\nfunction collectListIndexes(\n context: TraversalContext,\n blocks: ReadonlyArray<Block | undefined>,\n basePath: Path,\n parent: RegisteredContainer | undefined,\n listIndexMap: Map<string, number>,\n): void {\n const levelIndexMaps = new Map<string, Map<number, number>>()\n\n let previousListItem:\n | {\n listItem: string\n level: number\n }\n | undefined\n\n for (const block of blocks) {\n if (block === undefined || block._key === undefined) {\n continue\n }\n\n const blockPath: Path = [...basePath, {_key: block._key}]\n\n // A non-list block breaks the list run for this array. Containers are\n // descended into; each nested array numbers independently.\n //\n // Unlike the engine's internal `isTextBlockNode`, `isTextBlock` also\n // requires `children`, which holds for the post-apply snapshot values\n // this plugin reads.\n if (\n !isTextBlock(context, block) ||\n block.listItem === undefined ||\n block.level === undefined\n ) {\n levelIndexMaps.clear()\n previousListItem = undefined\n\n const childResult = getContainerChildren(\n context.containers,\n block,\n parent,\n )\n if (childResult) {\n collectListIndexes(\n context,\n childResult.children,\n [...blockPath, childResult.container.field.name],\n childResult.container,\n listIndexMap,\n )\n }\n\n continue\n }\n\n // If we encounter a new list item, we set the initial index to 1 for the\n // list type on that level.\n if (!previousListItem) {\n const listIndex = 1\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n levelIndexMap.set(block.level, listIndex)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), listIndex)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n\n continue\n }\n\n // If the previous list item is of the same type but on a lower level, we\n // need to reset the level index map for that type.\n if (\n previousListItem.listItem === block.listItem &&\n previousListItem.level < block.level\n ) {\n const listIndex = 1\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n levelIndexMap.set(block.level, listIndex)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), listIndex)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n\n continue\n }\n\n // Reset other list types at current level and deeper\n levelIndexMaps.forEach((levelIndexMap, listItem) => {\n if (listItem === block.listItem) {\n return\n }\n\n // Reset all levels that are >= current level\n const levelsToDelete: number[] = []\n\n levelIndexMap.forEach((_, level) => {\n if (block.level !== undefined && level >= block.level) {\n levelsToDelete.push(level)\n }\n })\n\n levelsToDelete.forEach((level) => {\n levelIndexMap.delete(level)\n })\n })\n\n const levelIndexMap =\n levelIndexMaps.get(block.listItem) ?? new Map<number, number>()\n const levelCounter = levelIndexMap.get(block.level) ?? 0\n levelIndexMap.set(block.level, levelCounter + 1)\n levelIndexMaps.set(block.listItem, levelIndexMap)\n\n listIndexMap.set(serializePath(blockPath), levelCounter + 1)\n\n previousListItem = {\n listItem: block.listItem,\n level: block.level,\n }\n }\n}\n","import {useEditor, type Editor, type Path} from '@portabletext/editor'\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from 'react'\nimport {buildListIndexMap, serializePath} from './build-list-index-map'\n\n/**\n * One store per editor: a single `operation` subscription maintains the\n * list index map, and per-path subscriber buckets make notification\n * O(changed paths) rather than O(subscribers).\n */\ntype ListIndexStore = {\n get: (serializedPath: string) => number | undefined\n subscribeKey: (serializedPath: string, callback: () => void) => () => void\n /**\n * Starts the `operation` subscription. Returns the unsubscribe.\n */\n subscribe: () => () => void\n}\n\nfunction createListIndexStore(editor: Editor): ListIndexStore {\n let listIndexMap = buildListIndexMap(editor.getSnapshot().context)\n const subscribers = new Map<string, Set<() => void>>()\n\n function rebuild() {\n const previousListIndexMap = listIndexMap\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 listIndexMap = buildListIndexMap(editor.getSnapshot().context)\n\n for (const [serializedPath, callbacks] of subscribers) {\n if (\n previousListIndexMap.get(serializedPath) !==\n listIndexMap.get(serializedPath)\n ) {\n for (const callback of callbacks) {\n callback()\n }\n }\n }\n }\n\n return {\n get: (serializedPath) => listIndexMap.get(serializedPath),\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 subscribe: () => {\n // Operations applied between store creation (render) and subscription\n // (effect) are not observed, so reconcile once up front.\n rebuild()\n\n // Batched delivery coalesces a burst (e.g. one operation per block\n // during a large delete, insert, or undo) into a single call carrying\n // the whole burst, so the map rebuilds at most once per burst instead\n // of once per operation. Text ops never change list structure; every\n // other document-changing op can (including deep edits inside a\n // container), so rebuild only when the burst contains one. Inspecting\n // the whole burst is why this needs `batch` rather than a last-event\n // delivery, which would drop a structural op whenever a text op\n // happened to be last in the burst.\n const subscription = editor.on(\n 'operation',\n (events) => {\n if (\n events.some(\n (event) =>\n event.operation.type !== 'insert.text' &&\n event.operation.type !== 'remove.text',\n )\n ) {\n rebuild()\n }\n },\n {batch: true},\n )\n\n return () => {\n subscription.unsubscribe()\n }\n },\n }\n}\n\nconst ListIndexContext = createContext<ListIndexStore | undefined>(undefined)\n\n/**\n * Maintains a list index map for the editor and serves it through context.\n * Mount inside `EditorProvider`, wrapping whatever reads the indices:\n *\n * ```tsx\n * <EditorProvider initialConfig={...}>\n * <ListIndexProvider>\n * <PortableTextEditable />\n * </ListIndexProvider>\n * </EditorProvider>\n * ```\n *\n * The map is rebuilt at most once per microtask burst of operations,\n * regardless of how many operations the burst contains or how many\n * components read it, and only when the burst contains an operation that can\n * affect list indices. Reads via {@link useListIndex} only re-render when the\n * index at their own path changes.\n *\n * @beta\n */\nexport function ListIndexProvider(props: {children?: ReactNode}) {\n const editor = useEditor()\n const store = useMemo(() => createListIndexStore(editor), [editor])\n\n useEffect(() => {\n return store.subscribe()\n }, [store])\n\n return (\n <ListIndexContext.Provider value={store}>\n {props.children}\n </ListIndexContext.Provider>\n )\n}\n\n/**\n * Read the 1-based list index of the block at `path`, or `undefined` when\n * the block is not a list item. Re-renders only when the index at this\n * path changes.\n *\n * `path` is the keyed block path render callbacks receive, e.g.\n * `[{_key: 'b0'}]`.\n *\n * @beta\n */\nexport function useListIndex(path: Path): number | undefined {\n const store = useContext(ListIndexContext)\n\n if (store === undefined) {\n throw new Error('useListIndex must be used below a <ListIndexProvider>')\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. Memoizing on the array would never hit.\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"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/plugin-list-index",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "Compute the list index of each list item for custom list rendering",
5
5
  "keywords": [
6
6
  "list-index",
@@ -25,17 +25,19 @@
25
25
  "type": "module",
26
26
  "sideEffects": false,
27
27
  "main": "./dist/index.js",
28
+ "module": "./dist/index.js",
28
29
  "types": "./dist/index.d.ts",
29
30
  "exports": {
30
31
  ".": "./dist/index.js",
31
32
  "./package.json": "./package.json"
32
33
  },
33
34
  "devDependencies": {
34
- "@sanity/pkg-utils": "^10.8.2",
35
+ "@rolldown/plugin-babel": "^0.2.3",
36
+ "@sanity/pkg-utils": "^12.1.0",
35
37
  "@sanity/tsconfig": "^2.1.0",
36
38
  "@types/react": "^19.2.17",
37
39
  "@types/react-dom": "^19.2.3",
38
- "@vitejs/plugin-react": "^5.2.0",
40
+ "@vitejs/plugin-react": "^6.0.5",
39
41
  "@vitest/browser": "^4.1.10",
40
42
  "@vitest/browser-playwright": "^4.1.10",
41
43
  "babel-plugin-react-compiler": "^1.0.0",
@@ -45,22 +47,22 @@
45
47
  "react-dom": "^19.2.7",
46
48
  "typescript": "6.0.3",
47
49
  "typescript-eslint": "^8.62.0",
48
- "vite": "^7.3.5",
50
+ "vite": "^8.2.0",
49
51
  "vitest": "^4.1.10",
50
52
  "vitest-browser-react": "^2.2.0",
51
- "@portabletext/editor": "^7.10.15",
52
- "@portabletext/schema": "^2.2.3",
53
- "@portabletext/test": "^1.0.4"
53
+ "@portabletext/editor": "^7.10.17",
54
+ "@portabletext/schema": "^2.2.4",
55
+ "@portabletext/test": "^1.0.5"
54
56
  },
55
57
  "peerDependencies": {
56
58
  "react": "^19.2",
57
- "@portabletext/editor": "^7.10.15"
59
+ "@portabletext/editor": "^7.10.17"
58
60
  },
59
61
  "engines": {
60
62
  "node": ">=20.19 <22 || >=22.12"
61
63
  },
62
64
  "scripts": {
63
- "build": "pkg-utils build --strict --check --clean",
65
+ "build": "pkg-utils build --strict --check",
64
66
  "check:lint": "biome lint .",
65
67
  "check:react-compiler": "eslint .",
66
68
  "check:types": "tsc",