@relevaince/mentions 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -13
- package/dist/index.d.mts +34 -3
- package/dist/index.d.ts +34 -3
- package/dist/index.js +182 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +182 -34
- 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
|
/**
|
|
@@ -142,15 +154,34 @@ declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps
|
|
|
142
154
|
/**
|
|
143
155
|
* Serialize a Tiptap JSON document to a markdown string.
|
|
144
156
|
*
|
|
145
|
-
* Mention nodes are encoded as `@[label](id)`
|
|
157
|
+
* Mention nodes are encoded as `@[label](id)` or `@rootLabel[label](id)` when the mention has a root (e.g. file under workspace).
|
|
146
158
|
* All other text passes through verbatim.
|
|
147
159
|
*/
|
|
148
160
|
declare function serializeToMarkdown(doc: JSONContent): string;
|
|
149
161
|
|
|
150
162
|
/**
|
|
151
|
-
* Parse a markdown string (with `@[label](id)
|
|
163
|
+
* Parse a markdown string (with `@[label](id)`, `@[label](type:id)`, or `@rootLabel[label](id)` tokens)
|
|
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
|
/**
|
|
@@ -142,15 +154,34 @@ declare const MentionsInput: React.ForwardRefExoticComponent<MentionsInputProps
|
|
|
142
154
|
/**
|
|
143
155
|
* Serialize a Tiptap JSON document to a markdown string.
|
|
144
156
|
*
|
|
145
|
-
* Mention nodes are encoded as `@[label](id)`
|
|
157
|
+
* Mention nodes are encoded as `@[label](id)` or `@rootLabel[label](id)` when the mention has a root (e.g. file under workspace).
|
|
146
158
|
* All other text passes through verbatim.
|
|
147
159
|
*/
|
|
148
160
|
declare function serializeToMarkdown(doc: JSONContent): string;
|
|
149
161
|
|
|
150
162
|
/**
|
|
151
|
-
* Parse a markdown string (with `@[label](id)
|
|
163
|
+
* Parse a markdown string (with `@[label](id)`, `@[label](type:id)`, or `@rootLabel[label](id)` tokens)
|
|
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");
|
|
@@ -78,6 +80,11 @@ var MentionNode = import_core.Node.create({
|
|
|
78
80
|
default: null,
|
|
79
81
|
parseHTML: (element) => element.getAttribute("data-type"),
|
|
80
82
|
renderHTML: (attributes) => ({ "data-type": attributes.entityType })
|
|
83
|
+
},
|
|
84
|
+
rootLabel: {
|
|
85
|
+
default: null,
|
|
86
|
+
parseHTML: (element) => element.getAttribute("data-root-label"),
|
|
87
|
+
renderHTML: (attributes) => attributes.rootLabel ? { "data-root-label": attributes.rootLabel } : {}
|
|
81
88
|
}
|
|
82
89
|
};
|
|
83
90
|
},
|
|
@@ -88,13 +95,14 @@ var MentionNode = import_core.Node.create({
|
|
|
88
95
|
const entityType = node.attrs.entityType;
|
|
89
96
|
const label = node.attrs.label;
|
|
90
97
|
const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
|
|
98
|
+
const display = `${prefix}${label}`;
|
|
91
99
|
return [
|
|
92
100
|
"span",
|
|
93
101
|
(0, import_core.mergeAttributes)(HTMLAttributes, {
|
|
94
102
|
"data-mention": "",
|
|
95
103
|
class: "mention-chip"
|
|
96
104
|
}),
|
|
97
|
-
|
|
105
|
+
display
|
|
98
106
|
];
|
|
99
107
|
},
|
|
100
108
|
renderText({ node }) {
|
|
@@ -146,6 +154,7 @@ var suggestionPluginKey = new import_state.PluginKey("mentionSuggestion");
|
|
|
146
154
|
function createSuggestionExtension(triggers, callbacksRef) {
|
|
147
155
|
return import_core2.Extension.create({
|
|
148
156
|
name: "mentionSuggestion",
|
|
157
|
+
priority: 200,
|
|
149
158
|
addProseMirrorPlugins() {
|
|
150
159
|
const editor = this.editor;
|
|
151
160
|
let active = false;
|
|
@@ -174,7 +183,8 @@ function createSuggestionExtension(triggers, callbacksRef) {
|
|
|
174
183
|
attrs: {
|
|
175
184
|
id: attrs.id,
|
|
176
185
|
label: attrs.label,
|
|
177
|
-
entityType: attrs.entityType
|
|
186
|
+
entityType: attrs.entityType,
|
|
187
|
+
rootLabel: attrs.rootLabel ?? null
|
|
178
188
|
}
|
|
179
189
|
},
|
|
180
190
|
{ type: "text", text: " " }
|
|
@@ -276,7 +286,10 @@ function serializeParagraph(node) {
|
|
|
276
286
|
if (!node.content) return "";
|
|
277
287
|
return node.content.map((child) => {
|
|
278
288
|
if (child.type === "mention") {
|
|
279
|
-
const { id, label } = child.attrs ?? {};
|
|
289
|
+
const { id, label, rootLabel } = child.attrs ?? {};
|
|
290
|
+
if (rootLabel != null && rootLabel !== "") {
|
|
291
|
+
return `@${rootLabel}[${label}](${id})`;
|
|
292
|
+
}
|
|
280
293
|
return `@[${label}](${id})`;
|
|
281
294
|
}
|
|
282
295
|
return child.text ?? "";
|
|
@@ -322,7 +335,7 @@ function extractParagraphText(node) {
|
|
|
322
335
|
}
|
|
323
336
|
|
|
324
337
|
// src/core/markdownParser.ts
|
|
325
|
-
var MENTION_RE =
|
|
338
|
+
var MENTION_RE = /@(?:([^\[]+)\[)?\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
|
|
326
339
|
function parseFromMarkdown(markdown) {
|
|
327
340
|
const lines = markdown.split("\n");
|
|
328
341
|
const content = lines.map((line) => ({
|
|
@@ -338,9 +351,10 @@ function parseLine(line) {
|
|
|
338
351
|
let match;
|
|
339
352
|
while ((match = MENTION_RE.exec(line)) !== null) {
|
|
340
353
|
const fullMatch = match[0];
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
const
|
|
354
|
+
const rootLabel = match[1] ?? null;
|
|
355
|
+
const label = match[2];
|
|
356
|
+
const entityType = match[3] ?? "unknown";
|
|
357
|
+
const id = match[4];
|
|
344
358
|
if (match.index > lastIndex) {
|
|
345
359
|
nodes.push({
|
|
346
360
|
type: "text",
|
|
@@ -352,7 +366,8 @@ function parseLine(line) {
|
|
|
352
366
|
attrs: {
|
|
353
367
|
id,
|
|
354
368
|
label,
|
|
355
|
-
entityType
|
|
369
|
+
entityType,
|
|
370
|
+
rootLabel
|
|
356
371
|
}
|
|
357
372
|
});
|
|
358
373
|
lastIndex = match.index + fullMatch.length;
|
|
@@ -368,6 +383,13 @@ function parseLine(line) {
|
|
|
368
383
|
}
|
|
369
384
|
return nodes;
|
|
370
385
|
}
|
|
386
|
+
function extractFromMarkdown(markdown) {
|
|
387
|
+
const doc = parseFromMarkdown(markdown);
|
|
388
|
+
return {
|
|
389
|
+
tokens: extractTokens(doc),
|
|
390
|
+
plainText: extractPlainText(doc)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
371
393
|
|
|
372
394
|
// src/hooks/useMentionsEditor.ts
|
|
373
395
|
function buildOutput(editor) {
|
|
@@ -381,6 +403,7 @@ function buildOutput(editor) {
|
|
|
381
403
|
function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
|
|
382
404
|
return import_core3.Extension.create({
|
|
383
405
|
name: "submitShortcut",
|
|
406
|
+
priority: 150,
|
|
384
407
|
addKeyboardShortcuts() {
|
|
385
408
|
return {
|
|
386
409
|
"Mod-Enter": () => {
|
|
@@ -396,22 +419,35 @@ function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
|
|
|
396
419
|
}
|
|
397
420
|
});
|
|
398
421
|
}
|
|
422
|
+
var enterSubmitPluginKey = new import_state2.PluginKey("enterSubmit");
|
|
399
423
|
function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
|
|
400
424
|
return import_core3.Extension.create({
|
|
401
425
|
name: "enterSubmit",
|
|
402
|
-
priority:
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
426
|
+
priority: 150,
|
|
427
|
+
addProseMirrorPlugins() {
|
|
428
|
+
const editor = this.editor;
|
|
429
|
+
return [
|
|
430
|
+
new import_state2.Plugin({
|
|
431
|
+
key: enterSubmitPluginKey,
|
|
432
|
+
props: {
|
|
433
|
+
handleKeyDown(_view, event) {
|
|
434
|
+
if (event.key !== "Enter") return false;
|
|
435
|
+
if (event.shiftKey) {
|
|
436
|
+
editor.commands.splitBlock();
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
if (event.metaKey || event.ctrlKey) return false;
|
|
440
|
+
if (onSubmitRef.current) {
|
|
441
|
+
onSubmitRef.current(buildOutput(editor));
|
|
442
|
+
if (clearOnSubmitRef.current) {
|
|
443
|
+
editor.commands.clearContent(true);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
410
447
|
}
|
|
411
448
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
};
|
|
449
|
+
})
|
|
450
|
+
];
|
|
415
451
|
}
|
|
416
452
|
});
|
|
417
453
|
}
|
|
@@ -464,10 +500,12 @@ function useMentionsEditor({
|
|
|
464
500
|
bulletList: false,
|
|
465
501
|
orderedList: false,
|
|
466
502
|
listItem: false,
|
|
467
|
-
horizontalRule: false
|
|
503
|
+
horizontalRule: false,
|
|
504
|
+
hardBreak: false
|
|
468
505
|
}),
|
|
469
506
|
import_extension_placeholder.default.configure({
|
|
470
|
-
placeholder: placeholder ?? "Type a message..."
|
|
507
|
+
placeholder: placeholder ?? "Type a message...",
|
|
508
|
+
showOnlyCurrent: false
|
|
471
509
|
}),
|
|
472
510
|
MentionNode,
|
|
473
511
|
suggestionExtension,
|
|
@@ -535,10 +573,17 @@ function useSuggestion(providers) {
|
|
|
535
573
|
);
|
|
536
574
|
const providerRef = (0, import_react3.useRef)(null);
|
|
537
575
|
const fetchItems = (0, import_react3.useCallback)(
|
|
538
|
-
async (provider, query, parent) => {
|
|
576
|
+
async (provider, query, parent, useSearchAll) => {
|
|
539
577
|
setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
|
|
540
578
|
try {
|
|
541
|
-
|
|
579
|
+
let items;
|
|
580
|
+
if (useSearchAll && provider.searchAll) {
|
|
581
|
+
items = await provider.searchAll(query);
|
|
582
|
+
} else if (parent && provider.getChildren) {
|
|
583
|
+
items = await provider.getChildren(parent, query);
|
|
584
|
+
} else {
|
|
585
|
+
items = await provider.getRootItems(query);
|
|
586
|
+
}
|
|
542
587
|
setUIState((prev) => ({
|
|
543
588
|
...prev,
|
|
544
589
|
items,
|
|
@@ -575,7 +620,11 @@ function useSuggestion(providers) {
|
|
|
575
620
|
trigger: props.trigger,
|
|
576
621
|
query: props.query
|
|
577
622
|
});
|
|
578
|
-
|
|
623
|
+
if (props.query.trim() && provider.searchAll) {
|
|
624
|
+
fetchItems(provider, props.query, void 0, true);
|
|
625
|
+
} else {
|
|
626
|
+
fetchItems(provider, props.query);
|
|
627
|
+
}
|
|
579
628
|
},
|
|
580
629
|
[fetchItems]
|
|
581
630
|
);
|
|
@@ -584,12 +633,25 @@ function useSuggestion(providers) {
|
|
|
584
633
|
const provider = providerRef.current;
|
|
585
634
|
if (!provider) return;
|
|
586
635
|
commandRef.current = props.command;
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
636
|
+
const current = stateRef.current;
|
|
637
|
+
if (current.breadcrumbs.length > 0) {
|
|
638
|
+
setUIState((prev) => ({
|
|
639
|
+
...prev,
|
|
640
|
+
breadcrumbs: [],
|
|
641
|
+
clientRect: props.clientRect,
|
|
642
|
+
query: props.query,
|
|
643
|
+
activeIndex: 0
|
|
644
|
+
}));
|
|
645
|
+
} else {
|
|
646
|
+
setUIState((prev) => ({
|
|
647
|
+
...prev,
|
|
648
|
+
clientRect: props.clientRect,
|
|
649
|
+
query: props.query
|
|
650
|
+
}));
|
|
651
|
+
}
|
|
652
|
+
if (props.query.trim() && provider.searchAll) {
|
|
653
|
+
fetchItems(provider, props.query, void 0, true);
|
|
654
|
+
} else {
|
|
593
655
|
fetchItems(provider, props.query);
|
|
594
656
|
}
|
|
595
657
|
},
|
|
@@ -631,10 +693,12 @@ function useSuggestion(providers) {
|
|
|
631
693
|
return;
|
|
632
694
|
}
|
|
633
695
|
if (commandRef.current) {
|
|
696
|
+
const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
|
|
634
697
|
commandRef.current({
|
|
635
698
|
id: selected.id,
|
|
636
699
|
label: selected.label,
|
|
637
|
-
entityType: selected.type
|
|
700
|
+
entityType: selected.type,
|
|
701
|
+
rootLabel
|
|
638
702
|
});
|
|
639
703
|
}
|
|
640
704
|
},
|
|
@@ -663,6 +727,18 @@ function useSuggestion(providers) {
|
|
|
663
727
|
const close = (0, import_react3.useCallback)(() => {
|
|
664
728
|
setUIState(IDLE_STATE);
|
|
665
729
|
}, []);
|
|
730
|
+
const searchNested = (0, import_react3.useCallback)(
|
|
731
|
+
(query) => {
|
|
732
|
+
const provider = providerRef.current;
|
|
733
|
+
if (!provider) return;
|
|
734
|
+
const current = stateRef.current;
|
|
735
|
+
const parent = current.breadcrumbs[current.breadcrumbs.length - 1];
|
|
736
|
+
if (parent) {
|
|
737
|
+
fetchItems(provider, query, parent);
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
[fetchItems]
|
|
741
|
+
);
|
|
666
742
|
const onKeyDown = (0, import_react3.useCallback)(
|
|
667
743
|
({ event }) => {
|
|
668
744
|
const current = stateRef.current;
|
|
@@ -722,7 +798,8 @@ function useSuggestion(providers) {
|
|
|
722
798
|
navigateDown,
|
|
723
799
|
select,
|
|
724
800
|
goBack,
|
|
725
|
-
close
|
|
801
|
+
close,
|
|
802
|
+
searchNested
|
|
726
803
|
};
|
|
727
804
|
return { uiState, actions, callbacksRef };
|
|
728
805
|
}
|
|
@@ -768,10 +845,29 @@ function SuggestionList({
|
|
|
768
845
|
onSelect,
|
|
769
846
|
onHover,
|
|
770
847
|
onGoBack,
|
|
848
|
+
onSearchNested,
|
|
849
|
+
onNavigateUp,
|
|
850
|
+
onNavigateDown,
|
|
851
|
+
onClose,
|
|
771
852
|
renderItem
|
|
772
853
|
}) {
|
|
773
854
|
const listRef = (0, import_react4.useRef)(null);
|
|
855
|
+
const searchInputRef = (0, import_react4.useRef)(null);
|
|
774
856
|
const depth = breadcrumbs.length;
|
|
857
|
+
const [nestedQuery, setNestedQuery] = (0, import_react4.useState)("");
|
|
858
|
+
const breadcrumbKey = breadcrumbs.map((b) => b.id).join("/");
|
|
859
|
+
const prevBreadcrumbKey = (0, import_react4.useRef)(breadcrumbKey);
|
|
860
|
+
(0, import_react4.useEffect)(() => {
|
|
861
|
+
if (prevBreadcrumbKey.current !== breadcrumbKey) {
|
|
862
|
+
setNestedQuery("");
|
|
863
|
+
prevBreadcrumbKey.current = breadcrumbKey;
|
|
864
|
+
}
|
|
865
|
+
}, [breadcrumbKey]);
|
|
866
|
+
(0, import_react4.useEffect)(() => {
|
|
867
|
+
if (breadcrumbs.length > 0 && searchInputRef.current) {
|
|
868
|
+
requestAnimationFrame(() => searchInputRef.current?.focus());
|
|
869
|
+
}
|
|
870
|
+
}, [breadcrumbKey, breadcrumbs.length]);
|
|
775
871
|
(0, import_react4.useEffect)(() => {
|
|
776
872
|
if (!listRef.current) return;
|
|
777
873
|
const active = listRef.current.querySelector('[aria-selected="true"]');
|
|
@@ -779,6 +875,35 @@ function SuggestionList({
|
|
|
779
875
|
}, [activeIndex]);
|
|
780
876
|
const style = usePopoverPosition(clientRect);
|
|
781
877
|
if (items.length === 0 && !loading) return null;
|
|
878
|
+
const handleSearchKeyDown = (e) => {
|
|
879
|
+
switch (e.key) {
|
|
880
|
+
case "ArrowDown":
|
|
881
|
+
e.preventDefault();
|
|
882
|
+
onNavigateDown?.();
|
|
883
|
+
break;
|
|
884
|
+
case "ArrowUp":
|
|
885
|
+
e.preventDefault();
|
|
886
|
+
onNavigateUp?.();
|
|
887
|
+
break;
|
|
888
|
+
case "Enter": {
|
|
889
|
+
e.preventDefault();
|
|
890
|
+
e.stopPropagation();
|
|
891
|
+
const selectedItem = items[activeIndex];
|
|
892
|
+
if (selectedItem) onSelect(selectedItem);
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
case "Escape":
|
|
896
|
+
e.preventDefault();
|
|
897
|
+
onClose?.();
|
|
898
|
+
break;
|
|
899
|
+
case "Backspace":
|
|
900
|
+
if (nestedQuery === "") {
|
|
901
|
+
e.preventDefault();
|
|
902
|
+
onGoBack();
|
|
903
|
+
}
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
};
|
|
782
907
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
783
908
|
"div",
|
|
784
909
|
{
|
|
@@ -803,6 +928,24 @@ function SuggestionList({
|
|
|
803
928
|
crumb.label
|
|
804
929
|
] }, crumb.id))
|
|
805
930
|
] }),
|
|
931
|
+
breadcrumbs.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-search": "", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
932
|
+
"input",
|
|
933
|
+
{
|
|
934
|
+
ref: searchInputRef,
|
|
935
|
+
type: "text",
|
|
936
|
+
"data-suggestion-search-input": "",
|
|
937
|
+
placeholder: "Search...",
|
|
938
|
+
value: nestedQuery,
|
|
939
|
+
onChange: (e) => {
|
|
940
|
+
const q = e.target.value;
|
|
941
|
+
setNestedQuery(q);
|
|
942
|
+
onSearchNested?.(q);
|
|
943
|
+
},
|
|
944
|
+
onKeyDown: handleSearchKeyDown,
|
|
945
|
+
autoComplete: "off",
|
|
946
|
+
spellCheck: false
|
|
947
|
+
}
|
|
948
|
+
) }),
|
|
806
949
|
loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-loading": "", children: "Loading..." }),
|
|
807
950
|
!loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...listboxAttrs(LISTBOX_ID, `${trigger ?? ""} suggestions`), children: items.map((item, index) => {
|
|
808
951
|
const isActive = index === activeIndex;
|
|
@@ -885,7 +1028,8 @@ var MentionsInput = (0, import_react5.forwardRef)(
|
|
|
885
1028
|
[clear, setContent, focus]
|
|
886
1029
|
);
|
|
887
1030
|
const isExpanded = uiState.state !== "idle";
|
|
888
|
-
const handleHover = (0, import_react5.useCallback)((
|
|
1031
|
+
const handleHover = (0, import_react5.useCallback)((index) => {
|
|
1032
|
+
void index;
|
|
889
1033
|
}, []);
|
|
890
1034
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
891
1035
|
"div",
|
|
@@ -909,6 +1053,10 @@ var MentionsInput = (0, import_react5.forwardRef)(
|
|
|
909
1053
|
onSelect: (item) => actions.select(item),
|
|
910
1054
|
onHover: handleHover,
|
|
911
1055
|
onGoBack: actions.goBack,
|
|
1056
|
+
onSearchNested: actions.searchNested,
|
|
1057
|
+
onNavigateUp: actions.navigateUp,
|
|
1058
|
+
onNavigateDown: actions.navigateDown,
|
|
1059
|
+
onClose: actions.close,
|
|
912
1060
|
renderItem
|
|
913
1061
|
}
|
|
914
1062
|
)
|
|
@@ -920,6 +1068,7 @@ var MentionsInput = (0, import_react5.forwardRef)(
|
|
|
920
1068
|
// Annotate the CommonJS export names for ESM import in node:
|
|
921
1069
|
0 && (module.exports = {
|
|
922
1070
|
MentionsInput,
|
|
1071
|
+
extractFromMarkdown,
|
|
923
1072
|
parseFromMarkdown,
|
|
924
1073
|
serializeToMarkdown
|
|
925
1074
|
});
|