@relevaince/mentions 0.3.3 → 0.6.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 +160 -73
- package/dist/index.d.mts +55 -9
- package/dist/index.d.ts +55 -9
- package/dist/index.js +644 -86
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +650 -92
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/components/MentionsInput.tsx
|
|
2
|
-
import { forwardRef, useCallback as useCallback3, useImperativeHandle } from "react";
|
|
2
|
+
import { forwardRef, useCallback as useCallback3, useId, useImperativeHandle } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
3
4
|
import { EditorContent } from "@tiptap/react";
|
|
4
5
|
|
|
5
6
|
// src/hooks/useMentionsEditor.ts
|
|
@@ -25,6 +26,12 @@ var MentionNode = Node.create({
|
|
|
25
26
|
atom: true,
|
|
26
27
|
selectable: true,
|
|
27
28
|
draggable: false,
|
|
29
|
+
addOptions() {
|
|
30
|
+
return {
|
|
31
|
+
onClickRef: void 0,
|
|
32
|
+
onHoverRef: void 0
|
|
33
|
+
};
|
|
34
|
+
},
|
|
28
35
|
addAttributes() {
|
|
29
36
|
return {
|
|
30
37
|
id: {
|
|
@@ -57,11 +64,17 @@ var MentionNode = Node.create({
|
|
|
57
64
|
const label = node.attrs.label;
|
|
58
65
|
const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
|
|
59
66
|
const display = `${prefix}${label}`;
|
|
67
|
+
const hasClick = !!this.options.onClickRef?.current;
|
|
68
|
+
const extraAttrs = {};
|
|
69
|
+
if (hasClick) {
|
|
70
|
+
extraAttrs["data-mention-clickable"] = "";
|
|
71
|
+
}
|
|
60
72
|
return [
|
|
61
73
|
"span",
|
|
62
74
|
mergeAttributes(HTMLAttributes, {
|
|
63
75
|
"data-mention": "",
|
|
64
|
-
class: "mention-chip"
|
|
76
|
+
class: "mention-chip",
|
|
77
|
+
...extraAttrs
|
|
65
78
|
}),
|
|
66
79
|
display
|
|
67
80
|
];
|
|
@@ -72,6 +85,58 @@ var MentionNode = Node.create({
|
|
|
72
85
|
const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
|
|
73
86
|
return `${prefix}${label}`;
|
|
74
87
|
},
|
|
88
|
+
addNodeView() {
|
|
89
|
+
const options = this.options;
|
|
90
|
+
return ({ node, HTMLAttributes }) => {
|
|
91
|
+
const entityType = node.attrs.entityType;
|
|
92
|
+
const label = node.attrs.label;
|
|
93
|
+
const id = node.attrs.id;
|
|
94
|
+
const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
|
|
95
|
+
const dom = document.createElement("span");
|
|
96
|
+
Object.entries(
|
|
97
|
+
mergeAttributes(HTMLAttributes, {
|
|
98
|
+
"data-mention": "",
|
|
99
|
+
"data-type": entityType,
|
|
100
|
+
"data-id": id,
|
|
101
|
+
class: "mention-chip"
|
|
102
|
+
})
|
|
103
|
+
).forEach(([key, val]) => {
|
|
104
|
+
if (val != null && val !== false) dom.setAttribute(key, String(val));
|
|
105
|
+
});
|
|
106
|
+
dom.textContent = `${prefix}${label}`;
|
|
107
|
+
if (options.onClickRef?.current) {
|
|
108
|
+
dom.setAttribute("data-mention-clickable", "");
|
|
109
|
+
dom.style.cursor = "pointer";
|
|
110
|
+
}
|
|
111
|
+
dom.addEventListener("click", (event) => {
|
|
112
|
+
const handler = options.onClickRef?.current;
|
|
113
|
+
if (handler) {
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
event.stopPropagation();
|
|
116
|
+
handler({ id, type: entityType, label }, event);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
let tooltip = null;
|
|
120
|
+
dom.addEventListener("mouseenter", () => {
|
|
121
|
+
const hoverFn = options.onHoverRef?.current;
|
|
122
|
+
if (!hoverFn) return;
|
|
123
|
+
const content = hoverFn({ id, type: entityType, label });
|
|
124
|
+
if (!content) return;
|
|
125
|
+
tooltip = document.createElement("div");
|
|
126
|
+
tooltip.setAttribute("data-mention-tooltip", "");
|
|
127
|
+
tooltip.textContent = typeof content === "string" ? content : "";
|
|
128
|
+
dom.style.position = "relative";
|
|
129
|
+
dom.appendChild(tooltip);
|
|
130
|
+
});
|
|
131
|
+
dom.addEventListener("mouseleave", () => {
|
|
132
|
+
if (tooltip && tooltip.parentNode) {
|
|
133
|
+
tooltip.parentNode.removeChild(tooltip);
|
|
134
|
+
tooltip = null;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return { dom };
|
|
138
|
+
};
|
|
139
|
+
},
|
|
75
140
|
addKeyboardShortcuts() {
|
|
76
141
|
return {
|
|
77
142
|
Backspace: () => this.editor.commands.command(({ tr, state }) => {
|
|
@@ -104,7 +169,13 @@ function detectTrigger(text, cursorPos, docStartPos, triggers) {
|
|
|
104
169
|
if (before.substring(i, i + trigger.length) === trigger) {
|
|
105
170
|
if (i === 0 || /\s/.test(before[i - 1])) {
|
|
106
171
|
const query = before.slice(i + trigger.length);
|
|
107
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
trigger,
|
|
174
|
+
query,
|
|
175
|
+
from: docStartPos + i,
|
|
176
|
+
to: cursorPos,
|
|
177
|
+
textBefore: before.slice(0, i)
|
|
178
|
+
};
|
|
108
179
|
}
|
|
109
180
|
}
|
|
110
181
|
}
|
|
@@ -112,7 +183,7 @@ function detectTrigger(text, cursorPos, docStartPos, triggers) {
|
|
|
112
183
|
return null;
|
|
113
184
|
}
|
|
114
185
|
var suggestionPluginKey = new PluginKey("mentionSuggestion");
|
|
115
|
-
function createSuggestionExtension(triggers, callbacksRef) {
|
|
186
|
+
function createSuggestionExtension(triggers, callbacksRef, allowTriggerRef, streamingRef) {
|
|
116
187
|
return Extension.create({
|
|
117
188
|
name: "mentionSuggestion",
|
|
118
189
|
priority: 200,
|
|
@@ -163,6 +234,15 @@ function createSuggestionExtension(triggers, callbacksRef) {
|
|
|
163
234
|
view() {
|
|
164
235
|
return {
|
|
165
236
|
update(view, _prevState) {
|
|
237
|
+
if (streamingRef?.current) {
|
|
238
|
+
if (active) {
|
|
239
|
+
active = false;
|
|
240
|
+
lastQuery = null;
|
|
241
|
+
lastTrigger = null;
|
|
242
|
+
callbacksRef.current.onExit();
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
166
246
|
const { state } = view;
|
|
167
247
|
const { selection } = state;
|
|
168
248
|
if (!selection.empty) {
|
|
@@ -190,6 +270,20 @@ function createSuggestionExtension(triggers, callbacksRef) {
|
|
|
190
270
|
const cursorPos = $pos.pos;
|
|
191
271
|
const match = detectTrigger(blockText, cursorPos, blockStart, triggers);
|
|
192
272
|
if (match) {
|
|
273
|
+
if (allowTriggerRef?.current) {
|
|
274
|
+
const allowed = allowTriggerRef.current(match.trigger, {
|
|
275
|
+
textBefore: match.textBefore
|
|
276
|
+
});
|
|
277
|
+
if (!allowed) {
|
|
278
|
+
if (active) {
|
|
279
|
+
active = false;
|
|
280
|
+
lastQuery = null;
|
|
281
|
+
lastTrigger = null;
|
|
282
|
+
callbacksRef.current.onExit();
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
193
287
|
const range = { from: match.from, to: match.to };
|
|
194
288
|
const props = {
|
|
195
289
|
query: match.query,
|
|
@@ -359,27 +453,48 @@ function buildOutput(editor) {
|
|
|
359
453
|
plainText: extractPlainText(json)
|
|
360
454
|
};
|
|
361
455
|
}
|
|
362
|
-
function
|
|
456
|
+
function collectMentionTokens(doc) {
|
|
457
|
+
const tokens = [];
|
|
458
|
+
function walk(node) {
|
|
459
|
+
if (node.type === "mention" && node.attrs) {
|
|
460
|
+
tokens.push({
|
|
461
|
+
id: node.attrs.id,
|
|
462
|
+
type: node.attrs.entityType ?? node.attrs.type,
|
|
463
|
+
label: node.attrs.label
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
if (node.content) {
|
|
467
|
+
for (const child of node.content) walk(child);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
walk(doc);
|
|
471
|
+
return tokens;
|
|
472
|
+
}
|
|
473
|
+
function createSubmitExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef) {
|
|
363
474
|
return Extension2.create({
|
|
364
475
|
name: "submitShortcut",
|
|
365
476
|
priority: 150,
|
|
366
477
|
addKeyboardShortcuts() {
|
|
367
478
|
return {
|
|
368
479
|
"Mod-Enter": () => {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
if (
|
|
372
|
-
this.editor
|
|
480
|
+
const key = submitKeyRef.current;
|
|
481
|
+
if (key === "mod+enter" || key === "enter") {
|
|
482
|
+
if (onSubmitRef.current) {
|
|
483
|
+
onSubmitRef.current(buildOutput(this.editor));
|
|
484
|
+
if (clearOnSubmitRef.current) {
|
|
485
|
+
this.editor.commands.clearContent(true);
|
|
486
|
+
}
|
|
373
487
|
}
|
|
488
|
+
return true;
|
|
374
489
|
}
|
|
375
|
-
return
|
|
490
|
+
return false;
|
|
376
491
|
}
|
|
377
492
|
};
|
|
378
493
|
}
|
|
379
494
|
});
|
|
380
495
|
}
|
|
381
496
|
var enterSubmitPluginKey = new PluginKey2("enterSubmit");
|
|
382
|
-
function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
|
|
497
|
+
function createEnterExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef) {
|
|
383
498
|
return Extension2.create({
|
|
384
499
|
name: "enterSubmit",
|
|
385
500
|
priority: 150,
|
|
@@ -391,6 +506,11 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
|
|
|
391
506
|
props: {
|
|
392
507
|
handleKeyDown(_view, event) {
|
|
393
508
|
if (event.key !== "Enter") return false;
|
|
509
|
+
const key = submitKeyRef.current;
|
|
510
|
+
if (key === "none") return false;
|
|
511
|
+
if (key === "mod+enter") {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
394
514
|
if (event.shiftKey) {
|
|
395
515
|
editor.commands.splitBlock();
|
|
396
516
|
return true;
|
|
@@ -410,6 +530,61 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
|
|
|
410
530
|
}
|
|
411
531
|
});
|
|
412
532
|
}
|
|
533
|
+
var mentionRemovePluginKey = new PluginKey2("mentionRemove");
|
|
534
|
+
function createMentionRemoveExtension(onMentionRemoveRef) {
|
|
535
|
+
return Extension2.create({
|
|
536
|
+
name: "mentionRemoveDetector",
|
|
537
|
+
priority: 100,
|
|
538
|
+
addProseMirrorPlugins() {
|
|
539
|
+
return [
|
|
540
|
+
new Plugin2({
|
|
541
|
+
key: mentionRemovePluginKey,
|
|
542
|
+
appendTransaction(transactions, oldState, newState) {
|
|
543
|
+
if (!onMentionRemoveRef.current) return null;
|
|
544
|
+
const oldMentions = collectMentionTokens(oldState.doc.toJSON());
|
|
545
|
+
const newMentions = collectMentionTokens(newState.doc.toJSON());
|
|
546
|
+
if (oldMentions.length <= newMentions.length) return null;
|
|
547
|
+
const newIds = new Set(newMentions.map((m) => m.id));
|
|
548
|
+
for (const m of oldMentions) {
|
|
549
|
+
if (!newIds.has(m.id)) {
|
|
550
|
+
onMentionRemoveRef.current(m);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
})
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
var streamingBlockPluginKey = new PluginKey2("streamingBlock");
|
|
561
|
+
function createStreamingBlockExtension(streamingRef) {
|
|
562
|
+
return Extension2.create({
|
|
563
|
+
name: "streamingBlock",
|
|
564
|
+
priority: 200,
|
|
565
|
+
addProseMirrorPlugins() {
|
|
566
|
+
return [
|
|
567
|
+
new Plugin2({
|
|
568
|
+
key: streamingBlockPluginKey,
|
|
569
|
+
props: {
|
|
570
|
+
handleKeyDown() {
|
|
571
|
+
return streamingRef.current;
|
|
572
|
+
},
|
|
573
|
+
handleKeyPress() {
|
|
574
|
+
return streamingRef.current;
|
|
575
|
+
},
|
|
576
|
+
handlePaste() {
|
|
577
|
+
return streamingRef.current;
|
|
578
|
+
},
|
|
579
|
+
handleDrop() {
|
|
580
|
+
return streamingRef.current;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
];
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
413
588
|
function useMentionsEditor({
|
|
414
589
|
providers,
|
|
415
590
|
value,
|
|
@@ -419,7 +594,17 @@ function useMentionsEditor({
|
|
|
419
594
|
placeholder,
|
|
420
595
|
autoFocus = false,
|
|
421
596
|
editable = true,
|
|
422
|
-
callbacksRef
|
|
597
|
+
callbacksRef,
|
|
598
|
+
onFocus,
|
|
599
|
+
onBlur,
|
|
600
|
+
submitKey = "enter",
|
|
601
|
+
onMentionRemove,
|
|
602
|
+
onMentionClick,
|
|
603
|
+
onMentionHover,
|
|
604
|
+
allowTrigger,
|
|
605
|
+
validateMention,
|
|
606
|
+
streaming = false,
|
|
607
|
+
onStreamingComplete
|
|
423
608
|
}) {
|
|
424
609
|
const onChangeRef = useRef(onChange);
|
|
425
610
|
onChangeRef.current = onChange;
|
|
@@ -427,6 +612,30 @@ function useMentionsEditor({
|
|
|
427
612
|
onSubmitRef.current = onSubmit;
|
|
428
613
|
const clearOnSubmitRef = useRef(clearOnSubmit);
|
|
429
614
|
clearOnSubmitRef.current = clearOnSubmit;
|
|
615
|
+
const onFocusRef = useRef(onFocus);
|
|
616
|
+
onFocusRef.current = onFocus;
|
|
617
|
+
const onBlurRef = useRef(onBlur);
|
|
618
|
+
onBlurRef.current = onBlur;
|
|
619
|
+
const submitKeyRef = useRef(submitKey);
|
|
620
|
+
submitKeyRef.current = submitKey;
|
|
621
|
+
const onMentionRemoveRef = useRef(onMentionRemove);
|
|
622
|
+
onMentionRemoveRef.current = onMentionRemove;
|
|
623
|
+
const onMentionClickRef = useRef(onMentionClick);
|
|
624
|
+
onMentionClickRef.current = onMentionClick;
|
|
625
|
+
const onMentionHoverRef = useRef(onMentionHover);
|
|
626
|
+
onMentionHoverRef.current = onMentionHover;
|
|
627
|
+
const allowTriggerRef = useRef(allowTrigger);
|
|
628
|
+
allowTriggerRef.current = allowTrigger;
|
|
629
|
+
const validateMentionRef = useRef(validateMention);
|
|
630
|
+
validateMentionRef.current = validateMention;
|
|
631
|
+
const onStreamingCompleteRef = useRef(onStreamingComplete);
|
|
632
|
+
onStreamingCompleteRef.current = onStreamingComplete;
|
|
633
|
+
const streamingRef = useRef(streaming);
|
|
634
|
+
streamingRef.current = streaming;
|
|
635
|
+
const prevStreamingRef = useRef(streaming);
|
|
636
|
+
const throttleTimerRef = useRef(null);
|
|
637
|
+
const pendingOutputRef = useRef(null);
|
|
638
|
+
const internalMarkdownRef = useRef(null);
|
|
430
639
|
const initialContent = useMemo(() => {
|
|
431
640
|
if (!value) return void 0;
|
|
432
641
|
return parseFromMarkdown(value);
|
|
@@ -438,16 +647,36 @@ function useMentionsEditor({
|
|
|
438
647
|
[triggersKey]
|
|
439
648
|
);
|
|
440
649
|
const suggestionExtension = useMemo(
|
|
441
|
-
() => createSuggestionExtension(
|
|
650
|
+
() => createSuggestionExtension(
|
|
651
|
+
triggers,
|
|
652
|
+
callbacksRef,
|
|
653
|
+
allowTriggerRef,
|
|
654
|
+
streamingRef
|
|
655
|
+
),
|
|
442
656
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
443
657
|
[triggersKey]
|
|
444
658
|
);
|
|
445
659
|
const submitExt = useMemo(
|
|
446
|
-
() => createSubmitExtension(onSubmitRef, clearOnSubmitRef),
|
|
660
|
+
() => createSubmitExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef),
|
|
447
661
|
[]
|
|
448
662
|
);
|
|
449
663
|
const enterExt = useMemo(
|
|
450
|
-
() => createEnterExtension(onSubmitRef, clearOnSubmitRef),
|
|
664
|
+
() => createEnterExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef),
|
|
665
|
+
[]
|
|
666
|
+
);
|
|
667
|
+
const mentionRemoveExt = useMemo(
|
|
668
|
+
() => createMentionRemoveExtension(onMentionRemoveRef),
|
|
669
|
+
[]
|
|
670
|
+
);
|
|
671
|
+
const streamingBlockExt = useMemo(
|
|
672
|
+
() => createStreamingBlockExtension(streamingRef),
|
|
673
|
+
[]
|
|
674
|
+
);
|
|
675
|
+
const mentionNodeExt = useMemo(
|
|
676
|
+
() => MentionNode.configure({
|
|
677
|
+
onClickRef: onMentionClickRef,
|
|
678
|
+
onHoverRef: onMentionHoverRef
|
|
679
|
+
}),
|
|
451
680
|
[]
|
|
452
681
|
);
|
|
453
682
|
const editor = useEditor({
|
|
@@ -466,10 +695,12 @@ function useMentionsEditor({
|
|
|
466
695
|
placeholder: ({ editor: editor2 }) => editor2.isEmpty ? placeholder ?? "Type a message..." : "",
|
|
467
696
|
showOnlyCurrent: true
|
|
468
697
|
}),
|
|
469
|
-
|
|
698
|
+
mentionNodeExt,
|
|
470
699
|
suggestionExtension,
|
|
471
700
|
submitExt,
|
|
472
|
-
enterExt
|
|
701
|
+
enterExt,
|
|
702
|
+
mentionRemoveExt,
|
|
703
|
+
streamingBlockExt
|
|
473
704
|
],
|
|
474
705
|
content: initialContent,
|
|
475
706
|
autofocus: autoFocus ? "end" : false,
|
|
@@ -480,14 +711,85 @@ function useMentionsEditor({
|
|
|
480
711
|
}
|
|
481
712
|
},
|
|
482
713
|
onUpdate: ({ editor: editor2 }) => {
|
|
483
|
-
|
|
714
|
+
const output = buildOutput(editor2);
|
|
715
|
+
internalMarkdownRef.current = output.markdown;
|
|
716
|
+
if (streamingRef.current) {
|
|
717
|
+
pendingOutputRef.current = output;
|
|
718
|
+
if (!throttleTimerRef.current) {
|
|
719
|
+
throttleTimerRef.current = setTimeout(() => {
|
|
720
|
+
throttleTimerRef.current = null;
|
|
721
|
+
if (pendingOutputRef.current) {
|
|
722
|
+
onChangeRef.current?.(pendingOutputRef.current);
|
|
723
|
+
pendingOutputRef.current = null;
|
|
724
|
+
}
|
|
725
|
+
}, 150);
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
onChangeRef.current?.(output);
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
onFocus: () => {
|
|
732
|
+
onFocusRef.current?.();
|
|
733
|
+
},
|
|
734
|
+
onBlur: () => {
|
|
735
|
+
onBlurRef.current?.();
|
|
484
736
|
}
|
|
485
737
|
});
|
|
738
|
+
useEffect(() => {
|
|
739
|
+
if (prevStreamingRef.current && !streaming && editor) {
|
|
740
|
+
if (throttleTimerRef.current) {
|
|
741
|
+
clearTimeout(throttleTimerRef.current);
|
|
742
|
+
throttleTimerRef.current = null;
|
|
743
|
+
}
|
|
744
|
+
const output = buildOutput(editor);
|
|
745
|
+
onChangeRef.current?.(output);
|
|
746
|
+
onStreamingCompleteRef.current?.(output);
|
|
747
|
+
pendingOutputRef.current = null;
|
|
748
|
+
}
|
|
749
|
+
prevStreamingRef.current = streaming;
|
|
750
|
+
}, [streaming, editor]);
|
|
751
|
+
useEffect(() => {
|
|
752
|
+
return () => {
|
|
753
|
+
if (throttleTimerRef.current) {
|
|
754
|
+
clearTimeout(throttleTimerRef.current);
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}, []);
|
|
486
758
|
useEffect(() => {
|
|
487
759
|
if (editor && editor.isEditable !== editable) {
|
|
488
760
|
editor.setEditable(editable);
|
|
489
761
|
}
|
|
490
762
|
}, [editor, editable]);
|
|
763
|
+
useEffect(() => {
|
|
764
|
+
if (!editor || value === void 0) return;
|
|
765
|
+
if (value === internalMarkdownRef.current) return;
|
|
766
|
+
const doc = parseFromMarkdown(value);
|
|
767
|
+
editor.commands.setContent(doc);
|
|
768
|
+
internalMarkdownRef.current = value;
|
|
769
|
+
}, [editor, value]);
|
|
770
|
+
useEffect(() => {
|
|
771
|
+
if (!editor || !validateMention) return;
|
|
772
|
+
const runValidation = async () => {
|
|
773
|
+
const doc = editor.getJSON();
|
|
774
|
+
const tokens = collectMentionTokens(doc);
|
|
775
|
+
const invalidIds = /* @__PURE__ */ new Set();
|
|
776
|
+
await Promise.all(
|
|
777
|
+
tokens.map(async (token) => {
|
|
778
|
+
const valid = await validateMention(token);
|
|
779
|
+
if (!valid) invalidIds.add(token.id);
|
|
780
|
+
})
|
|
781
|
+
);
|
|
782
|
+
editor.view.dom.querySelectorAll("[data-mention]").forEach((el) => {
|
|
783
|
+
const id = el.getAttribute("data-id");
|
|
784
|
+
if (id && invalidIds.has(id)) {
|
|
785
|
+
el.setAttribute("data-mention-invalid", "");
|
|
786
|
+
} else {
|
|
787
|
+
el.removeAttribute("data-mention-invalid");
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
};
|
|
791
|
+
runValidation();
|
|
792
|
+
}, [editor, validateMention]);
|
|
491
793
|
const clear = useCallback(() => {
|
|
492
794
|
editor?.commands.clearContent(true);
|
|
493
795
|
}, [editor]);
|
|
@@ -496,6 +798,21 @@ function useMentionsEditor({
|
|
|
496
798
|
if (!editor) return;
|
|
497
799
|
const doc = parseFromMarkdown(markdown);
|
|
498
800
|
editor.commands.setContent(doc);
|
|
801
|
+
internalMarkdownRef.current = markdown;
|
|
802
|
+
if (streamingRef.current) {
|
|
803
|
+
editor.commands.focus("end");
|
|
804
|
+
}
|
|
805
|
+
},
|
|
806
|
+
[editor]
|
|
807
|
+
);
|
|
808
|
+
const appendText = useCallback(
|
|
809
|
+
(text) => {
|
|
810
|
+
if (!editor) return;
|
|
811
|
+
const endPos = editor.state.doc.content.size - 1;
|
|
812
|
+
editor.commands.insertContentAt(endPos, text);
|
|
813
|
+
if (streamingRef.current) {
|
|
814
|
+
editor.commands.focus("end");
|
|
815
|
+
}
|
|
499
816
|
},
|
|
500
817
|
[editor]
|
|
501
818
|
);
|
|
@@ -506,11 +823,32 @@ function useMentionsEditor({
|
|
|
506
823
|
if (!editor) return null;
|
|
507
824
|
return buildOutput(editor);
|
|
508
825
|
}, [editor]);
|
|
509
|
-
return { editor, getOutput, clear, setContent, focus };
|
|
826
|
+
return { editor, getOutput, clear, setContent, appendText, focus };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/hooks/useSuggestion.ts
|
|
830
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
831
|
+
|
|
832
|
+
// src/utils/debounce.ts
|
|
833
|
+
function debounce(fn, ms) {
|
|
834
|
+
let timer = null;
|
|
835
|
+
const debounced = ((...args) => {
|
|
836
|
+
if (timer != null) clearTimeout(timer);
|
|
837
|
+
timer = setTimeout(() => {
|
|
838
|
+
timer = null;
|
|
839
|
+
fn(...args);
|
|
840
|
+
}, ms);
|
|
841
|
+
});
|
|
842
|
+
debounced.cancel = () => {
|
|
843
|
+
if (timer != null) {
|
|
844
|
+
clearTimeout(timer);
|
|
845
|
+
timer = null;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
return debounced;
|
|
510
849
|
}
|
|
511
850
|
|
|
512
851
|
// src/hooks/useSuggestion.ts
|
|
513
|
-
import { useCallback as useCallback2, useRef as useRef2, useState } from "react";
|
|
514
852
|
var IDLE_STATE = {
|
|
515
853
|
state: "idle",
|
|
516
854
|
items: [],
|
|
@@ -521,16 +859,24 @@ var IDLE_STATE = {
|
|
|
521
859
|
trigger: null,
|
|
522
860
|
query: ""
|
|
523
861
|
};
|
|
524
|
-
function useSuggestion(providers) {
|
|
862
|
+
function useSuggestion(providers, options = {}) {
|
|
525
863
|
const [uiState, setUIState] = useState(IDLE_STATE);
|
|
526
864
|
const stateRef = useRef2(uiState);
|
|
527
865
|
stateRef.current = uiState;
|
|
528
866
|
const providersRef = useRef2(providers);
|
|
529
867
|
providersRef.current = providers;
|
|
868
|
+
const onMentionAddRef = useRef2(options.onMentionAdd);
|
|
869
|
+
onMentionAddRef.current = options.onMentionAdd;
|
|
530
870
|
const commandRef = useRef2(
|
|
531
871
|
null
|
|
532
872
|
);
|
|
533
873
|
const providerRef = useRef2(null);
|
|
874
|
+
const debouncedFetchRef = useRef2(null);
|
|
875
|
+
useEffect2(() => {
|
|
876
|
+
return () => {
|
|
877
|
+
debouncedFetchRef.current?.cancel();
|
|
878
|
+
};
|
|
879
|
+
}, []);
|
|
534
880
|
const fetchItems = useCallback2(
|
|
535
881
|
async (provider, query, parent, useSearchAll) => {
|
|
536
882
|
setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
|
|
@@ -561,6 +907,23 @@ function useSuggestion(providers) {
|
|
|
561
907
|
},
|
|
562
908
|
[]
|
|
563
909
|
);
|
|
910
|
+
const scheduleFetch = useCallback2(
|
|
911
|
+
(provider, query, parent, useSearchAll) => {
|
|
912
|
+
debouncedFetchRef.current?.cancel();
|
|
913
|
+
const ms = provider.debounceMs;
|
|
914
|
+
if (ms && ms > 0) {
|
|
915
|
+
setUIState((prev) => ({ ...prev, loading: true }));
|
|
916
|
+
const debouncedFn = debounce(() => {
|
|
917
|
+
fetchItems(provider, query, parent, useSearchAll);
|
|
918
|
+
}, ms);
|
|
919
|
+
debouncedFetchRef.current = debouncedFn;
|
|
920
|
+
debouncedFn();
|
|
921
|
+
} else {
|
|
922
|
+
fetchItems(provider, query, parent, useSearchAll);
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
[fetchItems]
|
|
926
|
+
);
|
|
564
927
|
const onStart = useCallback2(
|
|
565
928
|
(props) => {
|
|
566
929
|
const provider = providersRef.current.find(
|
|
@@ -579,13 +942,31 @@ function useSuggestion(providers) {
|
|
|
579
942
|
trigger: props.trigger,
|
|
580
943
|
query: props.query
|
|
581
944
|
});
|
|
945
|
+
if (!props.query.trim() && provider.getRecentItems) {
|
|
946
|
+
provider.getRecentItems().then((recentItems) => {
|
|
947
|
+
const tagged = recentItems.map((item) => ({
|
|
948
|
+
...item,
|
|
949
|
+
group: item.group ?? "Recent"
|
|
950
|
+
}));
|
|
951
|
+
setUIState((prev) => ({
|
|
952
|
+
...prev,
|
|
953
|
+
items: tagged,
|
|
954
|
+
loading: false,
|
|
955
|
+
state: "showing",
|
|
956
|
+
activeIndex: 0
|
|
957
|
+
}));
|
|
958
|
+
}).catch(() => {
|
|
959
|
+
scheduleFetch(provider, props.query);
|
|
960
|
+
});
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
582
963
|
if (props.query.trim() && provider.searchAll) {
|
|
583
|
-
|
|
964
|
+
scheduleFetch(provider, props.query, void 0, true);
|
|
584
965
|
} else {
|
|
585
|
-
|
|
966
|
+
scheduleFetch(provider, props.query);
|
|
586
967
|
}
|
|
587
968
|
},
|
|
588
|
-
[
|
|
969
|
+
[scheduleFetch]
|
|
589
970
|
);
|
|
590
971
|
const onUpdate = useCallback2(
|
|
591
972
|
(props) => {
|
|
@@ -609,14 +990,31 @@ function useSuggestion(providers) {
|
|
|
609
990
|
}));
|
|
610
991
|
}
|
|
611
992
|
if (props.query.trim() && provider.searchAll) {
|
|
612
|
-
|
|
993
|
+
scheduleFetch(provider, props.query, void 0, true);
|
|
994
|
+
} else if (!props.query.trim() && provider.getRecentItems) {
|
|
995
|
+
provider.getRecentItems().then((recentItems) => {
|
|
996
|
+
const tagged = recentItems.map((item) => ({
|
|
997
|
+
...item,
|
|
998
|
+
group: item.group ?? "Recent"
|
|
999
|
+
}));
|
|
1000
|
+
setUIState((prev) => ({
|
|
1001
|
+
...prev,
|
|
1002
|
+
items: tagged,
|
|
1003
|
+
loading: false,
|
|
1004
|
+
state: "showing",
|
|
1005
|
+
activeIndex: 0
|
|
1006
|
+
}));
|
|
1007
|
+
}).catch(() => {
|
|
1008
|
+
scheduleFetch(provider, props.query);
|
|
1009
|
+
});
|
|
613
1010
|
} else {
|
|
614
|
-
|
|
1011
|
+
scheduleFetch(provider, props.query);
|
|
615
1012
|
}
|
|
616
1013
|
},
|
|
617
|
-
[
|
|
1014
|
+
[scheduleFetch]
|
|
618
1015
|
);
|
|
619
1016
|
const onExit = useCallback2(() => {
|
|
1017
|
+
debouncedFetchRef.current?.cancel();
|
|
620
1018
|
providerRef.current = null;
|
|
621
1019
|
commandRef.current = null;
|
|
622
1020
|
setUIState(IDLE_STATE);
|
|
@@ -651,8 +1049,8 @@ function useSuggestion(providers) {
|
|
|
651
1049
|
fetchItems(provider, "", selected);
|
|
652
1050
|
return;
|
|
653
1051
|
}
|
|
1052
|
+
const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
|
|
654
1053
|
if (commandRef.current) {
|
|
655
|
-
const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
|
|
656
1054
|
commandRef.current({
|
|
657
1055
|
id: selected.id,
|
|
658
1056
|
label: selected.label,
|
|
@@ -660,6 +1058,11 @@ function useSuggestion(providers) {
|
|
|
660
1058
|
rootLabel
|
|
661
1059
|
});
|
|
662
1060
|
}
|
|
1061
|
+
onMentionAddRef.current?.({
|
|
1062
|
+
id: selected.id,
|
|
1063
|
+
type: selected.type,
|
|
1064
|
+
label: selected.label
|
|
1065
|
+
});
|
|
663
1066
|
},
|
|
664
1067
|
[fetchItems]
|
|
665
1068
|
);
|
|
@@ -684,6 +1087,7 @@ function useSuggestion(providers) {
|
|
|
684
1087
|
});
|
|
685
1088
|
}, [fetchItems]);
|
|
686
1089
|
const close = useCallback2(() => {
|
|
1090
|
+
debouncedFetchRef.current?.cancel();
|
|
687
1091
|
setUIState(IDLE_STATE);
|
|
688
1092
|
}, []);
|
|
689
1093
|
const searchNested = useCallback2(
|
|
@@ -693,10 +1097,10 @@ function useSuggestion(providers) {
|
|
|
693
1097
|
const current = stateRef.current;
|
|
694
1098
|
const parent = current.breadcrumbs[current.breadcrumbs.length - 1];
|
|
695
1099
|
if (parent) {
|
|
696
|
-
|
|
1100
|
+
scheduleFetch(provider, query, parent);
|
|
697
1101
|
}
|
|
698
1102
|
},
|
|
699
|
-
[
|
|
1103
|
+
[scheduleFetch]
|
|
700
1104
|
);
|
|
701
1105
|
const onKeyDown = useCallback2(
|
|
702
1106
|
({ event }) => {
|
|
@@ -719,6 +1123,14 @@ function useSuggestion(providers) {
|
|
|
719
1123
|
}
|
|
720
1124
|
return true;
|
|
721
1125
|
}
|
|
1126
|
+
case "Tab": {
|
|
1127
|
+
event.preventDefault();
|
|
1128
|
+
const selectedItem = current.items[current.activeIndex];
|
|
1129
|
+
if (selectedItem) {
|
|
1130
|
+
select(selectedItem);
|
|
1131
|
+
}
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
722
1134
|
case "ArrowRight": {
|
|
723
1135
|
const activeItem = current.items[current.activeIndex];
|
|
724
1136
|
if (activeItem?.hasChildren) {
|
|
@@ -764,7 +1176,7 @@ function useSuggestion(providers) {
|
|
|
764
1176
|
}
|
|
765
1177
|
|
|
766
1178
|
// src/components/SuggestionList.tsx
|
|
767
|
-
import { useEffect as
|
|
1179
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
|
|
768
1180
|
|
|
769
1181
|
// src/utils/ariaHelpers.ts
|
|
770
1182
|
function comboboxAttrs(expanded, listboxId) {
|
|
@@ -793,13 +1205,13 @@ function optionAttrs(id, selected, index) {
|
|
|
793
1205
|
|
|
794
1206
|
// src/components/SuggestionList.tsx
|
|
795
1207
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
796
|
-
var LISTBOX_ID = "mentions-suggestion-listbox";
|
|
797
1208
|
function SuggestionList({
|
|
798
1209
|
items,
|
|
799
1210
|
activeIndex,
|
|
800
1211
|
breadcrumbs,
|
|
801
1212
|
loading,
|
|
802
1213
|
trigger,
|
|
1214
|
+
query,
|
|
803
1215
|
clientRect,
|
|
804
1216
|
onSelect,
|
|
805
1217
|
onHover,
|
|
@@ -808,7 +1220,12 @@ function SuggestionList({
|
|
|
808
1220
|
onNavigateUp,
|
|
809
1221
|
onNavigateDown,
|
|
810
1222
|
onClose,
|
|
811
|
-
|
|
1223
|
+
onFocusEditor,
|
|
1224
|
+
renderItem,
|
|
1225
|
+
renderEmpty,
|
|
1226
|
+
renderLoading,
|
|
1227
|
+
renderGroupHeader,
|
|
1228
|
+
listboxId
|
|
812
1229
|
}) {
|
|
813
1230
|
const listRef = useRef3(null);
|
|
814
1231
|
const searchInputRef = useRef3(null);
|
|
@@ -816,24 +1233,30 @@ function SuggestionList({
|
|
|
816
1233
|
const [nestedQuery, setNestedQuery] = useState2("");
|
|
817
1234
|
const breadcrumbKey = breadcrumbs.map((b) => b.id).join("/");
|
|
818
1235
|
const prevBreadcrumbKey = useRef3(breadcrumbKey);
|
|
819
|
-
|
|
1236
|
+
useEffect3(() => {
|
|
820
1237
|
if (prevBreadcrumbKey.current !== breadcrumbKey) {
|
|
821
1238
|
setNestedQuery("");
|
|
822
1239
|
prevBreadcrumbKey.current = breadcrumbKey;
|
|
823
1240
|
}
|
|
824
1241
|
}, [breadcrumbKey]);
|
|
825
|
-
|
|
826
|
-
|
|
1242
|
+
const prevBreadcrumbsLen = useRef3(breadcrumbs.length);
|
|
1243
|
+
useEffect3(() => {
|
|
1244
|
+
if (prevBreadcrumbsLen.current > 0 && breadcrumbs.length === 0) {
|
|
1245
|
+
onFocusEditor?.();
|
|
1246
|
+
} else if (breadcrumbs.length > 0 && searchInputRef.current) {
|
|
827
1247
|
requestAnimationFrame(() => searchInputRef.current?.focus());
|
|
828
1248
|
}
|
|
829
|
-
|
|
830
|
-
|
|
1249
|
+
prevBreadcrumbsLen.current = breadcrumbs.length;
|
|
1250
|
+
}, [breadcrumbKey, breadcrumbs.length, onFocusEditor]);
|
|
1251
|
+
useEffect3(() => {
|
|
831
1252
|
if (!listRef.current) return;
|
|
832
1253
|
const active = listRef.current.querySelector('[aria-selected="true"]');
|
|
833
1254
|
active?.scrollIntoView({ block: "nearest" });
|
|
834
1255
|
}, [activeIndex]);
|
|
835
|
-
const style = usePopoverPosition(clientRect);
|
|
836
|
-
|
|
1256
|
+
const { style, position } = usePopoverPosition(clientRect);
|
|
1257
|
+
const activeQuery = breadcrumbs.length > 0 ? nestedQuery : query;
|
|
1258
|
+
const showEmpty = !loading && items.length === 0 && activeQuery.trim().length > 0;
|
|
1259
|
+
if (items.length === 0 && !loading && !showEmpty) return null;
|
|
837
1260
|
const handleSearchKeyDown = (e) => {
|
|
838
1261
|
switch (e.key) {
|
|
839
1262
|
case "ArrowDown":
|
|
@@ -853,8 +1276,23 @@ function SuggestionList({
|
|
|
853
1276
|
}
|
|
854
1277
|
case "Escape":
|
|
855
1278
|
e.preventDefault();
|
|
1279
|
+
onFocusEditor?.();
|
|
856
1280
|
onClose?.();
|
|
857
1281
|
break;
|
|
1282
|
+
case "ArrowLeft":
|
|
1283
|
+
if (nestedQuery === "" || e.currentTarget.selectionStart === 0) {
|
|
1284
|
+
e.preventDefault();
|
|
1285
|
+
onGoBack();
|
|
1286
|
+
}
|
|
1287
|
+
break;
|
|
1288
|
+
case "ArrowRight": {
|
|
1289
|
+
const item = items[activeIndex];
|
|
1290
|
+
if (item?.hasChildren) {
|
|
1291
|
+
e.preventDefault();
|
|
1292
|
+
onSelect(item);
|
|
1293
|
+
}
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
858
1296
|
case "Backspace":
|
|
859
1297
|
if (nestedQuery === "") {
|
|
860
1298
|
e.preventDefault();
|
|
@@ -863,11 +1301,13 @@ function SuggestionList({
|
|
|
863
1301
|
break;
|
|
864
1302
|
}
|
|
865
1303
|
};
|
|
1304
|
+
const hasGroups = items.some((item) => item.group);
|
|
866
1305
|
return /* @__PURE__ */ jsxs(
|
|
867
1306
|
"div",
|
|
868
1307
|
{
|
|
869
1308
|
"data-suggestions": "",
|
|
870
1309
|
"data-trigger": trigger,
|
|
1310
|
+
"data-suggestions-position": position,
|
|
871
1311
|
style,
|
|
872
1312
|
ref: listRef,
|
|
873
1313
|
children: [
|
|
@@ -905,28 +1345,76 @@ function SuggestionList({
|
|
|
905
1345
|
spellCheck: false
|
|
906
1346
|
}
|
|
907
1347
|
) }),
|
|
908
|
-
loading && /* @__PURE__ */ jsx("div", { "data-suggestion-loading": "", children: "Loading..." }),
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
item.id
|
|
924
|
-
);
|
|
925
|
-
}) })
|
|
1348
|
+
loading && /* @__PURE__ */ jsx("div", { "data-suggestion-loading": "", children: renderLoading ? renderLoading() : "Loading..." }),
|
|
1349
|
+
showEmpty && /* @__PURE__ */ jsx("div", { "data-suggestion-empty": "", children: renderEmpty ? renderEmpty(activeQuery) : "No results" }),
|
|
1350
|
+
!loading && items.length > 0 && /* @__PURE__ */ jsx("div", { ...listboxAttrs(listboxId, `${trigger ?? ""} suggestions`), children: hasGroups ? renderGroupedItems(items, activeIndex, depth, onSelect, onHover, renderItem, renderGroupHeader) : items.map((item, index) => /* @__PURE__ */ jsx(
|
|
1351
|
+
SuggestionItem,
|
|
1352
|
+
{
|
|
1353
|
+
item,
|
|
1354
|
+
index,
|
|
1355
|
+
isActive: index === activeIndex,
|
|
1356
|
+
depth,
|
|
1357
|
+
onSelect,
|
|
1358
|
+
onHover,
|
|
1359
|
+
renderItem
|
|
1360
|
+
},
|
|
1361
|
+
item.id
|
|
1362
|
+
)) })
|
|
926
1363
|
]
|
|
927
1364
|
}
|
|
928
1365
|
);
|
|
929
1366
|
}
|
|
1367
|
+
function renderGroupedItems(items, activeIndex, depth, onSelect, onHover, renderItem, renderGroupHeader) {
|
|
1368
|
+
const elements = [];
|
|
1369
|
+
let lastGroup;
|
|
1370
|
+
items.forEach((item, index) => {
|
|
1371
|
+
if (item.group && item.group !== lastGroup) {
|
|
1372
|
+
lastGroup = item.group;
|
|
1373
|
+
elements.push(
|
|
1374
|
+
/* @__PURE__ */ jsx("div", { "data-suggestion-group-header": "", children: renderGroupHeader ? renderGroupHeader(item.group) : item.group }, `group-${item.group}`)
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
elements.push(
|
|
1378
|
+
/* @__PURE__ */ jsx(
|
|
1379
|
+
SuggestionItem,
|
|
1380
|
+
{
|
|
1381
|
+
item,
|
|
1382
|
+
index,
|
|
1383
|
+
isActive: index === activeIndex,
|
|
1384
|
+
depth,
|
|
1385
|
+
onSelect,
|
|
1386
|
+
onHover,
|
|
1387
|
+
renderItem
|
|
1388
|
+
},
|
|
1389
|
+
item.id
|
|
1390
|
+
)
|
|
1391
|
+
);
|
|
1392
|
+
});
|
|
1393
|
+
return elements;
|
|
1394
|
+
}
|
|
1395
|
+
function SuggestionItem({
|
|
1396
|
+
item,
|
|
1397
|
+
index,
|
|
1398
|
+
isActive,
|
|
1399
|
+
depth,
|
|
1400
|
+
onSelect,
|
|
1401
|
+
onHover,
|
|
1402
|
+
renderItem
|
|
1403
|
+
}) {
|
|
1404
|
+
const itemId = `mention-option-${item.id}`;
|
|
1405
|
+
return /* @__PURE__ */ jsx(
|
|
1406
|
+
"div",
|
|
1407
|
+
{
|
|
1408
|
+
...optionAttrs(itemId, isActive, index),
|
|
1409
|
+
"data-suggestion-item": "",
|
|
1410
|
+
"data-suggestion-item-active": isActive ? "" : void 0,
|
|
1411
|
+
"data-has-children": item.hasChildren ? "" : void 0,
|
|
1412
|
+
onMouseEnter: () => onHover(index),
|
|
1413
|
+
onClick: () => onSelect(item),
|
|
1414
|
+
children: renderItem ? renderItem(item, depth) : /* @__PURE__ */ jsx(DefaultSuggestionItem, { item })
|
|
1415
|
+
}
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
930
1418
|
function DefaultSuggestionItem({ item }) {
|
|
931
1419
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
932
1420
|
item.icon && /* @__PURE__ */ jsx("span", { "data-suggestion-item-icon": "", children: item.icon }),
|
|
@@ -935,25 +1423,48 @@ function DefaultSuggestionItem({ item }) {
|
|
|
935
1423
|
item.hasChildren && /* @__PURE__ */ jsx("span", { "data-suggestion-item-chevron": "", "aria-hidden": "true", children: "\u203A" })
|
|
936
1424
|
] });
|
|
937
1425
|
}
|
|
1426
|
+
var POPOVER_HEIGHT_ESTIMATE = 280;
|
|
1427
|
+
var POPOVER_WIDTH_ESTIMATE = 360;
|
|
938
1428
|
function usePopoverPosition(clientRect) {
|
|
939
1429
|
if (!clientRect) {
|
|
940
|
-
return { display: "none" };
|
|
1430
|
+
return { style: { display: "none" }, position: "below" };
|
|
941
1431
|
}
|
|
942
1432
|
const rect = clientRect();
|
|
943
1433
|
if (!rect) {
|
|
944
|
-
return { display: "none" };
|
|
1434
|
+
return { style: { display: "none" }, position: "below" };
|
|
1435
|
+
}
|
|
1436
|
+
const viewportH = typeof window !== "undefined" ? window.innerHeight : 800;
|
|
1437
|
+
const viewportW = typeof window !== "undefined" ? window.innerWidth : 1200;
|
|
1438
|
+
const spaceBelow = viewportH - rect.bottom;
|
|
1439
|
+
const shouldFlip = spaceBelow < POPOVER_HEIGHT_ESTIMATE && rect.top > spaceBelow;
|
|
1440
|
+
let left = rect.left;
|
|
1441
|
+
if (left + POPOVER_WIDTH_ESTIMATE > viewportW) {
|
|
1442
|
+
left = Math.max(0, viewportW - POPOVER_WIDTH_ESTIMATE);
|
|
1443
|
+
}
|
|
1444
|
+
if (shouldFlip) {
|
|
1445
|
+
return {
|
|
1446
|
+
style: {
|
|
1447
|
+
position: "fixed",
|
|
1448
|
+
left: `${left}px`,
|
|
1449
|
+
bottom: `${viewportH - rect.top + 4}px`,
|
|
1450
|
+
zIndex: 50
|
|
1451
|
+
},
|
|
1452
|
+
position: "above"
|
|
1453
|
+
};
|
|
945
1454
|
}
|
|
946
1455
|
return {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1456
|
+
style: {
|
|
1457
|
+
position: "fixed",
|
|
1458
|
+
left: `${left}px`,
|
|
1459
|
+
top: `${rect.bottom + 4}px`,
|
|
1460
|
+
zIndex: 50
|
|
1461
|
+
},
|
|
1462
|
+
position: "below"
|
|
951
1463
|
};
|
|
952
1464
|
}
|
|
953
1465
|
|
|
954
1466
|
// src/components/MentionsInput.tsx
|
|
955
1467
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
956
|
-
var LISTBOX_ID2 = "mentions-suggestion-listbox";
|
|
957
1468
|
var MentionsInput = forwardRef(
|
|
958
1469
|
function MentionsInput2({
|
|
959
1470
|
value,
|
|
@@ -967,10 +1478,31 @@ var MentionsInput = forwardRef(
|
|
|
967
1478
|
clearOnSubmit = true,
|
|
968
1479
|
maxLength,
|
|
969
1480
|
renderItem,
|
|
970
|
-
renderChip
|
|
1481
|
+
renderChip,
|
|
1482
|
+
renderEmpty,
|
|
1483
|
+
renderLoading,
|
|
1484
|
+
renderGroupHeader,
|
|
1485
|
+
onFocus,
|
|
1486
|
+
onBlur,
|
|
1487
|
+
onMentionAdd,
|
|
1488
|
+
onMentionRemove,
|
|
1489
|
+
onMentionClick,
|
|
1490
|
+
onMentionHover,
|
|
1491
|
+
minHeight,
|
|
1492
|
+
maxHeight,
|
|
1493
|
+
submitKey = "enter",
|
|
1494
|
+
allowTrigger,
|
|
1495
|
+
validateMention,
|
|
1496
|
+
portalContainer,
|
|
1497
|
+
streaming,
|
|
1498
|
+
onStreamingComplete
|
|
971
1499
|
}, ref) {
|
|
972
|
-
const
|
|
973
|
-
const
|
|
1500
|
+
const instanceId = useId();
|
|
1501
|
+
const listboxId = `mentions-listbox-${instanceId}`;
|
|
1502
|
+
const { uiState, actions, callbacksRef } = useSuggestion(providers, {
|
|
1503
|
+
onMentionAdd
|
|
1504
|
+
});
|
|
1505
|
+
const { editor, getOutput, clear, setContent, appendText, focus } = useMentionsEditor({
|
|
974
1506
|
providers,
|
|
975
1507
|
value,
|
|
976
1508
|
onChange,
|
|
@@ -979,46 +1511,72 @@ var MentionsInput = forwardRef(
|
|
|
979
1511
|
placeholder,
|
|
980
1512
|
autoFocus,
|
|
981
1513
|
editable: !disabled,
|
|
982
|
-
callbacksRef
|
|
1514
|
+
callbacksRef,
|
|
1515
|
+
onFocus,
|
|
1516
|
+
onBlur,
|
|
1517
|
+
submitKey,
|
|
1518
|
+
onMentionRemove,
|
|
1519
|
+
onMentionClick,
|
|
1520
|
+
onMentionHover,
|
|
1521
|
+
allowTrigger,
|
|
1522
|
+
validateMention,
|
|
1523
|
+
streaming,
|
|
1524
|
+
onStreamingComplete
|
|
983
1525
|
});
|
|
984
1526
|
useImperativeHandle(
|
|
985
1527
|
ref,
|
|
986
|
-
() => ({ clear, setContent, focus }),
|
|
987
|
-
[clear, setContent, focus]
|
|
1528
|
+
() => ({ clear, setContent, appendText, focus, getOutput }),
|
|
1529
|
+
[clear, setContent, appendText, focus, getOutput]
|
|
988
1530
|
);
|
|
989
1531
|
const isExpanded = uiState.state !== "idle";
|
|
990
1532
|
const handleHover = useCallback3((index) => {
|
|
991
1533
|
void index;
|
|
992
1534
|
}, []);
|
|
1535
|
+
const handleFocusEditor = useCallback3(() => {
|
|
1536
|
+
editor?.commands.focus();
|
|
1537
|
+
}, [editor]);
|
|
1538
|
+
const editorStyle = {};
|
|
1539
|
+
if (minHeight != null) editorStyle.minHeight = `${minHeight}px`;
|
|
1540
|
+
if (maxHeight != null) {
|
|
1541
|
+
editorStyle.maxHeight = `${maxHeight}px`;
|
|
1542
|
+
editorStyle.overflowY = "auto";
|
|
1543
|
+
}
|
|
1544
|
+
const suggestionList = isExpanded ? /* @__PURE__ */ jsx2(
|
|
1545
|
+
SuggestionList,
|
|
1546
|
+
{
|
|
1547
|
+
items: uiState.items,
|
|
1548
|
+
activeIndex: uiState.activeIndex,
|
|
1549
|
+
breadcrumbs: uiState.breadcrumbs,
|
|
1550
|
+
loading: uiState.loading,
|
|
1551
|
+
trigger: uiState.trigger,
|
|
1552
|
+
query: uiState.query,
|
|
1553
|
+
clientRect: uiState.clientRect,
|
|
1554
|
+
onSelect: (item) => actions.select(item),
|
|
1555
|
+
onHover: handleHover,
|
|
1556
|
+
onGoBack: actions.goBack,
|
|
1557
|
+
onSearchNested: actions.searchNested,
|
|
1558
|
+
onNavigateUp: actions.navigateUp,
|
|
1559
|
+
onNavigateDown: actions.navigateDown,
|
|
1560
|
+
onClose: actions.close,
|
|
1561
|
+
onFocusEditor: handleFocusEditor,
|
|
1562
|
+
renderItem,
|
|
1563
|
+
renderEmpty,
|
|
1564
|
+
renderLoading,
|
|
1565
|
+
renderGroupHeader,
|
|
1566
|
+
listboxId
|
|
1567
|
+
}
|
|
1568
|
+
) : null;
|
|
993
1569
|
return /* @__PURE__ */ jsxs2(
|
|
994
1570
|
"div",
|
|
995
1571
|
{
|
|
996
1572
|
className,
|
|
997
1573
|
"data-mentions-input": "",
|
|
998
1574
|
"data-disabled": disabled ? "" : void 0,
|
|
999
|
-
...comboboxAttrs(isExpanded,
|
|
1575
|
+
...comboboxAttrs(isExpanded, listboxId),
|
|
1000
1576
|
"aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
|
|
1001
1577
|
children: [
|
|
1002
|
-
/* @__PURE__ */ jsx2(EditorContent, { editor }),
|
|
1003
|
-
|
|
1004
|
-
SuggestionList,
|
|
1005
|
-
{
|
|
1006
|
-
items: uiState.items,
|
|
1007
|
-
activeIndex: uiState.activeIndex,
|
|
1008
|
-
breadcrumbs: uiState.breadcrumbs,
|
|
1009
|
-
loading: uiState.loading,
|
|
1010
|
-
trigger: uiState.trigger,
|
|
1011
|
-
clientRect: uiState.clientRect,
|
|
1012
|
-
onSelect: (item) => actions.select(item),
|
|
1013
|
-
onHover: handleHover,
|
|
1014
|
-
onGoBack: actions.goBack,
|
|
1015
|
-
onSearchNested: actions.searchNested,
|
|
1016
|
-
onNavigateUp: actions.navigateUp,
|
|
1017
|
-
onNavigateDown: actions.navigateDown,
|
|
1018
|
-
onClose: actions.close,
|
|
1019
|
-
renderItem
|
|
1020
|
-
}
|
|
1021
|
-
)
|
|
1578
|
+
/* @__PURE__ */ jsx2("div", { style: editorStyle, children: /* @__PURE__ */ jsx2(EditorContent, { editor }) }),
|
|
1579
|
+
portalContainer ? suggestionList && createPortal(suggestionList, portalContainer) : suggestionList
|
|
1022
1580
|
]
|
|
1023
1581
|
}
|
|
1024
1582
|
);
|