@relevaince/mentions 0.2.2 → 0.3.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/README.md +41 -13
- package/dist/index.d.mts +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +164 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +164 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,12 @@ This is **not** a simple `@mention` dropdown. It is a resource-addressing langua
|
|
|
8
8
|
|
|
9
9
|
- **Multiple trigger characters** — `@`, `#`, `:`, or any custom trigger
|
|
10
10
|
- **Nested suggestions** — drill into workspaces, then pick a file inside
|
|
11
|
+
- **Cross-group search** — `searchAll` finds items across every level with a single query
|
|
12
|
+
- **Nested search** — search input inside the dropdown to filter children in real time
|
|
11
13
|
- **Async providers** — fetch suggestions from any API
|
|
12
14
|
- **Structured output** — returns `{ markdown, tokens, plainText }` on every change
|
|
13
15
|
- **Markdown serialization** — `@[label](id)` token syntax for storage and LLM context
|
|
16
|
+
- **Markdown parsing** — `extractFromMarkdown()` returns tokens + plain text from a markdown string
|
|
14
17
|
- **Headless styling** — zero bundled CSS, style via `data-*` attributes with Tailwind or plain CSS
|
|
15
18
|
- **Accessible** — full ARIA combobox pattern with keyboard navigation
|
|
16
19
|
- **SSR compatible** — safe for Next.js with `"use client"` directive
|
|
@@ -78,7 +81,7 @@ type MentionToken = {
|
|
|
78
81
|
|
|
79
82
|
### MentionProvider
|
|
80
83
|
|
|
81
|
-
Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down:
|
|
84
|
+
Register one provider per trigger character. Each provider fetches suggestions and optionally supports nested drill-down and cross-group search:
|
|
82
85
|
|
|
83
86
|
```ts
|
|
84
87
|
type MentionProvider = {
|
|
@@ -86,9 +89,12 @@ type MentionProvider = {
|
|
|
86
89
|
name: string; // human label
|
|
87
90
|
getRootItems: (query: string) => Promise<MentionItem[]>;
|
|
88
91
|
getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
|
|
92
|
+
searchAll?: (query: string) => Promise<MentionItem[]>; // flat search across all levels
|
|
89
93
|
};
|
|
90
94
|
```
|
|
91
95
|
|
|
96
|
+
When `searchAll` is provided, typing a non-empty query at the root level will call `searchAll` instead of `getRootItems`, returning a flat list of results from every level (root items and their children).
|
|
97
|
+
|
|
92
98
|
### MentionItem
|
|
93
99
|
|
|
94
100
|
Items returned by providers:
|
|
@@ -102,9 +108,12 @@ type MentionItem = {
|
|
|
102
108
|
description?: string; // secondary text
|
|
103
109
|
hasChildren?: boolean; // when true, selecting drills into a child level
|
|
104
110
|
data?: unknown;
|
|
111
|
+
rootLabel?: string; // parent label for flat search results (used in serialization)
|
|
105
112
|
};
|
|
106
113
|
```
|
|
107
114
|
|
|
115
|
+
Set `rootLabel` on items returned by `searchAll` that originate from a nested level. This ensures correct `@rootLabel[label](id)` serialization.
|
|
116
|
+
|
|
108
117
|
### MentionsOutput
|
|
109
118
|
|
|
110
119
|
Structured output returned on every change and submit:
|
|
@@ -124,7 +133,7 @@ type MentionsOutput = {
|
|
|
124
133
|
| `value` | `string` | — | Initial markdown content with `@[label](id)` tokens |
|
|
125
134
|
| `providers` | `MentionProvider[]` | **required** | Suggestion providers, one per trigger |
|
|
126
135
|
| `onChange` | `(output: MentionsOutput) => void` | — | Called on every content change |
|
|
127
|
-
| `onSubmit` | `(output: MentionsOutput) => void` | — | Called on Enter
|
|
136
|
+
| `onSubmit` | `(output: MentionsOutput) => void` | — | Called on Enter or Cmd+Enter |
|
|
128
137
|
| `placeholder` | `string` | `"Type a message..."` | Placeholder text |
|
|
129
138
|
| `autoFocus` | `boolean` | `false` | Focus editor on mount |
|
|
130
139
|
| `disabled` | `boolean` | `false` | Disable editing |
|
|
@@ -132,7 +141,6 @@ type MentionsOutput = {
|
|
|
132
141
|
| `maxLength` | `number` | — | Max plain text character count |
|
|
133
142
|
| `renderItem` | `(item, depth) => ReactNode` | — | Custom suggestion item renderer |
|
|
134
143
|
| `clearOnSubmit` | `boolean` | `true` | Auto-clear the editor after `onSubmit` fires |
|
|
135
|
-
| `renderItem` | `(item, depth) => ReactNode` | — | Custom suggestion item renderer |
|
|
136
144
|
| `renderChip` | `(token) => ReactNode` | — | Custom inline mention chip renderer |
|
|
137
145
|
|
|
138
146
|
## Imperative ref API
|
|
@@ -194,14 +202,21 @@ The killer feature. When a `MentionItem` has `hasChildren: true`, selecting it d
|
|
|
194
202
|
:web → choose provider → type query
|
|
195
203
|
```
|
|
196
204
|
|
|
205
|
+
When inside a nested level, a search input appears in the dropdown. Type to filter children in real time. Press `Backspace` on an empty search input to go back one level.
|
|
206
|
+
|
|
197
207
|
Keyboard navigation:
|
|
198
208
|
|
|
199
|
-
| Key | Action |
|
|
200
|
-
|
|
201
|
-
| `↑` `↓` | Navigate suggestions |
|
|
202
|
-
| `Enter`
|
|
203
|
-
|
|
|
204
|
-
|
|
|
209
|
+
| Key | Context | Action |
|
|
210
|
+
|-----|---------|--------|
|
|
211
|
+
| `↑` `↓` | Suggestions open | Navigate suggestions |
|
|
212
|
+
| `Enter` | Suggestions open | Select / drill into children |
|
|
213
|
+
| `→` | Suggestions open | Drill into children (if item has children) |
|
|
214
|
+
| `←` | Nested level | Go back one level |
|
|
215
|
+
| `Backspace` | Nested level, empty search | Go back one level |
|
|
216
|
+
| `Escape` | Suggestions open | Close suggestions |
|
|
217
|
+
| `Enter` | Editor (no suggestions) | Submit message |
|
|
218
|
+
| `Shift+Enter` | Editor | New line |
|
|
219
|
+
| `Cmd/Ctrl+Enter` | Editor | Submit message |
|
|
205
220
|
|
|
206
221
|
## Markdown format
|
|
207
222
|
|
|
@@ -214,13 +229,24 @@ Mentions serialize to a compact token syntax:
|
|
|
214
229
|
Use the standalone helpers for server-side processing:
|
|
215
230
|
|
|
216
231
|
```ts
|
|
217
|
-
import {
|
|
232
|
+
import {
|
|
233
|
+
serializeToMarkdown,
|
|
234
|
+
parseFromMarkdown,
|
|
235
|
+
extractFromMarkdown,
|
|
236
|
+
} from "@relevaince/mentions";
|
|
218
237
|
|
|
219
238
|
// Parse markdown into a Tiptap-compatible JSON document
|
|
220
239
|
const doc = parseFromMarkdown("Check @[NDA](contract:c_44) for risks");
|
|
221
240
|
|
|
222
241
|
// Serialize a Tiptap JSON document back to markdown
|
|
223
242
|
const md = serializeToMarkdown(doc);
|
|
243
|
+
|
|
244
|
+
// Extract tokens and plain text directly from a markdown string
|
|
245
|
+
const { tokens, plainText } = extractFromMarkdown(
|
|
246
|
+
"Summarize @Marketing[Q4 Strategy.pdf](file:file_1) for the team"
|
|
247
|
+
);
|
|
248
|
+
// tokens → [{ id: "file_1", type: "file", label: "Q4 Strategy.pdf" }]
|
|
249
|
+
// plainText → "Summarize Q4 Strategy.pdf for the team"
|
|
224
250
|
```
|
|
225
251
|
|
|
226
252
|
## Styling
|
|
@@ -238,6 +264,8 @@ Zero bundled CSS. Every element exposes `data-*` attributes for styling:
|
|
|
238
264
|
[data-suggestion-item] { /* each item */ }
|
|
239
265
|
[data-suggestion-item-active] { /* highlighted item */ }
|
|
240
266
|
[data-suggestion-breadcrumb] { /* breadcrumb bar (nested) */ }
|
|
267
|
+
[data-suggestion-search] { /* search input wrapper (nested) */ }
|
|
268
|
+
[data-suggestion-search-input] { /* search input field */ }
|
|
241
269
|
[data-suggestion-loading] { /* loading indicator */ }
|
|
242
270
|
```
|
|
243
271
|
|
|
@@ -269,9 +297,9 @@ Example with Tailwind:
|
|
|
269
297
|
├── Tiptap Editor (ProseMirror)
|
|
270
298
|
│ ├── MentionNode Extension — inline atom nodes with id/label/type
|
|
271
299
|
│ ├── Suggestion Plugin — multi-trigger detection + popover callbacks
|
|
272
|
-
│ ├── Submit Extension
|
|
273
|
-
│ └── Markdown Serializer
|
|
274
|
-
└── SuggestionList (React) — headless popover with ARIA
|
|
300
|
+
│ ├── Enter/Submit Extension — Enter submits, Shift+Enter new line
|
|
301
|
+
│ └── Markdown Parser/Serializer — doc ↔ @[label](id) + extractFromMarkdown
|
|
302
|
+
└── SuggestionList (React) — headless popover with ARIA + nested search
|
|
275
303
|
```
|
|
276
304
|
|
|
277
305
|
Consumers interact with a single React component. Tiptap and ProseMirror are internal implementation details.
|
package/dist/index.d.mts
CHANGED
|
@@ -19,6 +19,12 @@ type MentionItem = {
|
|
|
19
19
|
hasChildren?: boolean;
|
|
20
20
|
/** Optional payload carried through to `MentionToken.data`. */
|
|
21
21
|
data?: unknown;
|
|
22
|
+
/**
|
|
23
|
+
* Root parent label for items returned by `searchAll`.
|
|
24
|
+
* Used for correct `@rootLabel[label](id)` serialization when the
|
|
25
|
+
* item originates from a nested level.
|
|
26
|
+
*/
|
|
27
|
+
rootLabel?: string;
|
|
22
28
|
};
|
|
23
29
|
/**
|
|
24
30
|
* Hierarchical suggestion source registered per trigger character.
|
|
@@ -36,6 +42,12 @@ type MentionProvider = {
|
|
|
36
42
|
getRootItems: (query: string) => Promise<MentionItem[]>;
|
|
37
43
|
/** Fetch child suggestions when the user drills into `parent`. */
|
|
38
44
|
getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Flat search across **all** items, including nested children.
|
|
47
|
+
* When provided and the user types a non-empty query at root level,
|
|
48
|
+
* results from `searchAll` are shown instead of `getRootItems`.
|
|
49
|
+
*/
|
|
50
|
+
searchAll?: (query: string) => Promise<MentionItem[]>;
|
|
39
51
|
};
|
|
40
52
|
|
|
41
53
|
/**
|
|
@@ -152,5 +164,24 @@ declare function serializeToMarkdown(doc: JSONContent): string;
|
|
|
152
164
|
* into a Tiptap-compatible JSON document.
|
|
153
165
|
*/
|
|
154
166
|
declare function parseFromMarkdown(markdown: string): JSONContent;
|
|
167
|
+
/**
|
|
168
|
+
* Parse a markdown string and extract structured data from it.
|
|
169
|
+
*
|
|
170
|
+
* This is a convenience wrapper that combines `parseFromMarkdown`,
|
|
171
|
+
* `extractTokens`, and `extractPlainText` into a single call.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* const { tokens, plainText } = extractFromMarkdown(
|
|
176
|
+
* "Check @Marketing[Q4 Strategy.pdf](file_1) for details"
|
|
177
|
+
* );
|
|
178
|
+
* // tokens → [{ id: "file_1", type: "unknown", label: "Q4 Strategy.pdf" }]
|
|
179
|
+
* // plainText → "Check Q4 Strategy.pdf for details"
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
declare function extractFromMarkdown(markdown: string): {
|
|
183
|
+
tokens: MentionToken[];
|
|
184
|
+
plainText: string;
|
|
185
|
+
};
|
|
155
186
|
|
|
156
|
-
export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };
|
|
187
|
+
export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, extractFromMarkdown, parseFromMarkdown, serializeToMarkdown };
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ type MentionItem = {
|
|
|
19
19
|
hasChildren?: boolean;
|
|
20
20
|
/** Optional payload carried through to `MentionToken.data`. */
|
|
21
21
|
data?: unknown;
|
|
22
|
+
/**
|
|
23
|
+
* Root parent label for items returned by `searchAll`.
|
|
24
|
+
* Used for correct `@rootLabel[label](id)` serialization when the
|
|
25
|
+
* item originates from a nested level.
|
|
26
|
+
*/
|
|
27
|
+
rootLabel?: string;
|
|
22
28
|
};
|
|
23
29
|
/**
|
|
24
30
|
* Hierarchical suggestion source registered per trigger character.
|
|
@@ -36,6 +42,12 @@ type MentionProvider = {
|
|
|
36
42
|
getRootItems: (query: string) => Promise<MentionItem[]>;
|
|
37
43
|
/** Fetch child suggestions when the user drills into `parent`. */
|
|
38
44
|
getChildren?: (parent: MentionItem, query: string) => Promise<MentionItem[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Flat search across **all** items, including nested children.
|
|
47
|
+
* When provided and the user types a non-empty query at root level,
|
|
48
|
+
* results from `searchAll` are shown instead of `getRootItems`.
|
|
49
|
+
*/
|
|
50
|
+
searchAll?: (query: string) => Promise<MentionItem[]>;
|
|
39
51
|
};
|
|
40
52
|
|
|
41
53
|
/**
|
|
@@ -152,5 +164,24 @@ declare function serializeToMarkdown(doc: JSONContent): string;
|
|
|
152
164
|
* into a Tiptap-compatible JSON document.
|
|
153
165
|
*/
|
|
154
166
|
declare function parseFromMarkdown(markdown: string): JSONContent;
|
|
167
|
+
/**
|
|
168
|
+
* Parse a markdown string and extract structured data from it.
|
|
169
|
+
*
|
|
170
|
+
* This is a convenience wrapper that combines `parseFromMarkdown`,
|
|
171
|
+
* `extractTokens`, and `extractPlainText` into a single call.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* const { tokens, plainText } = extractFromMarkdown(
|
|
176
|
+
* "Check @Marketing[Q4 Strategy.pdf](file_1) for details"
|
|
177
|
+
* );
|
|
178
|
+
* // tokens → [{ id: "file_1", type: "unknown", label: "Q4 Strategy.pdf" }]
|
|
179
|
+
* // plainText → "Check Q4 Strategy.pdf for details"
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
declare function extractFromMarkdown(markdown: string): {
|
|
183
|
+
tokens: MentionToken[];
|
|
184
|
+
plainText: string;
|
|
185
|
+
};
|
|
155
186
|
|
|
156
|
-
export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, parseFromMarkdown, serializeToMarkdown };
|
|
187
|
+
export { type MentionItem, type MentionProvider, type MentionToken, MentionsInput, type MentionsInputHandle, type MentionsInputProps, type MentionsOutput, extractFromMarkdown, parseFromMarkdown, serializeToMarkdown };
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
MentionsInput: () => MentionsInput,
|
|
34
|
+
extractFromMarkdown: () => extractFromMarkdown,
|
|
34
35
|
parseFromMarkdown: () => parseFromMarkdown,
|
|
35
36
|
serializeToMarkdown: () => serializeToMarkdown
|
|
36
37
|
});
|
|
@@ -46,6 +47,7 @@ var import_react2 = require("@tiptap/react");
|
|
|
46
47
|
var import_starter_kit = __toESM(require("@tiptap/starter-kit"));
|
|
47
48
|
var import_extension_placeholder = __toESM(require("@tiptap/extension-placeholder"));
|
|
48
49
|
var import_core3 = require("@tiptap/core");
|
|
50
|
+
var import_state2 = require("@tiptap/pm/state");
|
|
49
51
|
|
|
50
52
|
// src/core/mentionExtension.ts
|
|
51
53
|
var import_core = require("@tiptap/core");
|
|
@@ -152,6 +154,7 @@ var suggestionPluginKey = new import_state.PluginKey("mentionSuggestion");
|
|
|
152
154
|
function createSuggestionExtension(triggers, callbacksRef) {
|
|
153
155
|
return import_core2.Extension.create({
|
|
154
156
|
name: "mentionSuggestion",
|
|
157
|
+
priority: 200,
|
|
155
158
|
addProseMirrorPlugins() {
|
|
156
159
|
const editor = this.editor;
|
|
157
160
|
let active = false;
|
|
@@ -335,10 +338,10 @@ function extractParagraphText(node) {
|
|
|
335
338
|
var MENTION_RE = /@(?:([^\[]+)\[)?\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
|
|
336
339
|
function parseFromMarkdown(markdown) {
|
|
337
340
|
const lines = markdown.split("\n");
|
|
338
|
-
const content = lines.map((line) =>
|
|
339
|
-
|
|
340
|
-
content:
|
|
341
|
-
})
|
|
341
|
+
const content = lines.map((line) => {
|
|
342
|
+
const children = parseLine(line);
|
|
343
|
+
return children.length > 0 ? { type: "paragraph", content: children } : { type: "paragraph" };
|
|
344
|
+
});
|
|
342
345
|
return { type: "doc", content };
|
|
343
346
|
}
|
|
344
347
|
function parseLine(line) {
|
|
@@ -375,11 +378,15 @@ function parseLine(line) {
|
|
|
375
378
|
text: line.slice(lastIndex)
|
|
376
379
|
});
|
|
377
380
|
}
|
|
378
|
-
if (nodes.length === 0) {
|
|
379
|
-
nodes.push({ type: "text", text: "" });
|
|
380
|
-
}
|
|
381
381
|
return nodes;
|
|
382
382
|
}
|
|
383
|
+
function extractFromMarkdown(markdown) {
|
|
384
|
+
const doc = parseFromMarkdown(markdown);
|
|
385
|
+
return {
|
|
386
|
+
tokens: extractTokens(doc),
|
|
387
|
+
plainText: extractPlainText(doc)
|
|
388
|
+
};
|
|
389
|
+
}
|
|
383
390
|
|
|
384
391
|
// src/hooks/useMentionsEditor.ts
|
|
385
392
|
function buildOutput(editor) {
|
|
@@ -393,6 +400,7 @@ function buildOutput(editor) {
|
|
|
393
400
|
function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
|
|
394
401
|
return import_core3.Extension.create({
|
|
395
402
|
name: "submitShortcut",
|
|
403
|
+
priority: 150,
|
|
396
404
|
addKeyboardShortcuts() {
|
|
397
405
|
return {
|
|
398
406
|
"Mod-Enter": () => {
|
|
@@ -408,22 +416,35 @@ function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
|
|
|
408
416
|
}
|
|
409
417
|
});
|
|
410
418
|
}
|
|
419
|
+
var enterSubmitPluginKey = new import_state2.PluginKey("enterSubmit");
|
|
411
420
|
function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
|
|
412
421
|
return import_core3.Extension.create({
|
|
413
422
|
name: "enterSubmit",
|
|
414
|
-
priority:
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
423
|
+
priority: 150,
|
|
424
|
+
addProseMirrorPlugins() {
|
|
425
|
+
const editor = this.editor;
|
|
426
|
+
return [
|
|
427
|
+
new import_state2.Plugin({
|
|
428
|
+
key: enterSubmitPluginKey,
|
|
429
|
+
props: {
|
|
430
|
+
handleKeyDown(_view, event) {
|
|
431
|
+
if (event.key !== "Enter") return false;
|
|
432
|
+
if (event.shiftKey) {
|
|
433
|
+
editor.commands.splitBlock();
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
if (event.metaKey || event.ctrlKey) return false;
|
|
437
|
+
if (onSubmitRef.current) {
|
|
438
|
+
onSubmitRef.current(buildOutput(editor));
|
|
439
|
+
if (clearOnSubmitRef.current) {
|
|
440
|
+
editor.commands.clearContent(true);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return true;
|
|
422
444
|
}
|
|
423
445
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
};
|
|
446
|
+
})
|
|
447
|
+
];
|
|
427
448
|
}
|
|
428
449
|
});
|
|
429
450
|
}
|
|
@@ -476,10 +497,12 @@ function useMentionsEditor({
|
|
|
476
497
|
bulletList: false,
|
|
477
498
|
orderedList: false,
|
|
478
499
|
listItem: false,
|
|
479
|
-
horizontalRule: false
|
|
500
|
+
horizontalRule: false,
|
|
501
|
+
hardBreak: false
|
|
480
502
|
}),
|
|
481
503
|
import_extension_placeholder.default.configure({
|
|
482
|
-
placeholder: placeholder ?? "Type a message..."
|
|
504
|
+
placeholder: placeholder ?? "Type a message...",
|
|
505
|
+
showOnlyCurrent: false
|
|
483
506
|
}),
|
|
484
507
|
MentionNode,
|
|
485
508
|
suggestionExtension,
|
|
@@ -547,10 +570,17 @@ function useSuggestion(providers) {
|
|
|
547
570
|
);
|
|
548
571
|
const providerRef = (0, import_react3.useRef)(null);
|
|
549
572
|
const fetchItems = (0, import_react3.useCallback)(
|
|
550
|
-
async (provider, query, parent) => {
|
|
573
|
+
async (provider, query, parent, useSearchAll) => {
|
|
551
574
|
setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
|
|
552
575
|
try {
|
|
553
|
-
|
|
576
|
+
let items;
|
|
577
|
+
if (useSearchAll && provider.searchAll) {
|
|
578
|
+
items = await provider.searchAll(query);
|
|
579
|
+
} else if (parent && provider.getChildren) {
|
|
580
|
+
items = await provider.getChildren(parent, query);
|
|
581
|
+
} else {
|
|
582
|
+
items = await provider.getRootItems(query);
|
|
583
|
+
}
|
|
554
584
|
setUIState((prev) => ({
|
|
555
585
|
...prev,
|
|
556
586
|
items,
|
|
@@ -587,7 +617,11 @@ function useSuggestion(providers) {
|
|
|
587
617
|
trigger: props.trigger,
|
|
588
618
|
query: props.query
|
|
589
619
|
});
|
|
590
|
-
|
|
620
|
+
if (props.query.trim() && provider.searchAll) {
|
|
621
|
+
fetchItems(provider, props.query, void 0, true);
|
|
622
|
+
} else {
|
|
623
|
+
fetchItems(provider, props.query);
|
|
624
|
+
}
|
|
591
625
|
},
|
|
592
626
|
[fetchItems]
|
|
593
627
|
);
|
|
@@ -596,12 +630,25 @@ function useSuggestion(providers) {
|
|
|
596
630
|
const provider = providerRef.current;
|
|
597
631
|
if (!provider) return;
|
|
598
632
|
commandRef.current = props.command;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
633
|
+
const current = stateRef.current;
|
|
634
|
+
if (current.breadcrumbs.length > 0) {
|
|
635
|
+
setUIState((prev) => ({
|
|
636
|
+
...prev,
|
|
637
|
+
breadcrumbs: [],
|
|
638
|
+
clientRect: props.clientRect,
|
|
639
|
+
query: props.query,
|
|
640
|
+
activeIndex: 0
|
|
641
|
+
}));
|
|
642
|
+
} else {
|
|
643
|
+
setUIState((prev) => ({
|
|
644
|
+
...prev,
|
|
645
|
+
clientRect: props.clientRect,
|
|
646
|
+
query: props.query
|
|
647
|
+
}));
|
|
648
|
+
}
|
|
649
|
+
if (props.query.trim() && provider.searchAll) {
|
|
650
|
+
fetchItems(provider, props.query, void 0, true);
|
|
651
|
+
} else {
|
|
605
652
|
fetchItems(provider, props.query);
|
|
606
653
|
}
|
|
607
654
|
},
|
|
@@ -643,7 +690,7 @@ function useSuggestion(providers) {
|
|
|
643
690
|
return;
|
|
644
691
|
}
|
|
645
692
|
if (commandRef.current) {
|
|
646
|
-
const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : null;
|
|
693
|
+
const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
|
|
647
694
|
commandRef.current({
|
|
648
695
|
id: selected.id,
|
|
649
696
|
label: selected.label,
|
|
@@ -677,6 +724,18 @@ function useSuggestion(providers) {
|
|
|
677
724
|
const close = (0, import_react3.useCallback)(() => {
|
|
678
725
|
setUIState(IDLE_STATE);
|
|
679
726
|
}, []);
|
|
727
|
+
const searchNested = (0, import_react3.useCallback)(
|
|
728
|
+
(query) => {
|
|
729
|
+
const provider = providerRef.current;
|
|
730
|
+
if (!provider) return;
|
|
731
|
+
const current = stateRef.current;
|
|
732
|
+
const parent = current.breadcrumbs[current.breadcrumbs.length - 1];
|
|
733
|
+
if (parent) {
|
|
734
|
+
fetchItems(provider, query, parent);
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
[fetchItems]
|
|
738
|
+
);
|
|
680
739
|
const onKeyDown = (0, import_react3.useCallback)(
|
|
681
740
|
({ event }) => {
|
|
682
741
|
const current = stateRef.current;
|
|
@@ -736,7 +795,8 @@ function useSuggestion(providers) {
|
|
|
736
795
|
navigateDown,
|
|
737
796
|
select,
|
|
738
797
|
goBack,
|
|
739
|
-
close
|
|
798
|
+
close,
|
|
799
|
+
searchNested
|
|
740
800
|
};
|
|
741
801
|
return { uiState, actions, callbacksRef };
|
|
742
802
|
}
|
|
@@ -782,10 +842,29 @@ function SuggestionList({
|
|
|
782
842
|
onSelect,
|
|
783
843
|
onHover,
|
|
784
844
|
onGoBack,
|
|
845
|
+
onSearchNested,
|
|
846
|
+
onNavigateUp,
|
|
847
|
+
onNavigateDown,
|
|
848
|
+
onClose,
|
|
785
849
|
renderItem
|
|
786
850
|
}) {
|
|
787
851
|
const listRef = (0, import_react4.useRef)(null);
|
|
852
|
+
const searchInputRef = (0, import_react4.useRef)(null);
|
|
788
853
|
const depth = breadcrumbs.length;
|
|
854
|
+
const [nestedQuery, setNestedQuery] = (0, import_react4.useState)("");
|
|
855
|
+
const breadcrumbKey = breadcrumbs.map((b) => b.id).join("/");
|
|
856
|
+
const prevBreadcrumbKey = (0, import_react4.useRef)(breadcrumbKey);
|
|
857
|
+
(0, import_react4.useEffect)(() => {
|
|
858
|
+
if (prevBreadcrumbKey.current !== breadcrumbKey) {
|
|
859
|
+
setNestedQuery("");
|
|
860
|
+
prevBreadcrumbKey.current = breadcrumbKey;
|
|
861
|
+
}
|
|
862
|
+
}, [breadcrumbKey]);
|
|
863
|
+
(0, import_react4.useEffect)(() => {
|
|
864
|
+
if (breadcrumbs.length > 0 && searchInputRef.current) {
|
|
865
|
+
requestAnimationFrame(() => searchInputRef.current?.focus());
|
|
866
|
+
}
|
|
867
|
+
}, [breadcrumbKey, breadcrumbs.length]);
|
|
789
868
|
(0, import_react4.useEffect)(() => {
|
|
790
869
|
if (!listRef.current) return;
|
|
791
870
|
const active = listRef.current.querySelector('[aria-selected="true"]');
|
|
@@ -793,6 +872,35 @@ function SuggestionList({
|
|
|
793
872
|
}, [activeIndex]);
|
|
794
873
|
const style = usePopoverPosition(clientRect);
|
|
795
874
|
if (items.length === 0 && !loading) return null;
|
|
875
|
+
const handleSearchKeyDown = (e) => {
|
|
876
|
+
switch (e.key) {
|
|
877
|
+
case "ArrowDown":
|
|
878
|
+
e.preventDefault();
|
|
879
|
+
onNavigateDown?.();
|
|
880
|
+
break;
|
|
881
|
+
case "ArrowUp":
|
|
882
|
+
e.preventDefault();
|
|
883
|
+
onNavigateUp?.();
|
|
884
|
+
break;
|
|
885
|
+
case "Enter": {
|
|
886
|
+
e.preventDefault();
|
|
887
|
+
e.stopPropagation();
|
|
888
|
+
const selectedItem = items[activeIndex];
|
|
889
|
+
if (selectedItem) onSelect(selectedItem);
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
892
|
+
case "Escape":
|
|
893
|
+
e.preventDefault();
|
|
894
|
+
onClose?.();
|
|
895
|
+
break;
|
|
896
|
+
case "Backspace":
|
|
897
|
+
if (nestedQuery === "") {
|
|
898
|
+
e.preventDefault();
|
|
899
|
+
onGoBack();
|
|
900
|
+
}
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
};
|
|
796
904
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
797
905
|
"div",
|
|
798
906
|
{
|
|
@@ -817,6 +925,24 @@ function SuggestionList({
|
|
|
817
925
|
crumb.label
|
|
818
926
|
] }, crumb.id))
|
|
819
927
|
] }),
|
|
928
|
+
breadcrumbs.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-search": "", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
929
|
+
"input",
|
|
930
|
+
{
|
|
931
|
+
ref: searchInputRef,
|
|
932
|
+
type: "text",
|
|
933
|
+
"data-suggestion-search-input": "",
|
|
934
|
+
placeholder: "Search...",
|
|
935
|
+
value: nestedQuery,
|
|
936
|
+
onChange: (e) => {
|
|
937
|
+
const q = e.target.value;
|
|
938
|
+
setNestedQuery(q);
|
|
939
|
+
onSearchNested?.(q);
|
|
940
|
+
},
|
|
941
|
+
onKeyDown: handleSearchKeyDown,
|
|
942
|
+
autoComplete: "off",
|
|
943
|
+
spellCheck: false
|
|
944
|
+
}
|
|
945
|
+
) }),
|
|
820
946
|
loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-loading": "", children: "Loading..." }),
|
|
821
947
|
!loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...listboxAttrs(LISTBOX_ID, `${trigger ?? ""} suggestions`), children: items.map((item, index) => {
|
|
822
948
|
const isActive = index === activeIndex;
|
|
@@ -899,7 +1025,8 @@ var MentionsInput = (0, import_react5.forwardRef)(
|
|
|
899
1025
|
[clear, setContent, focus]
|
|
900
1026
|
);
|
|
901
1027
|
const isExpanded = uiState.state !== "idle";
|
|
902
|
-
const handleHover = (0, import_react5.useCallback)((
|
|
1028
|
+
const handleHover = (0, import_react5.useCallback)((index) => {
|
|
1029
|
+
void index;
|
|
903
1030
|
}, []);
|
|
904
1031
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
905
1032
|
"div",
|
|
@@ -923,6 +1050,10 @@ var MentionsInput = (0, import_react5.forwardRef)(
|
|
|
923
1050
|
onSelect: (item) => actions.select(item),
|
|
924
1051
|
onHover: handleHover,
|
|
925
1052
|
onGoBack: actions.goBack,
|
|
1053
|
+
onSearchNested: actions.searchNested,
|
|
1054
|
+
onNavigateUp: actions.navigateUp,
|
|
1055
|
+
onNavigateDown: actions.navigateDown,
|
|
1056
|
+
onClose: actions.close,
|
|
926
1057
|
renderItem
|
|
927
1058
|
}
|
|
928
1059
|
)
|
|
@@ -934,6 +1065,7 @@ var MentionsInput = (0, import_react5.forwardRef)(
|
|
|
934
1065
|
// Annotate the CommonJS export names for ESM import in node:
|
|
935
1066
|
0 && (module.exports = {
|
|
936
1067
|
MentionsInput,
|
|
1068
|
+
extractFromMarkdown,
|
|
937
1069
|
parseFromMarkdown,
|
|
938
1070
|
serializeToMarkdown
|
|
939
1071
|
});
|