@relevaince/mentions 0.1.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 +245 -0
- package/dist/index.d.mts +127 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +897 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +858 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
MentionsInput: () => MentionsInput,
|
|
34
|
+
parseFromMarkdown: () => parseFromMarkdown,
|
|
35
|
+
serializeToMarkdown: () => serializeToMarkdown
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// src/components/MentionsInput.tsx
|
|
40
|
+
var import_react5 = require("react");
|
|
41
|
+
var import_react6 = require("@tiptap/react");
|
|
42
|
+
|
|
43
|
+
// src/hooks/useMentionsEditor.ts
|
|
44
|
+
var import_react = require("react");
|
|
45
|
+
var import_react2 = require("@tiptap/react");
|
|
46
|
+
var import_starter_kit = __toESM(require("@tiptap/starter-kit"));
|
|
47
|
+
var import_core3 = require("@tiptap/core");
|
|
48
|
+
|
|
49
|
+
// src/core/mentionExtension.ts
|
|
50
|
+
var import_core = require("@tiptap/core");
|
|
51
|
+
var DEFAULT_PREFIXES = {
|
|
52
|
+
workspace: "@",
|
|
53
|
+
contract: "@",
|
|
54
|
+
file: "#",
|
|
55
|
+
web: ":"
|
|
56
|
+
};
|
|
57
|
+
var MentionNode = import_core.Node.create({
|
|
58
|
+
name: "mention",
|
|
59
|
+
group: "inline",
|
|
60
|
+
inline: true,
|
|
61
|
+
atom: true,
|
|
62
|
+
selectable: true,
|
|
63
|
+
draggable: false,
|
|
64
|
+
addAttributes() {
|
|
65
|
+
return {
|
|
66
|
+
id: {
|
|
67
|
+
default: null,
|
|
68
|
+
parseHTML: (element) => element.getAttribute("data-id"),
|
|
69
|
+
renderHTML: (attributes) => ({ "data-id": attributes.id })
|
|
70
|
+
},
|
|
71
|
+
label: {
|
|
72
|
+
default: null,
|
|
73
|
+
parseHTML: (element) => element.getAttribute("data-label"),
|
|
74
|
+
renderHTML: (attributes) => ({ "data-label": attributes.label })
|
|
75
|
+
},
|
|
76
|
+
entityType: {
|
|
77
|
+
default: null,
|
|
78
|
+
parseHTML: (element) => element.getAttribute("data-type"),
|
|
79
|
+
renderHTML: (attributes) => ({ "data-type": attributes.entityType })
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
parseHTML() {
|
|
84
|
+
return [{ tag: "span[data-mention]" }];
|
|
85
|
+
},
|
|
86
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
87
|
+
const entityType = node.attrs.entityType;
|
|
88
|
+
const label = node.attrs.label;
|
|
89
|
+
const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
|
|
90
|
+
return [
|
|
91
|
+
"span",
|
|
92
|
+
(0, import_core.mergeAttributes)(HTMLAttributes, {
|
|
93
|
+
"data-mention": "",
|
|
94
|
+
class: "mention-chip"
|
|
95
|
+
}),
|
|
96
|
+
`${prefix}${label}`
|
|
97
|
+
];
|
|
98
|
+
},
|
|
99
|
+
renderText({ node }) {
|
|
100
|
+
const entityType = node.attrs.entityType;
|
|
101
|
+
const label = node.attrs.label;
|
|
102
|
+
const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
|
|
103
|
+
return `${prefix}${label}`;
|
|
104
|
+
},
|
|
105
|
+
addKeyboardShortcuts() {
|
|
106
|
+
return {
|
|
107
|
+
Backspace: () => this.editor.commands.command(({ tr, state }) => {
|
|
108
|
+
let isMention = false;
|
|
109
|
+
const { selection } = state;
|
|
110
|
+
const { empty, anchor } = selection;
|
|
111
|
+
if (!empty) return false;
|
|
112
|
+
state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => {
|
|
113
|
+
if (node.type.name === this.name) {
|
|
114
|
+
isMention = true;
|
|
115
|
+
tr.insertText("", pos, pos + node.nodeSize);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
return isMention;
|
|
119
|
+
})
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// src/core/suggestionPlugin.ts
|
|
125
|
+
var import_core2 = require("@tiptap/core");
|
|
126
|
+
var import_state = require("@tiptap/pm/state");
|
|
127
|
+
function detectTrigger(text, cursorPos, docStartPos, triggers) {
|
|
128
|
+
const relCursor = cursorPos - docStartPos;
|
|
129
|
+
const before = text.slice(0, relCursor);
|
|
130
|
+
for (let i = before.length - 1; i >= 0; i--) {
|
|
131
|
+
const ch = before[i];
|
|
132
|
+
if (ch === "\n") return null;
|
|
133
|
+
for (const trigger of triggers) {
|
|
134
|
+
if (before.substring(i, i + trigger.length) === trigger) {
|
|
135
|
+
if (i === 0 || /\s/.test(before[i - 1])) {
|
|
136
|
+
const query = before.slice(i + trigger.length);
|
|
137
|
+
return { trigger, query, from: docStartPos + i, to: cursorPos };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
var suggestionPluginKey = new import_state.PluginKey("mentionSuggestion");
|
|
145
|
+
function createSuggestionExtension(triggers, callbacksRef) {
|
|
146
|
+
return import_core2.Extension.create({
|
|
147
|
+
name: "mentionSuggestion",
|
|
148
|
+
addProseMirrorPlugins() {
|
|
149
|
+
const editor = this.editor;
|
|
150
|
+
let active = false;
|
|
151
|
+
let lastQuery = null;
|
|
152
|
+
let lastTrigger = null;
|
|
153
|
+
const getClientRect = (view, from) => {
|
|
154
|
+
return () => {
|
|
155
|
+
try {
|
|
156
|
+
const coords = view.coordsAtPos(from);
|
|
157
|
+
return new DOMRect(
|
|
158
|
+
coords.left,
|
|
159
|
+
coords.top,
|
|
160
|
+
0,
|
|
161
|
+
coords.bottom - coords.top
|
|
162
|
+
);
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
const makeCommand = (range) => {
|
|
169
|
+
return (attrs) => {
|
|
170
|
+
editor.chain().focus().insertContentAt(range, [
|
|
171
|
+
{
|
|
172
|
+
type: "mention",
|
|
173
|
+
attrs: {
|
|
174
|
+
id: attrs.id,
|
|
175
|
+
label: attrs.label,
|
|
176
|
+
entityType: attrs.entityType
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{ type: "text", text: " " }
|
|
180
|
+
]).run();
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
const plugin = new import_state.Plugin({
|
|
184
|
+
key: suggestionPluginKey,
|
|
185
|
+
props: {
|
|
186
|
+
handleKeyDown(_view, event) {
|
|
187
|
+
if (!active) return false;
|
|
188
|
+
return callbacksRef.current.onKeyDown({ event });
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
view() {
|
|
192
|
+
return {
|
|
193
|
+
update(view, _prevState) {
|
|
194
|
+
const { state } = view;
|
|
195
|
+
const { selection } = state;
|
|
196
|
+
if (!selection.empty) {
|
|
197
|
+
if (active) {
|
|
198
|
+
active = false;
|
|
199
|
+
lastQuery = null;
|
|
200
|
+
lastTrigger = null;
|
|
201
|
+
callbacksRef.current.onExit();
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const $pos = selection.$from;
|
|
206
|
+
const textBlock = $pos.parent;
|
|
207
|
+
if (!textBlock.isTextblock) {
|
|
208
|
+
if (active) {
|
|
209
|
+
active = false;
|
|
210
|
+
lastQuery = null;
|
|
211
|
+
lastTrigger = null;
|
|
212
|
+
callbacksRef.current.onExit();
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const blockStart = $pos.start();
|
|
217
|
+
const blockText = textBlock.textContent;
|
|
218
|
+
const cursorPos = $pos.pos;
|
|
219
|
+
const match = detectTrigger(blockText, cursorPos, blockStart, triggers);
|
|
220
|
+
if (match) {
|
|
221
|
+
const range = { from: match.from, to: match.to };
|
|
222
|
+
const props = {
|
|
223
|
+
query: match.query,
|
|
224
|
+
trigger: match.trigger,
|
|
225
|
+
clientRect: getClientRect(view, match.from),
|
|
226
|
+
range,
|
|
227
|
+
command: makeCommand(range)
|
|
228
|
+
};
|
|
229
|
+
if (!active) {
|
|
230
|
+
active = true;
|
|
231
|
+
lastQuery = match.query;
|
|
232
|
+
lastTrigger = match.trigger;
|
|
233
|
+
callbacksRef.current.onStart(props);
|
|
234
|
+
} else if (match.query !== lastQuery || match.trigger !== lastTrigger) {
|
|
235
|
+
lastQuery = match.query;
|
|
236
|
+
lastTrigger = match.trigger;
|
|
237
|
+
callbacksRef.current.onUpdate(props);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
if (active) {
|
|
241
|
+
active = false;
|
|
242
|
+
lastQuery = null;
|
|
243
|
+
lastTrigger = null;
|
|
244
|
+
callbacksRef.current.onExit();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
destroy() {
|
|
249
|
+
if (active) {
|
|
250
|
+
callbacksRef.current.onExit();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
return [plugin];
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/core/markdownSerializer.ts
|
|
262
|
+
function serializeToMarkdown(doc) {
|
|
263
|
+
if (!doc.content) return "";
|
|
264
|
+
const parts = [];
|
|
265
|
+
for (const block of doc.content) {
|
|
266
|
+
if (block.type === "paragraph") {
|
|
267
|
+
parts.push(serializeParagraph(block));
|
|
268
|
+
} else if (block.type === "text") {
|
|
269
|
+
parts.push(block.text ?? "");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return parts.join("\n");
|
|
273
|
+
}
|
|
274
|
+
function serializeParagraph(node) {
|
|
275
|
+
if (!node.content) return "";
|
|
276
|
+
return node.content.map((child) => {
|
|
277
|
+
if (child.type === "mention") {
|
|
278
|
+
const { id, label } = child.attrs ?? {};
|
|
279
|
+
return `@[${label}](${id})`;
|
|
280
|
+
}
|
|
281
|
+
return child.text ?? "";
|
|
282
|
+
}).join("");
|
|
283
|
+
}
|
|
284
|
+
function extractTokens(doc) {
|
|
285
|
+
const tokens = [];
|
|
286
|
+
function walk(node) {
|
|
287
|
+
if (node.type === "mention" && node.attrs) {
|
|
288
|
+
tokens.push({
|
|
289
|
+
id: node.attrs.id,
|
|
290
|
+
type: node.attrs.entityType,
|
|
291
|
+
label: node.attrs.label
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (node.content) {
|
|
295
|
+
for (const child of node.content) {
|
|
296
|
+
walk(child);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
walk(doc);
|
|
301
|
+
return tokens;
|
|
302
|
+
}
|
|
303
|
+
function extractPlainText(doc) {
|
|
304
|
+
if (!doc.content) return "";
|
|
305
|
+
const parts = [];
|
|
306
|
+
for (const block of doc.content) {
|
|
307
|
+
if (block.type === "paragraph") {
|
|
308
|
+
parts.push(extractParagraphText(block));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return parts.join("\n");
|
|
312
|
+
}
|
|
313
|
+
function extractParagraphText(node) {
|
|
314
|
+
if (!node.content) return "";
|
|
315
|
+
return node.content.map((child) => {
|
|
316
|
+
if (child.type === "mention") {
|
|
317
|
+
return child.attrs?.label ?? "";
|
|
318
|
+
}
|
|
319
|
+
return child.text ?? "";
|
|
320
|
+
}).join("");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/core/markdownParser.ts
|
|
324
|
+
var MENTION_RE = /@\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
|
|
325
|
+
function parseFromMarkdown(markdown) {
|
|
326
|
+
const lines = markdown.split("\n");
|
|
327
|
+
const content = lines.map((line) => ({
|
|
328
|
+
type: "paragraph",
|
|
329
|
+
content: parseLine(line)
|
|
330
|
+
}));
|
|
331
|
+
return { type: "doc", content };
|
|
332
|
+
}
|
|
333
|
+
function parseLine(line) {
|
|
334
|
+
const nodes = [];
|
|
335
|
+
let lastIndex = 0;
|
|
336
|
+
MENTION_RE.lastIndex = 0;
|
|
337
|
+
let match;
|
|
338
|
+
while ((match = MENTION_RE.exec(line)) !== null) {
|
|
339
|
+
const fullMatch = match[0];
|
|
340
|
+
const label = match[1];
|
|
341
|
+
const entityType = match[2] ?? "unknown";
|
|
342
|
+
const id = match[3];
|
|
343
|
+
if (match.index > lastIndex) {
|
|
344
|
+
nodes.push({
|
|
345
|
+
type: "text",
|
|
346
|
+
text: line.slice(lastIndex, match.index)
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
nodes.push({
|
|
350
|
+
type: "mention",
|
|
351
|
+
attrs: {
|
|
352
|
+
id,
|
|
353
|
+
label,
|
|
354
|
+
entityType
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
lastIndex = match.index + fullMatch.length;
|
|
358
|
+
}
|
|
359
|
+
if (lastIndex < line.length) {
|
|
360
|
+
nodes.push({
|
|
361
|
+
type: "text",
|
|
362
|
+
text: line.slice(lastIndex)
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (nodes.length === 0) {
|
|
366
|
+
nodes.push({ type: "text", text: "" });
|
|
367
|
+
}
|
|
368
|
+
return nodes;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/hooks/useMentionsEditor.ts
|
|
372
|
+
function createSubmitExtension(onSubmitRef) {
|
|
373
|
+
return import_core3.Extension.create({
|
|
374
|
+
name: "submitShortcut",
|
|
375
|
+
addKeyboardShortcuts() {
|
|
376
|
+
return {
|
|
377
|
+
"Mod-Enter": () => {
|
|
378
|
+
if (onSubmitRef.current) {
|
|
379
|
+
const json = this.editor.getJSON();
|
|
380
|
+
onSubmitRef.current({
|
|
381
|
+
markdown: serializeToMarkdown(json),
|
|
382
|
+
tokens: extractTokens(json),
|
|
383
|
+
plainText: extractPlainText(json)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function createEnterExtension(onSubmitRef) {
|
|
393
|
+
return import_core3.Extension.create({
|
|
394
|
+
name: "enterSubmit",
|
|
395
|
+
priority: 50,
|
|
396
|
+
addKeyboardShortcuts() {
|
|
397
|
+
return {
|
|
398
|
+
Enter: () => {
|
|
399
|
+
if (onSubmitRef.current) {
|
|
400
|
+
const json = this.editor.getJSON();
|
|
401
|
+
onSubmitRef.current({
|
|
402
|
+
markdown: serializeToMarkdown(json),
|
|
403
|
+
tokens: extractTokens(json),
|
|
404
|
+
plainText: extractPlainText(json)
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function useMentionsEditor({
|
|
414
|
+
providers,
|
|
415
|
+
value,
|
|
416
|
+
onChange,
|
|
417
|
+
onSubmit,
|
|
418
|
+
placeholder,
|
|
419
|
+
autoFocus = false,
|
|
420
|
+
editable = true,
|
|
421
|
+
callbacksRef
|
|
422
|
+
}) {
|
|
423
|
+
const onChangeRef = (0, import_react.useRef)(onChange);
|
|
424
|
+
onChangeRef.current = onChange;
|
|
425
|
+
const onSubmitRef = (0, import_react.useRef)(onSubmit);
|
|
426
|
+
onSubmitRef.current = onSubmit;
|
|
427
|
+
const initialContent = (0, import_react.useMemo)(() => {
|
|
428
|
+
if (!value) return void 0;
|
|
429
|
+
return parseFromMarkdown(value);
|
|
430
|
+
}, []);
|
|
431
|
+
const triggersKey = providers.map((p) => p.trigger).join(",");
|
|
432
|
+
const triggers = (0, import_react.useMemo)(
|
|
433
|
+
() => providers.map((p) => p.trigger),
|
|
434
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
435
|
+
[triggersKey]
|
|
436
|
+
);
|
|
437
|
+
const suggestionExtension = (0, import_react.useMemo)(
|
|
438
|
+
() => createSuggestionExtension(triggers, callbacksRef),
|
|
439
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
440
|
+
[triggersKey]
|
|
441
|
+
);
|
|
442
|
+
const submitExt = (0, import_react.useMemo)(() => createSubmitExtension(onSubmitRef), []);
|
|
443
|
+
const enterExt = (0, import_react.useMemo)(() => createEnterExtension(onSubmitRef), []);
|
|
444
|
+
const editor = (0, import_react2.useEditor)({
|
|
445
|
+
extensions: [
|
|
446
|
+
import_starter_kit.default.configure({
|
|
447
|
+
heading: false,
|
|
448
|
+
blockquote: false,
|
|
449
|
+
codeBlock: false,
|
|
450
|
+
bulletList: false,
|
|
451
|
+
orderedList: false,
|
|
452
|
+
listItem: false,
|
|
453
|
+
horizontalRule: false
|
|
454
|
+
}),
|
|
455
|
+
MentionNode,
|
|
456
|
+
suggestionExtension,
|
|
457
|
+
submitExt,
|
|
458
|
+
enterExt
|
|
459
|
+
],
|
|
460
|
+
content: initialContent,
|
|
461
|
+
autofocus: autoFocus ? "end" : false,
|
|
462
|
+
editable,
|
|
463
|
+
editorProps: {
|
|
464
|
+
attributes: {
|
|
465
|
+
"data-placeholder": placeholder ?? "",
|
|
466
|
+
class: "mentions-editor"
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
onUpdate: ({ editor: editor2 }) => {
|
|
470
|
+
const json = editor2.getJSON();
|
|
471
|
+
onChangeRef.current?.({
|
|
472
|
+
markdown: serializeToMarkdown(json),
|
|
473
|
+
tokens: extractTokens(json),
|
|
474
|
+
plainText: extractPlainText(json)
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
(0, import_react.useEffect)(() => {
|
|
479
|
+
if (editor && editor.isEditable !== editable) {
|
|
480
|
+
editor.setEditable(editable);
|
|
481
|
+
}
|
|
482
|
+
}, [editor, editable]);
|
|
483
|
+
const getOutput = (0, import_react.useCallback)(() => {
|
|
484
|
+
if (!editor) return null;
|
|
485
|
+
const json = editor.getJSON();
|
|
486
|
+
return {
|
|
487
|
+
markdown: serializeToMarkdown(json),
|
|
488
|
+
tokens: extractTokens(json),
|
|
489
|
+
plainText: extractPlainText(json)
|
|
490
|
+
};
|
|
491
|
+
}, [editor]);
|
|
492
|
+
return { editor, getOutput };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/hooks/useSuggestion.ts
|
|
496
|
+
var import_react3 = require("react");
|
|
497
|
+
var IDLE_STATE = {
|
|
498
|
+
state: "idle",
|
|
499
|
+
items: [],
|
|
500
|
+
breadcrumbs: [],
|
|
501
|
+
activeIndex: 0,
|
|
502
|
+
loading: false,
|
|
503
|
+
clientRect: null,
|
|
504
|
+
trigger: null,
|
|
505
|
+
query: ""
|
|
506
|
+
};
|
|
507
|
+
function useSuggestion(providers) {
|
|
508
|
+
const [uiState, setUIState] = (0, import_react3.useState)(IDLE_STATE);
|
|
509
|
+
const stateRef = (0, import_react3.useRef)(uiState);
|
|
510
|
+
stateRef.current = uiState;
|
|
511
|
+
const providersRef = (0, import_react3.useRef)(providers);
|
|
512
|
+
providersRef.current = providers;
|
|
513
|
+
const commandRef = (0, import_react3.useRef)(
|
|
514
|
+
null
|
|
515
|
+
);
|
|
516
|
+
const providerRef = (0, import_react3.useRef)(null);
|
|
517
|
+
const fetchItems = (0, import_react3.useCallback)(
|
|
518
|
+
async (provider, query, parent) => {
|
|
519
|
+
setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
|
|
520
|
+
try {
|
|
521
|
+
const items = parent && provider.getChildren ? await provider.getChildren(parent, query) : await provider.getRootItems(query);
|
|
522
|
+
setUIState((prev) => ({
|
|
523
|
+
...prev,
|
|
524
|
+
items,
|
|
525
|
+
loading: false,
|
|
526
|
+
state: "showing",
|
|
527
|
+
activeIndex: 0
|
|
528
|
+
}));
|
|
529
|
+
} catch {
|
|
530
|
+
setUIState((prev) => ({
|
|
531
|
+
...prev,
|
|
532
|
+
items: [],
|
|
533
|
+
loading: false,
|
|
534
|
+
state: "showing"
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
[]
|
|
539
|
+
);
|
|
540
|
+
const onStart = (0, import_react3.useCallback)(
|
|
541
|
+
(props) => {
|
|
542
|
+
const provider = providersRef.current.find(
|
|
543
|
+
(p) => p.trigger === props.trigger
|
|
544
|
+
);
|
|
545
|
+
if (!provider) return;
|
|
546
|
+
providerRef.current = provider;
|
|
547
|
+
commandRef.current = props.command;
|
|
548
|
+
setUIState({
|
|
549
|
+
state: "loading",
|
|
550
|
+
items: [],
|
|
551
|
+
breadcrumbs: [],
|
|
552
|
+
activeIndex: 0,
|
|
553
|
+
loading: true,
|
|
554
|
+
clientRect: props.clientRect,
|
|
555
|
+
trigger: props.trigger,
|
|
556
|
+
query: props.query
|
|
557
|
+
});
|
|
558
|
+
fetchItems(provider, props.query);
|
|
559
|
+
},
|
|
560
|
+
[fetchItems]
|
|
561
|
+
);
|
|
562
|
+
const onUpdate = (0, import_react3.useCallback)(
|
|
563
|
+
(props) => {
|
|
564
|
+
const provider = providerRef.current;
|
|
565
|
+
if (!provider) return;
|
|
566
|
+
commandRef.current = props.command;
|
|
567
|
+
setUIState((prev) => ({
|
|
568
|
+
...prev,
|
|
569
|
+
clientRect: props.clientRect,
|
|
570
|
+
query: props.query
|
|
571
|
+
}));
|
|
572
|
+
if (stateRef.current.breadcrumbs.length === 0) {
|
|
573
|
+
fetchItems(provider, props.query);
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
[fetchItems]
|
|
577
|
+
);
|
|
578
|
+
const onExit = (0, import_react3.useCallback)(() => {
|
|
579
|
+
providerRef.current = null;
|
|
580
|
+
commandRef.current = null;
|
|
581
|
+
setUIState(IDLE_STATE);
|
|
582
|
+
}, []);
|
|
583
|
+
const navigateUp = (0, import_react3.useCallback)(() => {
|
|
584
|
+
setUIState((prev) => ({
|
|
585
|
+
...prev,
|
|
586
|
+
activeIndex: Math.max(0, prev.activeIndex - 1)
|
|
587
|
+
}));
|
|
588
|
+
}, []);
|
|
589
|
+
const navigateDown = (0, import_react3.useCallback)(() => {
|
|
590
|
+
setUIState((prev) => ({
|
|
591
|
+
...prev,
|
|
592
|
+
activeIndex: Math.min(prev.items.length - 1, prev.activeIndex + 1)
|
|
593
|
+
}));
|
|
594
|
+
}, []);
|
|
595
|
+
const select = (0, import_react3.useCallback)(
|
|
596
|
+
(item) => {
|
|
597
|
+
const current = stateRef.current;
|
|
598
|
+
const selected = item ?? current.items[current.activeIndex];
|
|
599
|
+
if (!selected) return;
|
|
600
|
+
const provider = providerRef.current;
|
|
601
|
+
if (selected.hasChildren && provider?.getChildren) {
|
|
602
|
+
setUIState((prev) => ({
|
|
603
|
+
...prev,
|
|
604
|
+
state: "drilling",
|
|
605
|
+
breadcrumbs: [...prev.breadcrumbs, selected],
|
|
606
|
+
items: [],
|
|
607
|
+
activeIndex: 0,
|
|
608
|
+
query: ""
|
|
609
|
+
}));
|
|
610
|
+
fetchItems(provider, "", selected);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (commandRef.current) {
|
|
614
|
+
commandRef.current({
|
|
615
|
+
id: selected.id,
|
|
616
|
+
label: selected.label,
|
|
617
|
+
entityType: selected.type
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
[fetchItems]
|
|
622
|
+
);
|
|
623
|
+
const goBack = (0, import_react3.useCallback)(() => {
|
|
624
|
+
const provider = providerRef.current;
|
|
625
|
+
if (!provider) return;
|
|
626
|
+
setUIState((prev) => {
|
|
627
|
+
const newBreadcrumbs = prev.breadcrumbs.slice(0, -1);
|
|
628
|
+
const parent = newBreadcrumbs[newBreadcrumbs.length - 1];
|
|
629
|
+
if (parent) {
|
|
630
|
+
fetchItems(provider, "", parent);
|
|
631
|
+
} else {
|
|
632
|
+
fetchItems(provider, prev.query);
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
...prev,
|
|
636
|
+
breadcrumbs: newBreadcrumbs,
|
|
637
|
+
items: [],
|
|
638
|
+
activeIndex: 0,
|
|
639
|
+
state: "loading"
|
|
640
|
+
};
|
|
641
|
+
});
|
|
642
|
+
}, [fetchItems]);
|
|
643
|
+
const close = (0, import_react3.useCallback)(() => {
|
|
644
|
+
setUIState(IDLE_STATE);
|
|
645
|
+
}, []);
|
|
646
|
+
const onKeyDown = (0, import_react3.useCallback)(
|
|
647
|
+
({ event }) => {
|
|
648
|
+
const current = stateRef.current;
|
|
649
|
+
if (current.state === "idle") return false;
|
|
650
|
+
switch (event.key) {
|
|
651
|
+
case "ArrowUp":
|
|
652
|
+
event.preventDefault();
|
|
653
|
+
navigateUp();
|
|
654
|
+
return true;
|
|
655
|
+
case "ArrowDown":
|
|
656
|
+
event.preventDefault();
|
|
657
|
+
navigateDown();
|
|
658
|
+
return true;
|
|
659
|
+
case "Enter": {
|
|
660
|
+
event.preventDefault();
|
|
661
|
+
const selectedItem = current.items[current.activeIndex];
|
|
662
|
+
if (selectedItem) {
|
|
663
|
+
select(selectedItem);
|
|
664
|
+
}
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
case "ArrowRight": {
|
|
668
|
+
const activeItem = current.items[current.activeIndex];
|
|
669
|
+
if (activeItem?.hasChildren) {
|
|
670
|
+
event.preventDefault();
|
|
671
|
+
select(activeItem);
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
case "ArrowLeft":
|
|
677
|
+
if (current.breadcrumbs.length > 0) {
|
|
678
|
+
event.preventDefault();
|
|
679
|
+
goBack();
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
return false;
|
|
683
|
+
case "Escape":
|
|
684
|
+
event.preventDefault();
|
|
685
|
+
close();
|
|
686
|
+
return true;
|
|
687
|
+
default:
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
[navigateUp, navigateDown, select, goBack, close]
|
|
692
|
+
);
|
|
693
|
+
const callbacksRef = (0, import_react3.useRef)({
|
|
694
|
+
onStart,
|
|
695
|
+
onUpdate,
|
|
696
|
+
onExit,
|
|
697
|
+
onKeyDown
|
|
698
|
+
});
|
|
699
|
+
callbacksRef.current = { onStart, onUpdate, onExit, onKeyDown };
|
|
700
|
+
const actions = {
|
|
701
|
+
navigateUp,
|
|
702
|
+
navigateDown,
|
|
703
|
+
select,
|
|
704
|
+
goBack,
|
|
705
|
+
close
|
|
706
|
+
};
|
|
707
|
+
return { uiState, actions, callbacksRef };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/components/SuggestionList.tsx
|
|
711
|
+
var import_react4 = require("react");
|
|
712
|
+
|
|
713
|
+
// src/utils/ariaHelpers.ts
|
|
714
|
+
function comboboxAttrs(expanded, listboxId) {
|
|
715
|
+
return {
|
|
716
|
+
role: "combobox",
|
|
717
|
+
"aria-haspopup": "listbox",
|
|
718
|
+
"aria-expanded": expanded,
|
|
719
|
+
"aria-owns": expanded ? listboxId : void 0
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function listboxAttrs(id, label) {
|
|
723
|
+
return {
|
|
724
|
+
id,
|
|
725
|
+
role: "listbox",
|
|
726
|
+
"aria-label": label
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function optionAttrs(id, selected, index) {
|
|
730
|
+
return {
|
|
731
|
+
id,
|
|
732
|
+
role: "option",
|
|
733
|
+
"aria-selected": selected,
|
|
734
|
+
"aria-posinset": index + 1
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/components/SuggestionList.tsx
|
|
739
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
740
|
+
var LISTBOX_ID = "mentions-suggestion-listbox";
|
|
741
|
+
function SuggestionList({
|
|
742
|
+
items,
|
|
743
|
+
activeIndex,
|
|
744
|
+
breadcrumbs,
|
|
745
|
+
loading,
|
|
746
|
+
trigger,
|
|
747
|
+
clientRect,
|
|
748
|
+
onSelect,
|
|
749
|
+
onHover,
|
|
750
|
+
onGoBack,
|
|
751
|
+
renderItem
|
|
752
|
+
}) {
|
|
753
|
+
const listRef = (0, import_react4.useRef)(null);
|
|
754
|
+
const depth = breadcrumbs.length;
|
|
755
|
+
(0, import_react4.useEffect)(() => {
|
|
756
|
+
if (!listRef.current) return;
|
|
757
|
+
const active = listRef.current.querySelector('[aria-selected="true"]');
|
|
758
|
+
active?.scrollIntoView({ block: "nearest" });
|
|
759
|
+
}, [activeIndex]);
|
|
760
|
+
const style = usePopoverPosition(clientRect);
|
|
761
|
+
if (items.length === 0 && !loading) return null;
|
|
762
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
763
|
+
"div",
|
|
764
|
+
{
|
|
765
|
+
"data-suggestions": "",
|
|
766
|
+
"data-trigger": trigger,
|
|
767
|
+
style,
|
|
768
|
+
ref: listRef,
|
|
769
|
+
children: [
|
|
770
|
+
breadcrumbs.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-suggestion-breadcrumb": "", children: [
|
|
771
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
772
|
+
"button",
|
|
773
|
+
{
|
|
774
|
+
type: "button",
|
|
775
|
+
"data-suggestion-back": "",
|
|
776
|
+
onClick: onGoBack,
|
|
777
|
+
"aria-label": "Go back",
|
|
778
|
+
children: "\u2190"
|
|
779
|
+
}
|
|
780
|
+
),
|
|
781
|
+
breadcrumbs.map((crumb, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { "data-suggestion-breadcrumb-item": "", children: [
|
|
782
|
+
i > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-breadcrumb-sep": "", children: "/" }),
|
|
783
|
+
crumb.label
|
|
784
|
+
] }, crumb.id))
|
|
785
|
+
] }),
|
|
786
|
+
loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-loading": "", children: "Loading..." }),
|
|
787
|
+
!loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...listboxAttrs(LISTBOX_ID, `${trigger ?? ""} suggestions`), children: items.map((item, index) => {
|
|
788
|
+
const isActive = index === activeIndex;
|
|
789
|
+
const itemId = `mention-option-${item.id}`;
|
|
790
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
791
|
+
"div",
|
|
792
|
+
{
|
|
793
|
+
...optionAttrs(itemId, isActive, index),
|
|
794
|
+
"data-suggestion-item": "",
|
|
795
|
+
"data-suggestion-item-active": isActive ? "" : void 0,
|
|
796
|
+
"data-has-children": item.hasChildren ? "" : void 0,
|
|
797
|
+
onMouseEnter: () => onHover(index),
|
|
798
|
+
onClick: () => onSelect(item),
|
|
799
|
+
children: renderItem ? renderItem(item, depth) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultSuggestionItem, { item })
|
|
800
|
+
},
|
|
801
|
+
item.id
|
|
802
|
+
);
|
|
803
|
+
}) })
|
|
804
|
+
]
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
function DefaultSuggestionItem({ item }) {
|
|
809
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
810
|
+
item.icon && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-icon": "", children: item.icon }),
|
|
811
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-label": "", children: item.label }),
|
|
812
|
+
item.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-description": "", children: item.description }),
|
|
813
|
+
item.hasChildren && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-chevron": "", "aria-hidden": "true", children: "\u203A" })
|
|
814
|
+
] });
|
|
815
|
+
}
|
|
816
|
+
function usePopoverPosition(clientRect) {
|
|
817
|
+
if (!clientRect) {
|
|
818
|
+
return { display: "none" };
|
|
819
|
+
}
|
|
820
|
+
const rect = clientRect();
|
|
821
|
+
if (!rect) {
|
|
822
|
+
return { display: "none" };
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
position: "fixed",
|
|
826
|
+
left: `${rect.left}px`,
|
|
827
|
+
top: `${rect.bottom + 4}px`,
|
|
828
|
+
zIndex: 50
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/components/MentionsInput.tsx
|
|
833
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
834
|
+
var LISTBOX_ID2 = "mentions-suggestion-listbox";
|
|
835
|
+
function MentionsInput({
|
|
836
|
+
value,
|
|
837
|
+
providers,
|
|
838
|
+
onChange,
|
|
839
|
+
placeholder = "Type a message...",
|
|
840
|
+
autoFocus = false,
|
|
841
|
+
disabled = false,
|
|
842
|
+
className,
|
|
843
|
+
onSubmit,
|
|
844
|
+
maxLength,
|
|
845
|
+
renderItem,
|
|
846
|
+
renderChip
|
|
847
|
+
}) {
|
|
848
|
+
const { uiState, actions, callbacksRef } = useSuggestion(providers);
|
|
849
|
+
const { editor } = useMentionsEditor({
|
|
850
|
+
providers,
|
|
851
|
+
value,
|
|
852
|
+
onChange,
|
|
853
|
+
onSubmit,
|
|
854
|
+
placeholder,
|
|
855
|
+
autoFocus,
|
|
856
|
+
editable: !disabled,
|
|
857
|
+
callbacksRef
|
|
858
|
+
});
|
|
859
|
+
const isExpanded = uiState.state !== "idle";
|
|
860
|
+
const handleHover = (0, import_react5.useCallback)((index) => {
|
|
861
|
+
}, []);
|
|
862
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
863
|
+
"div",
|
|
864
|
+
{
|
|
865
|
+
className,
|
|
866
|
+
"data-mentions-input": "",
|
|
867
|
+
"data-disabled": disabled ? "" : void 0,
|
|
868
|
+
...comboboxAttrs(isExpanded, LISTBOX_ID2),
|
|
869
|
+
"aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
|
|
870
|
+
children: [
|
|
871
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react6.EditorContent, { editor }),
|
|
872
|
+
isExpanded && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
873
|
+
SuggestionList,
|
|
874
|
+
{
|
|
875
|
+
items: uiState.items,
|
|
876
|
+
activeIndex: uiState.activeIndex,
|
|
877
|
+
breadcrumbs: uiState.breadcrumbs,
|
|
878
|
+
loading: uiState.loading,
|
|
879
|
+
trigger: uiState.trigger,
|
|
880
|
+
clientRect: uiState.clientRect,
|
|
881
|
+
onSelect: (item) => actions.select(item),
|
|
882
|
+
onHover: handleHover,
|
|
883
|
+
onGoBack: actions.goBack,
|
|
884
|
+
renderItem
|
|
885
|
+
}
|
|
886
|
+
)
|
|
887
|
+
]
|
|
888
|
+
}
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
892
|
+
0 && (module.exports = {
|
|
893
|
+
MentionsInput,
|
|
894
|
+
parseFromMarkdown,
|
|
895
|
+
serializeToMarkdown
|
|
896
|
+
});
|
|
897
|
+
//# sourceMappingURL=index.js.map
|