@sanity/block-insert-picker 0.0.1 → 1.0.1
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 +21 -0
- package/README.md +150 -43
- package/dist/index.d.ts +385 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1195 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -7
package/dist/index.js
ADDED
|
@@ -0,0 +1,1195 @@
|
|
|
1
|
+
import { c } from "react/compiler-runtime";
|
|
2
|
+
import { useEditor } from "@portabletext/editor";
|
|
3
|
+
import { Box, Card, Flex, Hotkeys, Popover, Stack, Text, useToast } from "@sanity/ui";
|
|
4
|
+
import { useContext, useEffect, useId, useLayoutEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
5
|
+
import { useFormCallbacks, useResolveInitialValueForType, useSchema } from "sanity";
|
|
6
|
+
import { defineBehavior, effect, forward, raise } from "@portabletext/editor/behaviors";
|
|
7
|
+
import { blockOffsetsToSelection, getTextBlockText, isTextBlock } from "@portabletext/editor/utils";
|
|
8
|
+
import { PortableTextMemberItemsContext, PortableTextMemberSchemaTypesContext } from "sanity/_singletons";
|
|
9
|
+
import { getBlockTextBefore, getFirstBlock, getFocusBlock, getFocusTextBlock, isSelectionCollapsed } from "@portabletext/editor/selectors";
|
|
10
|
+
import { defineInputRule, defineInputRuleBehavior } from "@portabletext/plugin-input-rule";
|
|
11
|
+
import { BlockElementIcon } from "@sanity/icons/BlockElement";
|
|
12
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
/**
|
|
14
|
+
* The member's own name plus every name up its compiled `type` chain, most
|
|
15
|
+
* specific first: `{type: 'image', name: 'photo'}` yields `['photo',
|
|
16
|
+
* 'image', ...]`. Metadata and presets match against this chain so curation
|
|
17
|
+
* can target either the member name or the underlying type name.
|
|
18
|
+
*/
|
|
19
|
+
function typeNameChain(memberType) {
|
|
20
|
+
let names = [], current = memberType;
|
|
21
|
+
for (; current && typeof current.name == "string";) names.push(current.name), current = current.type && typeof current.type == "object" && "name" in current.type ? current.type : void 0;
|
|
22
|
+
return names;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Derives picker items from a portable-text array's insertable member types
|
|
26
|
+
* (see PickerItemsContext), so the picker automatically covers every object
|
|
27
|
+
* block the schema allows. The optional metadata list curates triggers,
|
|
28
|
+
* keywords, grouping, badges, visibility, and descriptions per member type;
|
|
29
|
+
* its array order is the sort rank (see PickerItemMetadata). Titles and
|
|
30
|
+
* icons intentionally stay unresolved here: BlockInsertPicker resolves
|
|
31
|
+
* presentation from the member schema type, which carries member-level
|
|
32
|
+
* config (a member-specific icon, a per-member title override).
|
|
33
|
+
*/
|
|
34
|
+
function derivePickerItems(context, metadata = []) {
|
|
35
|
+
return context.memberTypes.map((memberType, schemaIndex) => {
|
|
36
|
+
let chain = typeNameChain(memberType), nameRank = metadata.findIndex((entry) => entry.type === memberType.name), rank = nameRank === -1 ? metadata.findIndex((entry) => chain.includes(entry.type)) : nameRank;
|
|
37
|
+
return {
|
|
38
|
+
entry: rank === -1 ? void 0 : metadata[rank],
|
|
39
|
+
name: memberType.name,
|
|
40
|
+
rank: rank === -1 ? 2 ** 53 - 1 : rank,
|
|
41
|
+
schemaIndex
|
|
42
|
+
};
|
|
43
|
+
}).filter(({ entry }) => !entry?.hidden).sort((a, b) => a.rank - b.rank || a.schemaIndex - b.schemaIndex).map(({ entry, name }) => ({
|
|
44
|
+
action: {
|
|
45
|
+
blockType: name,
|
|
46
|
+
type: "insertBlock"
|
|
47
|
+
},
|
|
48
|
+
badge: entry?.badge,
|
|
49
|
+
description: entry?.description,
|
|
50
|
+
group: entry?.group,
|
|
51
|
+
id: name,
|
|
52
|
+
keywords: entry?.keywords,
|
|
53
|
+
openOnInsert: entry?.openOnInsert,
|
|
54
|
+
title: "",
|
|
55
|
+
trigger: entry?.trigger
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Metadata entries whose `type` matches neither a member name nor any name
|
|
60
|
+
* in a member's resolved type chain — almost always a typo. Surfaced as a
|
|
61
|
+
* dev-mode warning by BlockInsertPicker.
|
|
62
|
+
*/
|
|
63
|
+
function unknownMetadataTypes(context, metadata) {
|
|
64
|
+
let known = new Set(context.memberTypes.flatMap((memberType) => typeNameChain(memberType)));
|
|
65
|
+
return metadata.map((entry) => entry.type).filter((type) => !known.has(type));
|
|
66
|
+
}
|
|
67
|
+
function filterPickerItems(items, query) {
|
|
68
|
+
if (query === "" || query === "/") return items;
|
|
69
|
+
let queryLower = query.toLowerCase(), bareQuery = queryLower.startsWith("/") ? queryLower.slice(1) : queryLower;
|
|
70
|
+
return items.filter((item) => {
|
|
71
|
+
let bareTrigger = item.trigger?.toLowerCase().replace(/^\//, "");
|
|
72
|
+
return bareTrigger && bareQuery !== "" && bareTrigger.startsWith(bareQuery) ? !0 : bareQuery !== "" && !!(item.title.toLowerCase().includes(bareQuery) || item.description?.toLowerCase().includes(bareQuery) || item.keywords?.some((kw) => kw.toLowerCase().includes(bareQuery)));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Flattens sections back into the single index space that keyboard navigation,
|
|
77
|
+
* selection, and scroll-into-view all share. Rendering the sections and
|
|
78
|
+
* indexing this array must stay derived from the same `groupPickerItems` call
|
|
79
|
+
* so Enter inserts exactly the highlighted row.
|
|
80
|
+
*/
|
|
81
|
+
function flattenSections(sections) {
|
|
82
|
+
return sections.flatMap((section) => section.items);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Partitions items into sections by their `group`, preserving the incoming
|
|
86
|
+
* order within each section and ordering the sections by first appearance.
|
|
87
|
+
*
|
|
88
|
+
* Because the caller feeds rank-ordered items (see derivePickerItems), the
|
|
89
|
+
* first-appearance rule places the section holding the most-used block first,
|
|
90
|
+
* without a separate ordering constant to drift out of sync. Items without a
|
|
91
|
+
* group collapse into a single `group: null` section (e.g. a curated `items`
|
|
92
|
+
* prop), so the flat/ungrouped case stays a no-op reordering.
|
|
93
|
+
*/
|
|
94
|
+
function groupPickerItems(items) {
|
|
95
|
+
let order = [], byGroup = /* @__PURE__ */ new Map();
|
|
96
|
+
for (let item of items) {
|
|
97
|
+
let key = item.group ?? null, bucket = byGroup.get(key);
|
|
98
|
+
bucket || (bucket = [], byGroup.set(key, bucket), order.push(key)), bucket.push(item);
|
|
99
|
+
}
|
|
100
|
+
return order.map((key) => ({
|
|
101
|
+
group: key,
|
|
102
|
+
items: byGroup.get(key)
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Performs the picker insertion as a single behavior action set so the whole
|
|
107
|
+
* operation is one undo step, and cleans up the typed "/query" text with
|
|
108
|
+
* delete.text over exactly that range (never delete.block) so text that
|
|
109
|
+
* already existed in the anchor block can't be destroyed: the caret can sit
|
|
110
|
+
* at offset 0 of a populated block and still open the picker.
|
|
111
|
+
*/
|
|
112
|
+
function createInsertBehavior() {
|
|
113
|
+
return defineBehavior({
|
|
114
|
+
actions: [(_, { anchor, block, cleanup, placement }) => [...cleanup ? [raise({
|
|
115
|
+
at: cleanup,
|
|
116
|
+
type: "delete.text"
|
|
117
|
+
})] : [], raise({
|
|
118
|
+
block,
|
|
119
|
+
placement,
|
|
120
|
+
select: "start",
|
|
121
|
+
type: "insert.block",
|
|
122
|
+
...anchor ? { at: anchor } : {}
|
|
123
|
+
})]],
|
|
124
|
+
guard: ({ event, snapshot }) => {
|
|
125
|
+
let { anchorBlockKey, block, mode } = event;
|
|
126
|
+
return {
|
|
127
|
+
anchor: snapshot.context.value.some((candidate) => candidate._key === anchorBlockKey) ? blockOffsetsToSelection({
|
|
128
|
+
offsets: {
|
|
129
|
+
anchor: {
|
|
130
|
+
offset: 0,
|
|
131
|
+
path: [{ _key: anchorBlockKey }]
|
|
132
|
+
},
|
|
133
|
+
focus: {
|
|
134
|
+
offset: 0,
|
|
135
|
+
path: [{ _key: anchorBlockKey }]
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
snapshot
|
|
139
|
+
}) : null,
|
|
140
|
+
block,
|
|
141
|
+
cleanup: resolveQueryCleanup(event, snapshot),
|
|
142
|
+
placement: mode === "slash" ? "auto" : "after"
|
|
143
|
+
};
|
|
144
|
+
},
|
|
145
|
+
on: "custom.blockInsertPicker.insert"
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function sendInsertPickerItem(editor, payload) {
|
|
149
|
+
editor.send({
|
|
150
|
+
type: "custom.blockInsertPicker.insert",
|
|
151
|
+
...payload
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The query-cleanup half of the insert behavior on its own, for `custom`
|
|
156
|
+
* picker actions that don't insert a block: the typed "/query" text still
|
|
157
|
+
* leaves the document before the host's `onSelect` runs, under the same
|
|
158
|
+
* only-if-unchanged guard as a real insert.
|
|
159
|
+
*/
|
|
160
|
+
function createCleanupQueryBehavior() {
|
|
161
|
+
return defineBehavior({
|
|
162
|
+
actions: [(_, cleanup) => [raise({
|
|
163
|
+
at: cleanup,
|
|
164
|
+
type: "delete.text"
|
|
165
|
+
})]],
|
|
166
|
+
guard: ({ event, snapshot }) => resolveQueryCleanup(event, snapshot) ?? !1,
|
|
167
|
+
on: "custom.blockInsertPicker.cleanupQuery"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function sendCleanupQuery(editor, payload) {
|
|
171
|
+
editor.send({
|
|
172
|
+
type: "custom.blockInsertPicker.cleanupQuery",
|
|
173
|
+
...payload
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The selection spanning the typed "/query" text in the anchor block, when it
|
|
178
|
+
* is safe to delete. Only slash mode leaves query text in the document, and
|
|
179
|
+
* only when the anchor block still starts with the typed query; if it changed
|
|
180
|
+
* underneath us (collaborator edit, uncaptured caret movement) the action is
|
|
181
|
+
* still the user's intent, but nothing may be deleted.
|
|
182
|
+
*/
|
|
183
|
+
function resolveQueryCleanup(event, snapshot) {
|
|
184
|
+
let { anchorBlockKey, mode, query } = event;
|
|
185
|
+
if (mode !== "slash" || query.length === 0) return null;
|
|
186
|
+
let anchorBlock = snapshot.context.value.find((candidate) => candidate._key === anchorBlockKey);
|
|
187
|
+
return !anchorBlock || !isTextBlock({ schema: snapshot.context.schema }, anchorBlock) || !getTextBlockText(anchorBlock).startsWith(query) ? null : blockOffsetsToSelection({
|
|
188
|
+
offsets: {
|
|
189
|
+
anchor: {
|
|
190
|
+
offset: 0,
|
|
191
|
+
path: [{ _key: anchorBlockKey }]
|
|
192
|
+
},
|
|
193
|
+
focus: {
|
|
194
|
+
offset: query.length,
|
|
195
|
+
path: [{ _key: anchorBlockKey }]
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
snapshot
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function insertPickerItem(ctx) {
|
|
202
|
+
let blockKey = ctx.keyGenerator();
|
|
203
|
+
sendInsertPickerItem(ctx.editor, {
|
|
204
|
+
anchorBlockKey: ctx.anchorBlockKey,
|
|
205
|
+
block: {
|
|
206
|
+
...ctx.initialValue,
|
|
207
|
+
_key: blockKey,
|
|
208
|
+
_type: ctx.blockType
|
|
209
|
+
},
|
|
210
|
+
mode: ctx.mode,
|
|
211
|
+
query: ctx.query
|
|
212
|
+
}), ctx.onInsertedKey?.(blockKey);
|
|
213
|
+
}
|
|
214
|
+
const DEFAULT_LABELS = {
|
|
215
|
+
footerAnywhere: "Anywhere",
|
|
216
|
+
footerDismiss: "Dismiss",
|
|
217
|
+
footerInsert: "Insert",
|
|
218
|
+
footerNavigate: "Navigate",
|
|
219
|
+
insertAnchorRemoved: "The block you were inserting into was removed",
|
|
220
|
+
insertError: "Could not insert block",
|
|
221
|
+
noMatches: "No matches for \"{query}\"",
|
|
222
|
+
positionAnnouncement: "{index} of {count}",
|
|
223
|
+
title: "Insert block",
|
|
224
|
+
ungroupedSection: "Other blocks"
|
|
225
|
+
};
|
|
226
|
+
function resolveLabels(overrides) {
|
|
227
|
+
return overrides ? {
|
|
228
|
+
...DEFAULT_LABELS,
|
|
229
|
+
...overrides
|
|
230
|
+
} : DEFAULT_LABELS;
|
|
231
|
+
}
|
|
232
|
+
/** Fills the `{query}` placeholder in the no-matches label. */
|
|
233
|
+
function formatNoMatches(labels, query) {
|
|
234
|
+
return labels.noMatches.replace("{query}", () => query);
|
|
235
|
+
}
|
|
236
|
+
/** Fills `{index}`/`{count}` in the screen-reader position announcement. */
|
|
237
|
+
function formatPosition(labels, index, count) {
|
|
238
|
+
return labels.positionAnnouncement.replace("{index}", () => String(index)).replace("{count}", () => String(count));
|
|
239
|
+
}
|
|
240
|
+
function usePickerItemsContext(arrayTypeName) {
|
|
241
|
+
let $ = c(7), memberSchemaTypes = useContext(PortableTextMemberSchemaTypesContext), schema = useSchema(), t0;
|
|
242
|
+
if ($[0] !== arrayTypeName || $[1] !== memberSchemaTypes || $[2] !== schema) {
|
|
243
|
+
bb0: {
|
|
244
|
+
if (memberSchemaTypes) {
|
|
245
|
+
let t1;
|
|
246
|
+
$[4] !== memberSchemaTypes.blockObjects || $[5] !== memberSchemaTypes.portableText ? (t1 = {
|
|
247
|
+
memberTypes: memberSchemaTypes.blockObjects,
|
|
248
|
+
schemaType: memberSchemaTypes.portableText
|
|
249
|
+
}, $[4] = memberSchemaTypes.blockObjects, $[5] = memberSchemaTypes.portableText, $[6] = t1) : t1 = $[6], t0 = t1;
|
|
250
|
+
break bb0;
|
|
251
|
+
}
|
|
252
|
+
let arrayType = arrayTypeName ? schema.get(arrayTypeName) : void 0;
|
|
253
|
+
if (!arrayType || arrayType.jsonType !== "array") {
|
|
254
|
+
t0 = null;
|
|
255
|
+
break bb0;
|
|
256
|
+
}
|
|
257
|
+
t0 = {
|
|
258
|
+
memberTypes: arrayType.of.filter(_temp$1),
|
|
259
|
+
schemaType: arrayType
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
$[0] = arrayTypeName, $[1] = memberSchemaTypes, $[2] = schema, $[3] = t0;
|
|
263
|
+
} else t0 = $[3];
|
|
264
|
+
return t0;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Whether a member resolves to the built-in text block: walk the compiled
|
|
268
|
+
* `type` chain to its root and compare the root's name — the same algorithm
|
|
269
|
+
* Studio's schema bridge uses to bucketize `of`. A literal `name === 'block'`
|
|
270
|
+
* check would miss aliased text blocks (`{name: 'myBlock', type: 'block'}`)
|
|
271
|
+
* and wrongly offer them as object inserts.
|
|
272
|
+
*/
|
|
273
|
+
function _temp$1(member) {
|
|
274
|
+
return member.jsonType === "object" && !isTextBlockMember(member);
|
|
275
|
+
}
|
|
276
|
+
function isTextBlockMember(memberType) {
|
|
277
|
+
let current = memberType;
|
|
278
|
+
for (; current.type && typeof current.type == "object" && "name" in current.type;) current = current.type;
|
|
279
|
+
return current.name === "block";
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Opens a just-inserted block's editing UI (dialog/popover per the member's
|
|
283
|
+
* config) once its member item mounts. An insert lands synchronously, but the
|
|
284
|
+
* member item only exists on a later render — so callers schedule the key and
|
|
285
|
+
* the effect opens it when it appears.
|
|
286
|
+
*
|
|
287
|
+
* Together with memberSchemaTypes.ts, this module is one of exactly two
|
|
288
|
+
* importers of `sanity/_singletons` in this plugin: the subpath is internal
|
|
289
|
+
* with no stability guarantee, so each dependency stays quarantined where it
|
|
290
|
+
* can be swapped for a public API in one place. Consumers that need to react
|
|
291
|
+
* to inserts without this internal should use the `onInsert` seam on the
|
|
292
|
+
* plugin components instead.
|
|
293
|
+
*/
|
|
294
|
+
function useOpenBlockOnInsert() {
|
|
295
|
+
let $ = c(6), memberItems = useContext(PortableTextMemberItemsContext), { onPathOpen, onSetPathCollapsed } = useFormCallbacks(), pendingKeyRef = useRef(null), t0, t1;
|
|
296
|
+
$[0] !== memberItems || $[1] !== onPathOpen || $[2] !== onSetPathCollapsed ? (t0 = () => {
|
|
297
|
+
let key = pendingKeyRef.current;
|
|
298
|
+
if (!key) return;
|
|
299
|
+
let item = memberItems.find((m) => m.member.key === key);
|
|
300
|
+
item && (onPathOpen(item.member.item.path), onSetPathCollapsed(item.member.item.path, !1), pendingKeyRef.current = null);
|
|
301
|
+
}, t1 = [
|
|
302
|
+
memberItems,
|
|
303
|
+
onPathOpen,
|
|
304
|
+
onSetPathCollapsed
|
|
305
|
+
], $[0] = memberItems, $[1] = onPathOpen, $[2] = onSetPathCollapsed, $[3] = t0, $[4] = t1) : (t0 = $[3], t1 = $[4]), useEffect(t0, t1);
|
|
306
|
+
let t2;
|
|
307
|
+
return $[5] === Symbol.for("react.memo_cache_sentinel") ? (t2 = (blockKey) => {
|
|
308
|
+
pendingKeyRef.current = blockKey;
|
|
309
|
+
}, $[5] = t2) : t2 = $[5], t2;
|
|
310
|
+
}
|
|
311
|
+
function createPickerBehavior({ getState, isShortcutEnabled, onIntent }) {
|
|
312
|
+
return defineBehavior({
|
|
313
|
+
actions: [(payload, guardResponse) => {
|
|
314
|
+
let { blockEvent, intent } = guardResponse, sideEffect = effect(() => onIntent(intent));
|
|
315
|
+
return blockEvent ? [sideEffect] : [sideEffect, forward(payload.event)];
|
|
316
|
+
}],
|
|
317
|
+
guard: ({ event, snapshot }) => {
|
|
318
|
+
let state = getState();
|
|
319
|
+
if (state.mode === "closed") {
|
|
320
|
+
if (event.type === "insert.text" && event.text === "/") {
|
|
321
|
+
let focus = getFocusTextBlock(snapshot);
|
|
322
|
+
return !focus || !isSelectionCollapsed(snapshot) || getBlockTextBefore(snapshot) !== "" ? !1 : {
|
|
323
|
+
blockEvent: !1,
|
|
324
|
+
intent: {
|
|
325
|
+
anchorBlockKey: focus.node._key,
|
|
326
|
+
mode: "slash",
|
|
327
|
+
query: "/",
|
|
328
|
+
type: "open"
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (event.type === "keyboard.keydown" && event.originEvent.key === "/" && (event.originEvent.metaKey || event.originEvent.ctrlKey) && isShortcutEnabled()) {
|
|
333
|
+
let focus = getFocusBlock(snapshot), fallback = getFirstBlock(snapshot), anchorBlock = focus ?? fallback;
|
|
334
|
+
return anchorBlock ? {
|
|
335
|
+
blockEvent: !0,
|
|
336
|
+
intent: {
|
|
337
|
+
anchorBlockKey: anchorBlock.node._key,
|
|
338
|
+
mode: "shortcut",
|
|
339
|
+
query: "",
|
|
340
|
+
type: "open"
|
|
341
|
+
}
|
|
342
|
+
} : !1;
|
|
343
|
+
}
|
|
344
|
+
return !1;
|
|
345
|
+
}
|
|
346
|
+
if (event.type === "insert.text") return state.mode === "slash" ? event.text === " " ? {
|
|
347
|
+
blockEvent: !1,
|
|
348
|
+
intent: { type: "close" }
|
|
349
|
+
} : {
|
|
350
|
+
blockEvent: !1,
|
|
351
|
+
intent: {
|
|
352
|
+
query: state.query + event.text,
|
|
353
|
+
type: "updateQuery"
|
|
354
|
+
}
|
|
355
|
+
} : {
|
|
356
|
+
blockEvent: !0,
|
|
357
|
+
intent: {
|
|
358
|
+
query: state.query + event.text,
|
|
359
|
+
type: "updateQuery"
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
if (event.type === "delete.backward") return state.mode === "shortcut" ? state.query.length === 0 ? {
|
|
363
|
+
blockEvent: !0,
|
|
364
|
+
intent: { type: "close" }
|
|
365
|
+
} : {
|
|
366
|
+
blockEvent: !0,
|
|
367
|
+
intent: {
|
|
368
|
+
query: event.unit === "character" ? state.query.slice(0, -1) : "",
|
|
369
|
+
type: "updateQuery"
|
|
370
|
+
}
|
|
371
|
+
} : event.unit === "character" && state.query.length > 1 ? {
|
|
372
|
+
blockEvent: !1,
|
|
373
|
+
intent: {
|
|
374
|
+
query: state.query.slice(0, -1),
|
|
375
|
+
type: "updateQuery"
|
|
376
|
+
}
|
|
377
|
+
} : {
|
|
378
|
+
blockEvent: !1,
|
|
379
|
+
intent: { type: "close" }
|
|
380
|
+
};
|
|
381
|
+
if (event.type === "delete.forward") return {
|
|
382
|
+
blockEvent: !1,
|
|
383
|
+
intent: { type: "close" }
|
|
384
|
+
};
|
|
385
|
+
if (event.type === "keyboard.keydown") {
|
|
386
|
+
let key = event.originEvent.key;
|
|
387
|
+
if (key === "ArrowDown") return {
|
|
388
|
+
blockEvent: !0,
|
|
389
|
+
intent: {
|
|
390
|
+
delta: 1,
|
|
391
|
+
type: "navigate"
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
if (key === "ArrowUp") return {
|
|
395
|
+
blockEvent: !0,
|
|
396
|
+
intent: {
|
|
397
|
+
delta: -1,
|
|
398
|
+
type: "navigate"
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
if (key === "Enter") return {
|
|
402
|
+
blockEvent: !0,
|
|
403
|
+
intent: { type: "select" }
|
|
404
|
+
};
|
|
405
|
+
if (key === "Escape") return {
|
|
406
|
+
blockEvent: !0,
|
|
407
|
+
intent: { type: "close" }
|
|
408
|
+
};
|
|
409
|
+
if (key === "ArrowLeft" || key === "ArrowRight" || key === "Home" || key === "End" || key === "PageUp" || key === "PageDown") return {
|
|
410
|
+
blockEvent: !1,
|
|
411
|
+
intent: { type: "close" }
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return !1;
|
|
415
|
+
},
|
|
416
|
+
on: "*"
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function pickerReducer(state, intent) {
|
|
420
|
+
switch (intent.type) {
|
|
421
|
+
case "close":
|
|
422
|
+
case "select": return { mode: "closed" };
|
|
423
|
+
case "navigate": return state.mode === "closed" ? state : {
|
|
424
|
+
...state,
|
|
425
|
+
highlightedIndex: state.highlightedIndex + intent.delta
|
|
426
|
+
};
|
|
427
|
+
case "open": return state.mode === "closed" ? {
|
|
428
|
+
anchorBlockKey: intent.anchorBlockKey,
|
|
429
|
+
highlightedIndex: 0,
|
|
430
|
+
mode: intent.mode,
|
|
431
|
+
query: intent.query
|
|
432
|
+
} : state;
|
|
433
|
+
case "setHighlightedIndex": return state.mode === "closed" ? state : {
|
|
434
|
+
...state,
|
|
435
|
+
highlightedIndex: intent.index
|
|
436
|
+
};
|
|
437
|
+
case "updateQuery": return state.mode === "closed" ? state : {
|
|
438
|
+
...state,
|
|
439
|
+
highlightedIndex: 0,
|
|
440
|
+
query: intent.query
|
|
441
|
+
};
|
|
442
|
+
default: return intent;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Builds InputRules for the configs whose block type the host array actually
|
|
447
|
+
* allows, so a rule can't insert a block the field would reject.
|
|
448
|
+
*/
|
|
449
|
+
function createMarkdownInputRules({ allowedBlockTypes, keyGenerator, onInserted, rules }) {
|
|
450
|
+
return rules.filter((config) => allowedBlockTypes.has(config.blockType)).map((config) => createInsertRule(config, keyGenerator, onInserted));
|
|
451
|
+
}
|
|
452
|
+
const CODE_FENCE_PATTERN = /^`{3}\S*\s$/, BLOCKQUOTE_PATTERN = /^>\s$/;
|
|
453
|
+
/** Sugar: a blockquote rule ("> " at the start of a block) for `blockType`. */
|
|
454
|
+
function blockquoteRule(options) {
|
|
455
|
+
return {
|
|
456
|
+
blockType: options.blockType,
|
|
457
|
+
buildValue: ({ keyGenerator }) => options.createValue?.({ keyGenerator }) ?? {},
|
|
458
|
+
pattern: BLOCKQUOTE_PATTERN
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Sugar: a code-fence rule ("```lang␣") for `blockType`. The language table
|
|
463
|
+
* resolves fence tokens (aliases included) to the values the block's language
|
|
464
|
+
* selector stores, plus an optional filename; tokens outside the table pass
|
|
465
|
+
* through as typed so nothing the writer means is lost. `createValue` turns
|
|
466
|
+
* the resolved language and filename into the block's fields.
|
|
467
|
+
*/
|
|
468
|
+
function codeFenceRule(options) {
|
|
469
|
+
let languages = options.languages ?? [];
|
|
470
|
+
return {
|
|
471
|
+
blockType: options.blockType,
|
|
472
|
+
buildValue: ({ keyGenerator, matchText }) => {
|
|
473
|
+
let language = normalizeFenceLanguage(fenceLanguageFromMatch(matchText), languages, options.defaultLanguage), filename = language ? languages.find((entry) => entry.value === language)?.filename : void 0;
|
|
474
|
+
return options.createValue({
|
|
475
|
+
filename,
|
|
476
|
+
keyGenerator,
|
|
477
|
+
language
|
|
478
|
+
});
|
|
479
|
+
},
|
|
480
|
+
pattern: CODE_FENCE_PATTERN
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/** Strips the leading "```" fence and surrounding whitespace from a match. */
|
|
484
|
+
function fenceLanguageFromMatch(matchText) {
|
|
485
|
+
return matchText.replace(/^`{3}/, "").trim();
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Resolves a typed fence token against a language table: a canonical value,
|
|
489
|
+
* an alias (`py`→python, `yml`→yaml), or — for anything the table doesn't
|
|
490
|
+
* know — the token as typed. An empty token yields `defaultLanguage`.
|
|
491
|
+
*/
|
|
492
|
+
function normalizeFenceLanguage(raw, languages, defaultLanguage) {
|
|
493
|
+
let token = raw.trim(), lower = token.toLowerCase();
|
|
494
|
+
if (!lower) return defaultLanguage;
|
|
495
|
+
for (let { aliases, value } of languages) if (value.toLowerCase() === lower || aliases?.some((alias) => alias.toLowerCase() === lower)) return value;
|
|
496
|
+
return token;
|
|
497
|
+
}
|
|
498
|
+
function createInsertRule(config, keyGenerator, onInserted) {
|
|
499
|
+
return defineInputRule({
|
|
500
|
+
actions: [({ event, snapshot }) => {
|
|
501
|
+
let match = event.matches[0];
|
|
502
|
+
if (!match) return [];
|
|
503
|
+
let block = {
|
|
504
|
+
...config.buildValue({
|
|
505
|
+
keyGenerator,
|
|
506
|
+
matchText: match.text
|
|
507
|
+
}),
|
|
508
|
+
_key: keyGenerator(),
|
|
509
|
+
_type: config.blockType
|
|
510
|
+
};
|
|
511
|
+
return [
|
|
512
|
+
raise({
|
|
513
|
+
at: blockOffsetsToSelection({
|
|
514
|
+
offsets: {
|
|
515
|
+
anchor: match.targetOffsets.anchor,
|
|
516
|
+
focus: match.targetOffsets.focus
|
|
517
|
+
},
|
|
518
|
+
snapshot
|
|
519
|
+
}) ?? match.selection,
|
|
520
|
+
type: "delete.text"
|
|
521
|
+
}),
|
|
522
|
+
raise({
|
|
523
|
+
block,
|
|
524
|
+
placement: "auto",
|
|
525
|
+
select: "start",
|
|
526
|
+
type: "insert.block"
|
|
527
|
+
}),
|
|
528
|
+
effect(() => onInserted(block))
|
|
529
|
+
];
|
|
530
|
+
}],
|
|
531
|
+
on: config.pattern
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Default triggers and search keywords for block types with fixed, official
|
|
536
|
+
* names: Studio's intrinsic types (`image`, `file`), first-party plugin
|
|
537
|
+
* types (`code`, `table`, `color`, `latex`, `mux.video`), and the Media
|
|
538
|
+
* Library's `sanity.video`. Presets are deliberately presentation-only —
|
|
539
|
+
* they never add items (a type must actually be a member of the array),
|
|
540
|
+
* never set groups (partial grouping would strand unmatched items in an
|
|
541
|
+
* "Other" bucket), never affect rank, and always lose to a host's `items`
|
|
542
|
+
* entry for the same type. Opt out entirely with `presets: false`.
|
|
543
|
+
*/
|
|
544
|
+
const standardBlockPresets = [
|
|
545
|
+
{
|
|
546
|
+
keywords: [
|
|
547
|
+
"photo",
|
|
548
|
+
"picture",
|
|
549
|
+
"figure",
|
|
550
|
+
"media"
|
|
551
|
+
],
|
|
552
|
+
trigger: "/image",
|
|
553
|
+
type: "image"
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
keywords: [
|
|
557
|
+
"attachment",
|
|
558
|
+
"download",
|
|
559
|
+
"document"
|
|
560
|
+
],
|
|
561
|
+
trigger: "/file",
|
|
562
|
+
type: "file"
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
keywords: [
|
|
566
|
+
"snippet",
|
|
567
|
+
"syntax",
|
|
568
|
+
"fence",
|
|
569
|
+
"pre"
|
|
570
|
+
],
|
|
571
|
+
trigger: "/code",
|
|
572
|
+
type: "code"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
keywords: [
|
|
576
|
+
"grid",
|
|
577
|
+
"rows",
|
|
578
|
+
"columns",
|
|
579
|
+
"spreadsheet"
|
|
580
|
+
],
|
|
581
|
+
trigger: "/table",
|
|
582
|
+
type: "table"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
keywords: ["swatch", "palette"],
|
|
586
|
+
trigger: "/color",
|
|
587
|
+
type: "color"
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
keywords: [
|
|
591
|
+
"formula",
|
|
592
|
+
"equation",
|
|
593
|
+
"math",
|
|
594
|
+
"tex",
|
|
595
|
+
"katex"
|
|
596
|
+
],
|
|
597
|
+
trigger: "/latex",
|
|
598
|
+
type: "latex"
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
keywords: [
|
|
602
|
+
"video",
|
|
603
|
+
"player",
|
|
604
|
+
"mux"
|
|
605
|
+
],
|
|
606
|
+
trigger: "/video",
|
|
607
|
+
type: "mux.video"
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
keywords: [
|
|
611
|
+
"video",
|
|
612
|
+
"player",
|
|
613
|
+
"media"
|
|
614
|
+
],
|
|
615
|
+
trigger: "/video",
|
|
616
|
+
type: "sanity.video"
|
|
617
|
+
}
|
|
618
|
+
], wellKnownInputRules = [codeFenceRule({
|
|
619
|
+
blockType: "code",
|
|
620
|
+
createValue: ({ language }) => language ? { language } : {}
|
|
621
|
+
})];
|
|
622
|
+
/**
|
|
623
|
+
* Fills preset triggers and keywords into derived items that don't already
|
|
624
|
+
* have them, matching presets against each member's resolved type chain so
|
|
625
|
+
* `{type: 'image', name: 'photo'}` still gets the image preset. Host `items`
|
|
626
|
+
* metadata has already been folded in by derivePickerItems, so anything it
|
|
627
|
+
* set wins by construction.
|
|
628
|
+
*/
|
|
629
|
+
function applyPresetMetadata(items, context, presets = standardBlockPresets) {
|
|
630
|
+
let presetByType = new Map(presets.map((preset) => [preset.type, preset]));
|
|
631
|
+
return items.map((item) => {
|
|
632
|
+
if (item.action.type !== "insertBlock" || item.trigger !== void 0 && item.keywords !== void 0) return item;
|
|
633
|
+
let blockType = item.action.blockType, memberType = context.memberTypes.find((candidate) => candidate.name === blockType);
|
|
634
|
+
if (!memberType) return item;
|
|
635
|
+
let preset = typeNameChain(memberType).map((name) => presetByType.get(name)).find(Boolean);
|
|
636
|
+
return preset ? {
|
|
637
|
+
...item,
|
|
638
|
+
keywords: item.keywords ?? preset.keywords,
|
|
639
|
+
trigger: item.trigger ?? preset.trigger
|
|
640
|
+
} : item;
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Resolves what a picker row shows for an item, following the same fallback
|
|
645
|
+
* chains Studio's built-in insert menu uses: icon from the member type, its
|
|
646
|
+
* parent type, a reference target, then the generic block icon; title from
|
|
647
|
+
* the member type or its capitalized name. The curated description (from
|
|
648
|
+
* `items` metadata) wins over the schema type's own — curation is the more
|
|
649
|
+
* specific intent.
|
|
650
|
+
*/
|
|
651
|
+
function resolveItemPresentation(item, schemaType) {
|
|
652
|
+
return schemaType ? {
|
|
653
|
+
description: item.description ?? schemaType.description,
|
|
654
|
+
icon: schemaType.icon ?? schemaType.type?.icon ?? schemaType.to?.[0]?.icon ?? BlockElementIcon,
|
|
655
|
+
title: schemaType.title ?? upperFirst(schemaType.name)
|
|
656
|
+
} : {
|
|
657
|
+
description: item.description,
|
|
658
|
+
icon: item.icon ?? BlockElementIcon,
|
|
659
|
+
title: item.title || (item.action.type === "insertBlock" ? item.action.blockType : item.id)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function upperFirst(value) {
|
|
663
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
664
|
+
}
|
|
665
|
+
function BlockInsertPicker({ arrayTypeName, filter, items, labels: labelOverrides, onInsert, openOnInsert = !0, presets = !0, resolveItems, shortcut = !0 }) {
|
|
666
|
+
let editor = useEditor(), openBlockOnInsert = useOpenBlockOnInsert(), itemsContext = usePickerItemsContext(arrayTypeName), schema = useSchema(), resolveInitialValueForType = useResolveInitialValueForType(), toast = useToast(), labels = useMemo(() => resolveLabels(labelOverrides), [labelOverrides]), instanceId = useId(), listboxId = `${instanceId}-listbox`, optionId = (id) => `${instanceId}-option-${id}`, pickerItems = useMemo(() => {
|
|
667
|
+
if (!itemsContext) return [];
|
|
668
|
+
let presented = (presets ? applyPresetMetadata(derivePickerItems(itemsContext, items), itemsContext) : derivePickerItems(itemsContext, items)).map((item) => {
|
|
669
|
+
if (item.action.type !== "insertBlock") return item;
|
|
670
|
+
let presentation = resolveItemPresentation(item, memberTypeFor(itemsContext, schema, item.action.blockType));
|
|
671
|
+
return Object.assign(item, {
|
|
672
|
+
description: presentation.description,
|
|
673
|
+
icon: presentation.icon,
|
|
674
|
+
title: presentation.title
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
return resolveItems ? [...resolveItems(presented, itemsContext)] : presented;
|
|
678
|
+
}, [
|
|
679
|
+
items,
|
|
680
|
+
itemsContext,
|
|
681
|
+
presets,
|
|
682
|
+
resolveItems,
|
|
683
|
+
schema
|
|
684
|
+
]), hasItems = pickerItems.length > 0, warnedRef = useRef({});
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
if (process.env.NODE_ENV === "production") return;
|
|
687
|
+
let warned = warnedRef.current;
|
|
688
|
+
if (!itemsContext && !warned.context) {
|
|
689
|
+
warned.context = !0, console.warn("[@sanity/block-insert-picker] No Portable Text member schema context found and no `arrayTypeName` resolved to an array type — the picker is disabled for this field.");
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (itemsContext && items && !warned.unknown) {
|
|
693
|
+
let unknown = unknownMetadataTypes(itemsContext, items);
|
|
694
|
+
unknown.length > 0 && (warned.unknown = !0, console.warn(`[@sanity/block-insert-picker] \`items\` metadata references types that are not members of this array: ${unknown.join(", ")}`));
|
|
695
|
+
}
|
|
696
|
+
itemsContext && !hasItems && !warned.empty && (warned.empty = !0, console.warn("[@sanity/block-insert-picker] No insertable items: the array has no non-text-block members (and `resolveItems` added none) — the picker is disabled for this field."));
|
|
697
|
+
}, [
|
|
698
|
+
hasItems,
|
|
699
|
+
items,
|
|
700
|
+
itemsContext
|
|
701
|
+
]);
|
|
702
|
+
let [state, dispatch] = useReducer(pickerReducer, { mode: "closed" }), stateRef = useRef(state);
|
|
703
|
+
useLayoutEffect(() => {
|
|
704
|
+
stateRef.current = state;
|
|
705
|
+
});
|
|
706
|
+
let shortcutRef = useRef(shortcut);
|
|
707
|
+
useLayoutEffect(() => {
|
|
708
|
+
shortcutRef.current = shortcut;
|
|
709
|
+
});
|
|
710
|
+
let open = state.mode !== "closed", query = open ? state.query : "", mode = open ? state.mode : null, filterItems = filter ?? filterPickerItems, filterQuery = mode === "slash" ? bareQuery(query) : query, sections = useMemo(() => groupPickerItems(filterItems(pickerItems, filterQuery)), [
|
|
711
|
+
filterItems,
|
|
712
|
+
filterQuery,
|
|
713
|
+
pickerItems
|
|
714
|
+
]), filtered = useMemo(() => flattenSections(sections), [sections]), sectionOffsets = useMemo(() => {
|
|
715
|
+
let offsets = [], total = 0;
|
|
716
|
+
for (let section of sections) offsets.push(total), total += section.items.length;
|
|
717
|
+
return offsets;
|
|
718
|
+
}, [sections]), showGroupHeaders = sections.length > 1, insertInFlightRef = useRef(!1), insertItemAtIndex = async (current, idx) => {
|
|
719
|
+
if (insertInFlightRef.current) {
|
|
720
|
+
dispatch({ type: "close" });
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
insertInFlightRef.current = !0;
|
|
724
|
+
try {
|
|
725
|
+
let visible = flattenSections(groupPickerItems(filterItems(pickerItems, current.mode === "slash" ? bareQuery(current.query) : current.query))), chosen = visible[Math.max(0, Math.min(visible.length - 1, idx))];
|
|
726
|
+
if (dispatch({ type: "close" }), !chosen) return;
|
|
727
|
+
if (chosen.action.type === "custom") {
|
|
728
|
+
sendCleanupQuery(editor, {
|
|
729
|
+
anchorBlockKey: current.anchorBlockKey,
|
|
730
|
+
mode: current.mode,
|
|
731
|
+
query: current.query
|
|
732
|
+
}), chosen.action.onSelect({
|
|
733
|
+
closePicker: () => dispatch({ type: "close" }),
|
|
734
|
+
editor
|
|
735
|
+
});
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
let blockType = chosen.action.blockType, initialValue, schemaType_0 = memberTypeFor(itemsContext, schema, blockType);
|
|
739
|
+
schemaType_0 && (initialValue = await resolveInitialValueForType(schemaType_0, {}) ?? void 0);
|
|
740
|
+
let snapshot = editor.getSnapshot();
|
|
741
|
+
if (!snapshot.context.value.some((b) => b._key === current.anchorBlockKey)) {
|
|
742
|
+
toast.push({
|
|
743
|
+
closable: !0,
|
|
744
|
+
description: labels.insertAnchorRemoved,
|
|
745
|
+
status: "warning",
|
|
746
|
+
title: labels.insertError
|
|
747
|
+
});
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
insertPickerItem({
|
|
751
|
+
anchorBlockKey: current.anchorBlockKey,
|
|
752
|
+
blockType,
|
|
753
|
+
editor,
|
|
754
|
+
initialValue,
|
|
755
|
+
keyGenerator: () => snapshot.context.keyGenerator(),
|
|
756
|
+
mode: current.mode,
|
|
757
|
+
onInsertedKey: (key) => {
|
|
758
|
+
(chosen.openOnInsert ?? openOnInsert) && openBlockOnInsert(key), onInsert?.({
|
|
759
|
+
blockKey: key,
|
|
760
|
+
blockType,
|
|
761
|
+
mode: current.mode,
|
|
762
|
+
query: current.query,
|
|
763
|
+
via: "picker"
|
|
764
|
+
});
|
|
765
|
+
},
|
|
766
|
+
query: current.query
|
|
767
|
+
});
|
|
768
|
+
} catch (error) {
|
|
769
|
+
toast.push({
|
|
770
|
+
closable: !0,
|
|
771
|
+
description: error instanceof Error ? error.message : String(error),
|
|
772
|
+
status: "error",
|
|
773
|
+
title: labels.insertError
|
|
774
|
+
});
|
|
775
|
+
} finally {
|
|
776
|
+
insertInFlightRef.current = !1;
|
|
777
|
+
}
|
|
778
|
+
}, selectItemAtIndex = (idx_0) => {
|
|
779
|
+
let current_0 = stateRef.current;
|
|
780
|
+
current_0.mode !== "closed" && insertItemAtIndex(current_0, idx_0);
|
|
781
|
+
}, handleIntent = (intent) => {
|
|
782
|
+
if (intent.type === "navigate") {
|
|
783
|
+
let current_1 = stateRef.current;
|
|
784
|
+
if (current_1.mode === "closed") return;
|
|
785
|
+
let lastIndex = filtered.length - 1, base = Math.max(0, Math.min(lastIndex, current_1.highlightedIndex)), next = Math.max(0, Math.min(lastIndex, base + intent.delta));
|
|
786
|
+
dispatch({
|
|
787
|
+
index: next,
|
|
788
|
+
type: "setHighlightedIndex"
|
|
789
|
+
});
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (intent.type !== "select") {
|
|
793
|
+
dispatch(intent);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
let current_2 = stateRef.current;
|
|
797
|
+
current_2.mode !== "closed" && insertItemAtIndex(current_2, current_2.highlightedIndex);
|
|
798
|
+
}, handleIntentRef = useRef(handleIntent);
|
|
799
|
+
useLayoutEffect(() => {
|
|
800
|
+
handleIntentRef.current = handleIntent;
|
|
801
|
+
}), useEffect(() => {
|
|
802
|
+
if (!hasItems) return;
|
|
803
|
+
let behavior = createPickerBehavior({
|
|
804
|
+
getState: () => stateRef.current,
|
|
805
|
+
isShortcutEnabled: () => shortcutRef.current,
|
|
806
|
+
onIntent: (intent_0) => handleIntentRef.current(intent_0)
|
|
807
|
+
}), unregisterPicker = editor.registerBehavior({ behavior }), unregisterInsert = editor.registerBehavior({ behavior: createInsertBehavior() }), unregisterCleanup = editor.registerBehavior({ behavior: createCleanupQueryBehavior() });
|
|
808
|
+
return () => {
|
|
809
|
+
unregisterPicker(), unregisterInsert(), unregisterCleanup();
|
|
810
|
+
};
|
|
811
|
+
}, [editor, hasItems]), useEffect(() => {
|
|
812
|
+
!hasItems && stateRef.current.mode !== "closed" && dispatch({ type: "close" });
|
|
813
|
+
}, [hasItems]);
|
|
814
|
+
let highlightedIndex = open ? Math.max(0, Math.min(filtered.length - 1, state.highlightedIndex)) : 0, highlightedItem = open ? filtered[highlightedIndex] : void 0, announcement = highlightedItem ? [
|
|
815
|
+
highlightedItem.title,
|
|
816
|
+
highlightedItem.description,
|
|
817
|
+
formatPosition(labels, highlightedIndex + 1, filtered.length)
|
|
818
|
+
].filter(Boolean).join(", ") : "", [cursorRect, setCursorRect] = useState(null);
|
|
819
|
+
useLayoutEffect(() => {
|
|
820
|
+
if (!open) {
|
|
821
|
+
setCursorRect(null);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
let snapshot_0 = editor.getSnapshot(), rect = editor.dom.getSelectionRect(snapshot_0);
|
|
825
|
+
if (rect && (rect.x || rect.y || rect.width || rect.height)) {
|
|
826
|
+
setCursorRect(rect);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
let blockRect = editor.dom.getStartBlockElement(snapshot_0)?.getBoundingClientRect();
|
|
830
|
+
blockRect && setCursorRect(blockRect);
|
|
831
|
+
}, [editor, open]);
|
|
832
|
+
let popoverContentRef = useRef(null);
|
|
833
|
+
useEffect(() => {
|
|
834
|
+
if (!open) return;
|
|
835
|
+
let handlePointerDown = (event) => {
|
|
836
|
+
let target = event.target;
|
|
837
|
+
target instanceof Node && popoverContentRef.current?.contains(target) || dispatch({ type: "close" });
|
|
838
|
+
}, editorElement = editor.dom.getEditorElement(), handleFocusOut = (event_0) => {
|
|
839
|
+
let next_0 = event_0 instanceof FocusEvent ? event_0.relatedTarget : null;
|
|
840
|
+
next_0 instanceof Node && (editorElement?.contains(next_0) || popoverContentRef.current?.contains(next_0)) || dispatch({ type: "close" });
|
|
841
|
+
};
|
|
842
|
+
return document.addEventListener("pointerdown", handlePointerDown, !0), editorElement?.addEventListener("focusout", handleFocusOut), () => {
|
|
843
|
+
document.removeEventListener("pointerdown", handlePointerDown, !0), editorElement?.removeEventListener("focusout", handleFocusOut);
|
|
844
|
+
};
|
|
845
|
+
}, [editor, open]);
|
|
846
|
+
let highlightedRowRef = useRef(null), lastPointerRef = useRef(null), highlightOnPointerMove = (idx_1, x, y) => {
|
|
847
|
+
let last = lastPointerRef.current;
|
|
848
|
+
if (lastPointerRef.current = {
|
|
849
|
+
x,
|
|
850
|
+
y
|
|
851
|
+
}, !last || last.x === x && last.y === y) return;
|
|
852
|
+
let current_3 = stateRef.current;
|
|
853
|
+
current_3.mode !== "closed" && idx_1 !== current_3.highlightedIndex && dispatch({
|
|
854
|
+
index: idx_1,
|
|
855
|
+
type: "setHighlightedIndex"
|
|
856
|
+
});
|
|
857
|
+
};
|
|
858
|
+
useEffect(() => {
|
|
859
|
+
highlightedRowRef.current?.scrollIntoView({ block: "nearest" });
|
|
860
|
+
}, [highlightedIndex]);
|
|
861
|
+
let cursorElement = useMemo(() => cursorRect ? { getBoundingClientRect: () => cursorRect } : null, [cursorRect]);
|
|
862
|
+
return open ? filtered.length === 0 ? /* @__PURE__ */ jsx(Popover, {
|
|
863
|
+
constrainSize: !0,
|
|
864
|
+
content: /* @__PURE__ */ jsx(Card, {
|
|
865
|
+
"aria-live": "polite",
|
|
866
|
+
onMouseDown: preventFocusSteal,
|
|
867
|
+
padding: 3,
|
|
868
|
+
radius: 2,
|
|
869
|
+
ref: popoverContentRef,
|
|
870
|
+
role: "status",
|
|
871
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
872
|
+
muted: !0,
|
|
873
|
+
size: 1,
|
|
874
|
+
children: formatNoMatches(labels, query)
|
|
875
|
+
})
|
|
876
|
+
}),
|
|
877
|
+
fallbackPlacements: ["top-start"],
|
|
878
|
+
open: !0,
|
|
879
|
+
placement: "bottom-start",
|
|
880
|
+
portal: !0,
|
|
881
|
+
preventOverflow: !0,
|
|
882
|
+
referenceElement: cursorElement
|
|
883
|
+
}) : /* @__PURE__ */ jsx(Popover, {
|
|
884
|
+
constrainSize: !0,
|
|
885
|
+
content: /* @__PURE__ */ jsxs(Card, {
|
|
886
|
+
onMouseDown: preventFocusSteal,
|
|
887
|
+
padding: 1,
|
|
888
|
+
radius: 2,
|
|
889
|
+
ref: popoverContentRef,
|
|
890
|
+
style: {
|
|
891
|
+
display: "flex",
|
|
892
|
+
flexDirection: "column",
|
|
893
|
+
maxHeight: "100%",
|
|
894
|
+
minWidth: 300
|
|
895
|
+
},
|
|
896
|
+
children: [
|
|
897
|
+
/* @__PURE__ */ jsx("output", {
|
|
898
|
+
"aria-live": "polite",
|
|
899
|
+
style: SR_ONLY,
|
|
900
|
+
children: announcement
|
|
901
|
+
}),
|
|
902
|
+
/* @__PURE__ */ jsxs(Flex, {
|
|
903
|
+
align: "center",
|
|
904
|
+
flex: "none",
|
|
905
|
+
gap: 2,
|
|
906
|
+
padding: 2,
|
|
907
|
+
children: [/* @__PURE__ */ jsx(Box, {
|
|
908
|
+
flex: 1,
|
|
909
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
910
|
+
muted: !0,
|
|
911
|
+
size: 0,
|
|
912
|
+
style: {
|
|
913
|
+
letterSpacing: "0.05em",
|
|
914
|
+
textTransform: "uppercase"
|
|
915
|
+
},
|
|
916
|
+
weight: "medium",
|
|
917
|
+
children: labels.title
|
|
918
|
+
})
|
|
919
|
+
}), mode === "shortcut" && query !== "" ? /* @__PURE__ */ jsx(Text, {
|
|
920
|
+
muted: !0,
|
|
921
|
+
size: 1,
|
|
922
|
+
textOverflow: "ellipsis",
|
|
923
|
+
children: query
|
|
924
|
+
}) : null]
|
|
925
|
+
}),
|
|
926
|
+
/* @__PURE__ */ jsx(Box, {
|
|
927
|
+
"aria-label": labels.title,
|
|
928
|
+
id: listboxId,
|
|
929
|
+
role: "listbox",
|
|
930
|
+
style: {
|
|
931
|
+
flex: 1,
|
|
932
|
+
maxHeight: 320,
|
|
933
|
+
minHeight: 0,
|
|
934
|
+
overflowY: "auto"
|
|
935
|
+
},
|
|
936
|
+
children: sections.map((section_0, sectionIndex) => {
|
|
937
|
+
let sectionLabel = section_0.group ?? labels.ungroupedSection, sectionKey = section_0.group === null ? "ungrouped" : `group:${section_0.group}`, rows = section_0.items.map((item_0, rowIndex) => {
|
|
938
|
+
let idx_2 = (sectionOffsets[sectionIndex] ?? 0) + rowIndex, Icon = item_0.icon, isHighlighted = idx_2 === highlightedIndex;
|
|
939
|
+
return /* @__PURE__ */ jsx(Card, {
|
|
940
|
+
"aria-selected": isHighlighted,
|
|
941
|
+
id: optionId(String(idx_2)),
|
|
942
|
+
onClick: () => selectItemAtIndex(idx_2),
|
|
943
|
+
onMouseMove: (event_1) => highlightOnPointerMove(idx_2, event_1.clientX, event_1.clientY),
|
|
944
|
+
padding: 2,
|
|
945
|
+
radius: 1,
|
|
946
|
+
ref: isHighlighted ? highlightedRowRef : void 0,
|
|
947
|
+
role: "option",
|
|
948
|
+
style: { cursor: "pointer" },
|
|
949
|
+
tabIndex: -1,
|
|
950
|
+
tone: isHighlighted ? "primary" : "default",
|
|
951
|
+
children: /* @__PURE__ */ jsxs(Flex, {
|
|
952
|
+
align: "center",
|
|
953
|
+
gap: 3,
|
|
954
|
+
children: [
|
|
955
|
+
/* @__PURE__ */ jsx(Box, {
|
|
956
|
+
style: {
|
|
957
|
+
alignItems: "center",
|
|
958
|
+
display: "flex",
|
|
959
|
+
height: 22,
|
|
960
|
+
justifyContent: "center",
|
|
961
|
+
width: 22
|
|
962
|
+
},
|
|
963
|
+
children: Icon ? /* @__PURE__ */ jsx(Icon, {}) : null
|
|
964
|
+
}),
|
|
965
|
+
/* @__PURE__ */ jsxs(Stack, {
|
|
966
|
+
flex: 1,
|
|
967
|
+
gap: 2,
|
|
968
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
969
|
+
size: 1,
|
|
970
|
+
textOverflow: "ellipsis",
|
|
971
|
+
children: item_0.title
|
|
972
|
+
}), item_0.description ? /* @__PURE__ */ jsx(Text, {
|
|
973
|
+
muted: !0,
|
|
974
|
+
size: 0,
|
|
975
|
+
textOverflow: "ellipsis",
|
|
976
|
+
children: item_0.description
|
|
977
|
+
}) : null]
|
|
978
|
+
}),
|
|
979
|
+
item_0.badge ? /* @__PURE__ */ jsx(Box, {
|
|
980
|
+
flex: "none",
|
|
981
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
982
|
+
muted: !0,
|
|
983
|
+
size: 0,
|
|
984
|
+
children: item_0.badge
|
|
985
|
+
})
|
|
986
|
+
}) : null,
|
|
987
|
+
item_0.trigger ? /* @__PURE__ */ jsx(Box, {
|
|
988
|
+
flex: "none",
|
|
989
|
+
children: /* @__PURE__ */ jsx(Hotkeys, {
|
|
990
|
+
fontSize: 0,
|
|
991
|
+
keys: [item_0.trigger]
|
|
992
|
+
})
|
|
993
|
+
}) : null
|
|
994
|
+
]
|
|
995
|
+
})
|
|
996
|
+
}, `${item_0.id}-${idx_2}`);
|
|
997
|
+
});
|
|
998
|
+
return showGroupHeaders ? /* @__PURE__ */ jsxs(Box, {
|
|
999
|
+
"aria-label": sectionLabel,
|
|
1000
|
+
paddingTop: sectionIndex === 0 ? 0 : 2,
|
|
1001
|
+
role: "group",
|
|
1002
|
+
children: [/* @__PURE__ */ jsx(Box, {
|
|
1003
|
+
paddingBottom: 1,
|
|
1004
|
+
paddingTop: 2,
|
|
1005
|
+
paddingX: 2,
|
|
1006
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
1007
|
+
muted: !0,
|
|
1008
|
+
size: 0,
|
|
1009
|
+
style: {
|
|
1010
|
+
letterSpacing: "0.05em",
|
|
1011
|
+
textTransform: "uppercase"
|
|
1012
|
+
},
|
|
1013
|
+
weight: "medium",
|
|
1014
|
+
children: sectionLabel
|
|
1015
|
+
})
|
|
1016
|
+
}), /* @__PURE__ */ jsx(Stack, {
|
|
1017
|
+
gap: 1,
|
|
1018
|
+
children: rows
|
|
1019
|
+
})]
|
|
1020
|
+
}, sectionKey) : /* @__PURE__ */ jsx(Stack, {
|
|
1021
|
+
gap: 1,
|
|
1022
|
+
children: rows
|
|
1023
|
+
}, sectionKey);
|
|
1024
|
+
})
|
|
1025
|
+
}),
|
|
1026
|
+
/* @__PURE__ */ jsx(Card, {
|
|
1027
|
+
borderTop: !0,
|
|
1028
|
+
flex: "none",
|
|
1029
|
+
padding: 2,
|
|
1030
|
+
radius: 0,
|
|
1031
|
+
children: /* @__PURE__ */ jsxs(Flex, {
|
|
1032
|
+
align: "center",
|
|
1033
|
+
gap: 3,
|
|
1034
|
+
wrap: "wrap",
|
|
1035
|
+
children: [
|
|
1036
|
+
/* @__PURE__ */ jsx(FooterHint, {
|
|
1037
|
+
keys: ["↑", "↓"],
|
|
1038
|
+
label: labels.footerNavigate
|
|
1039
|
+
}),
|
|
1040
|
+
/* @__PURE__ */ jsx(FooterHint, {
|
|
1041
|
+
keys: ["↵"],
|
|
1042
|
+
label: labels.footerInsert
|
|
1043
|
+
}),
|
|
1044
|
+
/* @__PURE__ */ jsx(FooterHint, {
|
|
1045
|
+
keys: ["Esc"],
|
|
1046
|
+
label: labels.footerDismiss
|
|
1047
|
+
}),
|
|
1048
|
+
shortcut ? /* @__PURE__ */ jsx(FooterHint, {
|
|
1049
|
+
keys: [OPEN_MODIFIER, "/"],
|
|
1050
|
+
label: labels.footerAnywhere
|
|
1051
|
+
}) : null
|
|
1052
|
+
]
|
|
1053
|
+
})
|
|
1054
|
+
})
|
|
1055
|
+
]
|
|
1056
|
+
}),
|
|
1057
|
+
fallbackPlacements: ["top-start"],
|
|
1058
|
+
open: !0,
|
|
1059
|
+
placement: "bottom-start",
|
|
1060
|
+
portal: !0,
|
|
1061
|
+
preventOverflow: !0,
|
|
1062
|
+
referenceElement: cursorElement
|
|
1063
|
+
}) : null;
|
|
1064
|
+
}
|
|
1065
|
+
function FooterHint(t0) {
|
|
1066
|
+
let $ = c(7), { keys, label } = t0, t1;
|
|
1067
|
+
$[0] === keys ? t1 = $[1] : (t1 = /* @__PURE__ */ jsx(Hotkeys, {
|
|
1068
|
+
fontSize: 0,
|
|
1069
|
+
keys
|
|
1070
|
+
}), $[0] = keys, $[1] = t1);
|
|
1071
|
+
let t2;
|
|
1072
|
+
$[2] === label ? t2 = $[3] : (t2 = /* @__PURE__ */ jsx(Text, {
|
|
1073
|
+
muted: !0,
|
|
1074
|
+
size: 0,
|
|
1075
|
+
children: label
|
|
1076
|
+
}), $[2] = label, $[3] = t2);
|
|
1077
|
+
let t3;
|
|
1078
|
+
return $[4] !== t1 || $[5] !== t2 ? (t3 = /* @__PURE__ */ jsxs(Flex, {
|
|
1079
|
+
align: "center",
|
|
1080
|
+
gap: 2,
|
|
1081
|
+
children: [t1, t2]
|
|
1082
|
+
}), $[4] = t1, $[5] = t2, $[6] = t3) : t3 = $[6], t3;
|
|
1083
|
+
}
|
|
1084
|
+
function memberTypeFor(itemsContext, schema, blockType) {
|
|
1085
|
+
return itemsContext?.memberTypes.find((candidate) => candidate.name === blockType) ?? schema.get(blockType);
|
|
1086
|
+
}
|
|
1087
|
+
function bareQuery(query) {
|
|
1088
|
+
return query.startsWith("/") ? query.slice(1) : query;
|
|
1089
|
+
}
|
|
1090
|
+
function preventFocusSteal(event) {
|
|
1091
|
+
event.preventDefault();
|
|
1092
|
+
}
|
|
1093
|
+
const SR_ONLY = {
|
|
1094
|
+
border: 0,
|
|
1095
|
+
clipPath: "inset(50%)",
|
|
1096
|
+
height: 1,
|
|
1097
|
+
margin: -1,
|
|
1098
|
+
overflow: "hidden",
|
|
1099
|
+
padding: 0,
|
|
1100
|
+
position: "absolute",
|
|
1101
|
+
whiteSpace: "nowrap",
|
|
1102
|
+
width: 1
|
|
1103
|
+
}, OPEN_MODIFIER = typeof navigator < "u" && /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent || "") ? "⌘" : "Ctrl";
|
|
1104
|
+
/**
|
|
1105
|
+
* Mounts markdown input rules on the PTE and opens each inserted block for
|
|
1106
|
+
* editing once its member item mounts — the same open-on-insert behavior the
|
|
1107
|
+
* picker uses, so a fenced code block lands ready to type into.
|
|
1108
|
+
*/
|
|
1109
|
+
function MarkdownInputRules(t0) {
|
|
1110
|
+
let $ = c(15), { arrayTypeName, onInsert, openOnInsert: t1, rules } = t0, openOnInsert = t1 === void 0 || t1, editor = useEditor(), itemsContext = usePickerItemsContext(arrayTypeName), openBlockOnInsert = useOpenBlockOnInsert(), t2;
|
|
1111
|
+
$[0] === itemsContext?.memberTypes ? t2 = $[1] : (t2 = itemsContext?.memberTypes ?? [], $[0] = itemsContext?.memberTypes, $[1] = t2);
|
|
1112
|
+
let memberTypes = t2, t3;
|
|
1113
|
+
if ($[2] !== memberTypes || $[3] !== rules) {
|
|
1114
|
+
let t4;
|
|
1115
|
+
$[5] === memberTypes ? t4 = $[6] : (t4 = (rule) => {
|
|
1116
|
+
let member = memberTypes.find((candidate_0) => candidate_0.name === rule.blockType) ?? memberTypes.find((candidate) => typeNameChain(candidate).includes(rule.blockType));
|
|
1117
|
+
return member ? [rule.blockType === member.name ? rule : {
|
|
1118
|
+
...rule,
|
|
1119
|
+
blockType: member.name
|
|
1120
|
+
}] : [];
|
|
1121
|
+
}, $[5] = memberTypes, $[6] = t4), t3 = rules.flatMap(t4), $[2] = memberTypes, $[3] = rules, $[4] = t3;
|
|
1122
|
+
} else t3 = $[4];
|
|
1123
|
+
let resolvedRules = t3, onInsertedRef = useRef(_temp), t4;
|
|
1124
|
+
$[7] !== onInsert || $[8] !== openBlockOnInsert || $[9] !== openOnInsert ? (t4 = () => {
|
|
1125
|
+
onInsertedRef.current = (block) => {
|
|
1126
|
+
openOnInsert && openBlockOnInsert(block._key), onInsert?.({
|
|
1127
|
+
blockKey: block._key,
|
|
1128
|
+
blockType: block._type,
|
|
1129
|
+
via: "inputRule"
|
|
1130
|
+
});
|
|
1131
|
+
};
|
|
1132
|
+
}, $[7] = onInsert, $[8] = openBlockOnInsert, $[9] = openOnInsert, $[10] = t4) : t4 = $[10], useLayoutEffect(t4);
|
|
1133
|
+
let t5, t6;
|
|
1134
|
+
return $[11] !== editor || $[12] !== resolvedRules ? (t5 = () => {
|
|
1135
|
+
let inputRules = createMarkdownInputRules({
|
|
1136
|
+
allowedBlockTypes: new Set(resolvedRules.map(_temp2)),
|
|
1137
|
+
keyGenerator: () => editor.getSnapshot().context.keyGenerator(),
|
|
1138
|
+
onInserted: (block_0) => onInsertedRef.current(block_0),
|
|
1139
|
+
rules: resolvedRules
|
|
1140
|
+
});
|
|
1141
|
+
if (inputRules.length !== 0) return editor.registerBehavior({ behavior: defineInputRuleBehavior({ rules: inputRules }) });
|
|
1142
|
+
}, t6 = [editor, resolvedRules], $[11] = editor, $[12] = resolvedRules, $[13] = t5, $[14] = t6) : (t5 = $[13], t6 = $[14]), useEffect(t5, t6), null;
|
|
1143
|
+
}
|
|
1144
|
+
function _temp2(rule_0) {
|
|
1145
|
+
return rule_0.blockType;
|
|
1146
|
+
}
|
|
1147
|
+
function _temp() {}
|
|
1148
|
+
/**
|
|
1149
|
+
* Creates the Portable Text editor plugins component for the block-insert
|
|
1150
|
+
* picker: a slash-command / Cmd+/ menu of the blocks the host array allows,
|
|
1151
|
+
* plus optional markdown input rules. Zero-config is the intended call —
|
|
1152
|
+
* items derive from the array the plugin is mounted on. Attach it to the
|
|
1153
|
+
* array type's `components.portableText.plugins`:
|
|
1154
|
+
*
|
|
1155
|
+
* ```ts
|
|
1156
|
+
* defineType({
|
|
1157
|
+
* name: 'content',
|
|
1158
|
+
* type: 'array',
|
|
1159
|
+
* of: [{type: 'block'}, {type: 'callout'}],
|
|
1160
|
+
* components: {
|
|
1161
|
+
* portableText: {
|
|
1162
|
+
* plugins: blockInsertPicker(),
|
|
1163
|
+
* },
|
|
1164
|
+
* },
|
|
1165
|
+
* })
|
|
1166
|
+
* ```
|
|
1167
|
+
*/
|
|
1168
|
+
function blockInsertPicker(config = {}) {
|
|
1169
|
+
let { arrayTypeName, filter, inputRules, items, labels, onInsert, openOnInsert, presets, resolveItems, shortcut } = config;
|
|
1170
|
+
return function(props) {
|
|
1171
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1172
|
+
props.renderDefault(props),
|
|
1173
|
+
inputRules?.length ? /* @__PURE__ */ jsx(MarkdownInputRules, {
|
|
1174
|
+
arrayTypeName,
|
|
1175
|
+
onInsert,
|
|
1176
|
+
openOnInsert,
|
|
1177
|
+
rules: inputRules
|
|
1178
|
+
}) : null,
|
|
1179
|
+
/* @__PURE__ */ jsx(BlockInsertPicker, {
|
|
1180
|
+
arrayTypeName,
|
|
1181
|
+
filter,
|
|
1182
|
+
items,
|
|
1183
|
+
labels,
|
|
1184
|
+
onInsert,
|
|
1185
|
+
openOnInsert,
|
|
1186
|
+
presets,
|
|
1187
|
+
resolveItems,
|
|
1188
|
+
shortcut
|
|
1189
|
+
})
|
|
1190
|
+
] });
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
export { BLOCKQUOTE_PATTERN, BlockInsertPicker, CODE_FENCE_PATTERN, MarkdownInputRules, blockInsertPicker, blockquoteRule, codeFenceRule, derivePickerItems, fenceLanguageFromMatch, filterPickerItems, normalizeFenceLanguage, standardBlockPresets, wellKnownInputRules };
|
|
1194
|
+
|
|
1195
|
+
//# sourceMappingURL=index.js.map
|