@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/dist/index.js CHANGED
@@ -39,6 +39,7 @@ module.exports = __toCommonJS(index_exports);
39
39
 
40
40
  // src/components/MentionsInput.tsx
41
41
  var import_react5 = require("react");
42
+ var import_react_dom = require("react-dom");
42
43
  var import_react6 = require("@tiptap/react");
43
44
 
44
45
  // src/hooks/useMentionsEditor.ts
@@ -64,6 +65,12 @@ var MentionNode = import_core.Node.create({
64
65
  atom: true,
65
66
  selectable: true,
66
67
  draggable: false,
68
+ addOptions() {
69
+ return {
70
+ onClickRef: void 0,
71
+ onHoverRef: void 0
72
+ };
73
+ },
67
74
  addAttributes() {
68
75
  return {
69
76
  id: {
@@ -96,11 +103,17 @@ var MentionNode = import_core.Node.create({
96
103
  const label = node.attrs.label;
97
104
  const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
98
105
  const display = `${prefix}${label}`;
106
+ const hasClick = !!this.options.onClickRef?.current;
107
+ const extraAttrs = {};
108
+ if (hasClick) {
109
+ extraAttrs["data-mention-clickable"] = "";
110
+ }
99
111
  return [
100
112
  "span",
101
113
  (0, import_core.mergeAttributes)(HTMLAttributes, {
102
114
  "data-mention": "",
103
- class: "mention-chip"
115
+ class: "mention-chip",
116
+ ...extraAttrs
104
117
  }),
105
118
  display
106
119
  ];
@@ -111,6 +124,58 @@ var MentionNode = import_core.Node.create({
111
124
  const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
112
125
  return `${prefix}${label}`;
113
126
  },
127
+ addNodeView() {
128
+ const options = this.options;
129
+ return ({ node, HTMLAttributes }) => {
130
+ const entityType = node.attrs.entityType;
131
+ const label = node.attrs.label;
132
+ const id = node.attrs.id;
133
+ const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
134
+ const dom = document.createElement("span");
135
+ Object.entries(
136
+ (0, import_core.mergeAttributes)(HTMLAttributes, {
137
+ "data-mention": "",
138
+ "data-type": entityType,
139
+ "data-id": id,
140
+ class: "mention-chip"
141
+ })
142
+ ).forEach(([key, val]) => {
143
+ if (val != null && val !== false) dom.setAttribute(key, String(val));
144
+ });
145
+ dom.textContent = `${prefix}${label}`;
146
+ if (options.onClickRef?.current) {
147
+ dom.setAttribute("data-mention-clickable", "");
148
+ dom.style.cursor = "pointer";
149
+ }
150
+ dom.addEventListener("click", (event) => {
151
+ const handler = options.onClickRef?.current;
152
+ if (handler) {
153
+ event.preventDefault();
154
+ event.stopPropagation();
155
+ handler({ id, type: entityType, label }, event);
156
+ }
157
+ });
158
+ let tooltip = null;
159
+ dom.addEventListener("mouseenter", () => {
160
+ const hoverFn = options.onHoverRef?.current;
161
+ if (!hoverFn) return;
162
+ const content = hoverFn({ id, type: entityType, label });
163
+ if (!content) return;
164
+ tooltip = document.createElement("div");
165
+ tooltip.setAttribute("data-mention-tooltip", "");
166
+ tooltip.textContent = typeof content === "string" ? content : "";
167
+ dom.style.position = "relative";
168
+ dom.appendChild(tooltip);
169
+ });
170
+ dom.addEventListener("mouseleave", () => {
171
+ if (tooltip && tooltip.parentNode) {
172
+ tooltip.parentNode.removeChild(tooltip);
173
+ tooltip = null;
174
+ }
175
+ });
176
+ return { dom };
177
+ };
178
+ },
114
179
  addKeyboardShortcuts() {
115
180
  return {
116
181
  Backspace: () => this.editor.commands.command(({ tr, state }) => {
@@ -143,7 +208,13 @@ function detectTrigger(text, cursorPos, docStartPos, triggers) {
143
208
  if (before.substring(i, i + trigger.length) === trigger) {
144
209
  if (i === 0 || /\s/.test(before[i - 1])) {
145
210
  const query = before.slice(i + trigger.length);
146
- return { trigger, query, from: docStartPos + i, to: cursorPos };
211
+ return {
212
+ trigger,
213
+ query,
214
+ from: docStartPos + i,
215
+ to: cursorPos,
216
+ textBefore: before.slice(0, i)
217
+ };
147
218
  }
148
219
  }
149
220
  }
@@ -151,7 +222,7 @@ function detectTrigger(text, cursorPos, docStartPos, triggers) {
151
222
  return null;
152
223
  }
153
224
  var suggestionPluginKey = new import_state.PluginKey("mentionSuggestion");
154
- function createSuggestionExtension(triggers, callbacksRef) {
225
+ function createSuggestionExtension(triggers, callbacksRef, allowTriggerRef, streamingRef) {
155
226
  return import_core2.Extension.create({
156
227
  name: "mentionSuggestion",
157
228
  priority: 200,
@@ -202,6 +273,15 @@ function createSuggestionExtension(triggers, callbacksRef) {
202
273
  view() {
203
274
  return {
204
275
  update(view, _prevState) {
276
+ if (streamingRef?.current) {
277
+ if (active) {
278
+ active = false;
279
+ lastQuery = null;
280
+ lastTrigger = null;
281
+ callbacksRef.current.onExit();
282
+ }
283
+ return;
284
+ }
205
285
  const { state } = view;
206
286
  const { selection } = state;
207
287
  if (!selection.empty) {
@@ -229,6 +309,20 @@ function createSuggestionExtension(triggers, callbacksRef) {
229
309
  const cursorPos = $pos.pos;
230
310
  const match = detectTrigger(blockText, cursorPos, blockStart, triggers);
231
311
  if (match) {
312
+ if (allowTriggerRef?.current) {
313
+ const allowed = allowTriggerRef.current(match.trigger, {
314
+ textBefore: match.textBefore
315
+ });
316
+ if (!allowed) {
317
+ if (active) {
318
+ active = false;
319
+ lastQuery = null;
320
+ lastTrigger = null;
321
+ callbacksRef.current.onExit();
322
+ }
323
+ return;
324
+ }
325
+ }
232
326
  const range = { from: match.from, to: match.to };
233
327
  const props = {
234
328
  query: match.query,
@@ -398,27 +492,48 @@ function buildOutput(editor) {
398
492
  plainText: extractPlainText(json)
399
493
  };
400
494
  }
401
- function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
495
+ function collectMentionTokens(doc) {
496
+ const tokens = [];
497
+ function walk(node) {
498
+ if (node.type === "mention" && node.attrs) {
499
+ tokens.push({
500
+ id: node.attrs.id,
501
+ type: node.attrs.entityType ?? node.attrs.type,
502
+ label: node.attrs.label
503
+ });
504
+ }
505
+ if (node.content) {
506
+ for (const child of node.content) walk(child);
507
+ }
508
+ }
509
+ walk(doc);
510
+ return tokens;
511
+ }
512
+ function createSubmitExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef) {
402
513
  return import_core3.Extension.create({
403
514
  name: "submitShortcut",
404
515
  priority: 150,
405
516
  addKeyboardShortcuts() {
406
517
  return {
407
518
  "Mod-Enter": () => {
408
- if (onSubmitRef.current) {
409
- onSubmitRef.current(buildOutput(this.editor));
410
- if (clearOnSubmitRef.current) {
411
- this.editor.commands.clearContent(true);
519
+ const key = submitKeyRef.current;
520
+ if (key === "mod+enter" || key === "enter") {
521
+ if (onSubmitRef.current) {
522
+ onSubmitRef.current(buildOutput(this.editor));
523
+ if (clearOnSubmitRef.current) {
524
+ this.editor.commands.clearContent(true);
525
+ }
412
526
  }
527
+ return true;
413
528
  }
414
- return true;
529
+ return false;
415
530
  }
416
531
  };
417
532
  }
418
533
  });
419
534
  }
420
535
  var enterSubmitPluginKey = new import_state2.PluginKey("enterSubmit");
421
- function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
536
+ function createEnterExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef) {
422
537
  return import_core3.Extension.create({
423
538
  name: "enterSubmit",
424
539
  priority: 150,
@@ -430,6 +545,11 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
430
545
  props: {
431
546
  handleKeyDown(_view, event) {
432
547
  if (event.key !== "Enter") return false;
548
+ const key = submitKeyRef.current;
549
+ if (key === "none") return false;
550
+ if (key === "mod+enter") {
551
+ return false;
552
+ }
433
553
  if (event.shiftKey) {
434
554
  editor.commands.splitBlock();
435
555
  return true;
@@ -449,6 +569,61 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
449
569
  }
450
570
  });
451
571
  }
572
+ var mentionRemovePluginKey = new import_state2.PluginKey("mentionRemove");
573
+ function createMentionRemoveExtension(onMentionRemoveRef) {
574
+ return import_core3.Extension.create({
575
+ name: "mentionRemoveDetector",
576
+ priority: 100,
577
+ addProseMirrorPlugins() {
578
+ return [
579
+ new import_state2.Plugin({
580
+ key: mentionRemovePluginKey,
581
+ appendTransaction(transactions, oldState, newState) {
582
+ if (!onMentionRemoveRef.current) return null;
583
+ const oldMentions = collectMentionTokens(oldState.doc.toJSON());
584
+ const newMentions = collectMentionTokens(newState.doc.toJSON());
585
+ if (oldMentions.length <= newMentions.length) return null;
586
+ const newIds = new Set(newMentions.map((m) => m.id));
587
+ for (const m of oldMentions) {
588
+ if (!newIds.has(m.id)) {
589
+ onMentionRemoveRef.current(m);
590
+ }
591
+ }
592
+ return null;
593
+ }
594
+ })
595
+ ];
596
+ }
597
+ });
598
+ }
599
+ var streamingBlockPluginKey = new import_state2.PluginKey("streamingBlock");
600
+ function createStreamingBlockExtension(streamingRef) {
601
+ return import_core3.Extension.create({
602
+ name: "streamingBlock",
603
+ priority: 200,
604
+ addProseMirrorPlugins() {
605
+ return [
606
+ new import_state2.Plugin({
607
+ key: streamingBlockPluginKey,
608
+ props: {
609
+ handleKeyDown() {
610
+ return streamingRef.current;
611
+ },
612
+ handleKeyPress() {
613
+ return streamingRef.current;
614
+ },
615
+ handlePaste() {
616
+ return streamingRef.current;
617
+ },
618
+ handleDrop() {
619
+ return streamingRef.current;
620
+ }
621
+ }
622
+ })
623
+ ];
624
+ }
625
+ });
626
+ }
452
627
  function useMentionsEditor({
453
628
  providers,
454
629
  value,
@@ -458,7 +633,17 @@ function useMentionsEditor({
458
633
  placeholder,
459
634
  autoFocus = false,
460
635
  editable = true,
461
- callbacksRef
636
+ callbacksRef,
637
+ onFocus,
638
+ onBlur,
639
+ submitKey = "enter",
640
+ onMentionRemove,
641
+ onMentionClick,
642
+ onMentionHover,
643
+ allowTrigger,
644
+ validateMention,
645
+ streaming = false,
646
+ onStreamingComplete
462
647
  }) {
463
648
  const onChangeRef = (0, import_react.useRef)(onChange);
464
649
  onChangeRef.current = onChange;
@@ -466,6 +651,30 @@ function useMentionsEditor({
466
651
  onSubmitRef.current = onSubmit;
467
652
  const clearOnSubmitRef = (0, import_react.useRef)(clearOnSubmit);
468
653
  clearOnSubmitRef.current = clearOnSubmit;
654
+ const onFocusRef = (0, import_react.useRef)(onFocus);
655
+ onFocusRef.current = onFocus;
656
+ const onBlurRef = (0, import_react.useRef)(onBlur);
657
+ onBlurRef.current = onBlur;
658
+ const submitKeyRef = (0, import_react.useRef)(submitKey);
659
+ submitKeyRef.current = submitKey;
660
+ const onMentionRemoveRef = (0, import_react.useRef)(onMentionRemove);
661
+ onMentionRemoveRef.current = onMentionRemove;
662
+ const onMentionClickRef = (0, import_react.useRef)(onMentionClick);
663
+ onMentionClickRef.current = onMentionClick;
664
+ const onMentionHoverRef = (0, import_react.useRef)(onMentionHover);
665
+ onMentionHoverRef.current = onMentionHover;
666
+ const allowTriggerRef = (0, import_react.useRef)(allowTrigger);
667
+ allowTriggerRef.current = allowTrigger;
668
+ const validateMentionRef = (0, import_react.useRef)(validateMention);
669
+ validateMentionRef.current = validateMention;
670
+ const onStreamingCompleteRef = (0, import_react.useRef)(onStreamingComplete);
671
+ onStreamingCompleteRef.current = onStreamingComplete;
672
+ const streamingRef = (0, import_react.useRef)(streaming);
673
+ streamingRef.current = streaming;
674
+ const prevStreamingRef = (0, import_react.useRef)(streaming);
675
+ const throttleTimerRef = (0, import_react.useRef)(null);
676
+ const pendingOutputRef = (0, import_react.useRef)(null);
677
+ const internalMarkdownRef = (0, import_react.useRef)(null);
469
678
  const initialContent = (0, import_react.useMemo)(() => {
470
679
  if (!value) return void 0;
471
680
  return parseFromMarkdown(value);
@@ -477,16 +686,36 @@ function useMentionsEditor({
477
686
  [triggersKey]
478
687
  );
479
688
  const suggestionExtension = (0, import_react.useMemo)(
480
- () => createSuggestionExtension(triggers, callbacksRef),
689
+ () => createSuggestionExtension(
690
+ triggers,
691
+ callbacksRef,
692
+ allowTriggerRef,
693
+ streamingRef
694
+ ),
481
695
  // eslint-disable-next-line react-hooks/exhaustive-deps
482
696
  [triggersKey]
483
697
  );
484
698
  const submitExt = (0, import_react.useMemo)(
485
- () => createSubmitExtension(onSubmitRef, clearOnSubmitRef),
699
+ () => createSubmitExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef),
486
700
  []
487
701
  );
488
702
  const enterExt = (0, import_react.useMemo)(
489
- () => createEnterExtension(onSubmitRef, clearOnSubmitRef),
703
+ () => createEnterExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef),
704
+ []
705
+ );
706
+ const mentionRemoveExt = (0, import_react.useMemo)(
707
+ () => createMentionRemoveExtension(onMentionRemoveRef),
708
+ []
709
+ );
710
+ const streamingBlockExt = (0, import_react.useMemo)(
711
+ () => createStreamingBlockExtension(streamingRef),
712
+ []
713
+ );
714
+ const mentionNodeExt = (0, import_react.useMemo)(
715
+ () => MentionNode.configure({
716
+ onClickRef: onMentionClickRef,
717
+ onHoverRef: onMentionHoverRef
718
+ }),
490
719
  []
491
720
  );
492
721
  const editor = (0, import_react2.useEditor)({
@@ -505,10 +734,12 @@ function useMentionsEditor({
505
734
  placeholder: ({ editor: editor2 }) => editor2.isEmpty ? placeholder ?? "Type a message..." : "",
506
735
  showOnlyCurrent: true
507
736
  }),
508
- MentionNode,
737
+ mentionNodeExt,
509
738
  suggestionExtension,
510
739
  submitExt,
511
- enterExt
740
+ enterExt,
741
+ mentionRemoveExt,
742
+ streamingBlockExt
512
743
  ],
513
744
  content: initialContent,
514
745
  autofocus: autoFocus ? "end" : false,
@@ -519,14 +750,85 @@ function useMentionsEditor({
519
750
  }
520
751
  },
521
752
  onUpdate: ({ editor: editor2 }) => {
522
- onChangeRef.current?.(buildOutput(editor2));
753
+ const output = buildOutput(editor2);
754
+ internalMarkdownRef.current = output.markdown;
755
+ if (streamingRef.current) {
756
+ pendingOutputRef.current = output;
757
+ if (!throttleTimerRef.current) {
758
+ throttleTimerRef.current = setTimeout(() => {
759
+ throttleTimerRef.current = null;
760
+ if (pendingOutputRef.current) {
761
+ onChangeRef.current?.(pendingOutputRef.current);
762
+ pendingOutputRef.current = null;
763
+ }
764
+ }, 150);
765
+ }
766
+ } else {
767
+ onChangeRef.current?.(output);
768
+ }
769
+ },
770
+ onFocus: () => {
771
+ onFocusRef.current?.();
772
+ },
773
+ onBlur: () => {
774
+ onBlurRef.current?.();
523
775
  }
524
776
  });
777
+ (0, import_react.useEffect)(() => {
778
+ if (prevStreamingRef.current && !streaming && editor) {
779
+ if (throttleTimerRef.current) {
780
+ clearTimeout(throttleTimerRef.current);
781
+ throttleTimerRef.current = null;
782
+ }
783
+ const output = buildOutput(editor);
784
+ onChangeRef.current?.(output);
785
+ onStreamingCompleteRef.current?.(output);
786
+ pendingOutputRef.current = null;
787
+ }
788
+ prevStreamingRef.current = streaming;
789
+ }, [streaming, editor]);
790
+ (0, import_react.useEffect)(() => {
791
+ return () => {
792
+ if (throttleTimerRef.current) {
793
+ clearTimeout(throttleTimerRef.current);
794
+ }
795
+ };
796
+ }, []);
525
797
  (0, import_react.useEffect)(() => {
526
798
  if (editor && editor.isEditable !== editable) {
527
799
  editor.setEditable(editable);
528
800
  }
529
801
  }, [editor, editable]);
802
+ (0, import_react.useEffect)(() => {
803
+ if (!editor || value === void 0) return;
804
+ if (value === internalMarkdownRef.current) return;
805
+ const doc = parseFromMarkdown(value);
806
+ editor.commands.setContent(doc);
807
+ internalMarkdownRef.current = value;
808
+ }, [editor, value]);
809
+ (0, import_react.useEffect)(() => {
810
+ if (!editor || !validateMention) return;
811
+ const runValidation = async () => {
812
+ const doc = editor.getJSON();
813
+ const tokens = collectMentionTokens(doc);
814
+ const invalidIds = /* @__PURE__ */ new Set();
815
+ await Promise.all(
816
+ tokens.map(async (token) => {
817
+ const valid = await validateMention(token);
818
+ if (!valid) invalidIds.add(token.id);
819
+ })
820
+ );
821
+ editor.view.dom.querySelectorAll("[data-mention]").forEach((el) => {
822
+ const id = el.getAttribute("data-id");
823
+ if (id && invalidIds.has(id)) {
824
+ el.setAttribute("data-mention-invalid", "");
825
+ } else {
826
+ el.removeAttribute("data-mention-invalid");
827
+ }
828
+ });
829
+ };
830
+ runValidation();
831
+ }, [editor, validateMention]);
530
832
  const clear = (0, import_react.useCallback)(() => {
531
833
  editor?.commands.clearContent(true);
532
834
  }, [editor]);
@@ -535,6 +837,21 @@ function useMentionsEditor({
535
837
  if (!editor) return;
536
838
  const doc = parseFromMarkdown(markdown);
537
839
  editor.commands.setContent(doc);
840
+ internalMarkdownRef.current = markdown;
841
+ if (streamingRef.current) {
842
+ editor.commands.focus("end");
843
+ }
844
+ },
845
+ [editor]
846
+ );
847
+ const appendText = (0, import_react.useCallback)(
848
+ (text) => {
849
+ if (!editor) return;
850
+ const endPos = editor.state.doc.content.size - 1;
851
+ editor.commands.insertContentAt(endPos, text);
852
+ if (streamingRef.current) {
853
+ editor.commands.focus("end");
854
+ }
538
855
  },
539
856
  [editor]
540
857
  );
@@ -545,11 +862,32 @@ function useMentionsEditor({
545
862
  if (!editor) return null;
546
863
  return buildOutput(editor);
547
864
  }, [editor]);
548
- return { editor, getOutput, clear, setContent, focus };
865
+ return { editor, getOutput, clear, setContent, appendText, focus };
549
866
  }
550
867
 
551
868
  // src/hooks/useSuggestion.ts
552
869
  var import_react3 = require("react");
870
+
871
+ // src/utils/debounce.ts
872
+ function debounce(fn, ms) {
873
+ let timer = null;
874
+ const debounced = ((...args) => {
875
+ if (timer != null) clearTimeout(timer);
876
+ timer = setTimeout(() => {
877
+ timer = null;
878
+ fn(...args);
879
+ }, ms);
880
+ });
881
+ debounced.cancel = () => {
882
+ if (timer != null) {
883
+ clearTimeout(timer);
884
+ timer = null;
885
+ }
886
+ };
887
+ return debounced;
888
+ }
889
+
890
+ // src/hooks/useSuggestion.ts
553
891
  var IDLE_STATE = {
554
892
  state: "idle",
555
893
  items: [],
@@ -560,16 +898,24 @@ var IDLE_STATE = {
560
898
  trigger: null,
561
899
  query: ""
562
900
  };
563
- function useSuggestion(providers) {
901
+ function useSuggestion(providers, options = {}) {
564
902
  const [uiState, setUIState] = (0, import_react3.useState)(IDLE_STATE);
565
903
  const stateRef = (0, import_react3.useRef)(uiState);
566
904
  stateRef.current = uiState;
567
905
  const providersRef = (0, import_react3.useRef)(providers);
568
906
  providersRef.current = providers;
907
+ const onMentionAddRef = (0, import_react3.useRef)(options.onMentionAdd);
908
+ onMentionAddRef.current = options.onMentionAdd;
569
909
  const commandRef = (0, import_react3.useRef)(
570
910
  null
571
911
  );
572
912
  const providerRef = (0, import_react3.useRef)(null);
913
+ const debouncedFetchRef = (0, import_react3.useRef)(null);
914
+ (0, import_react3.useEffect)(() => {
915
+ return () => {
916
+ debouncedFetchRef.current?.cancel();
917
+ };
918
+ }, []);
573
919
  const fetchItems = (0, import_react3.useCallback)(
574
920
  async (provider, query, parent, useSearchAll) => {
575
921
  setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
@@ -600,6 +946,23 @@ function useSuggestion(providers) {
600
946
  },
601
947
  []
602
948
  );
949
+ const scheduleFetch = (0, import_react3.useCallback)(
950
+ (provider, query, parent, useSearchAll) => {
951
+ debouncedFetchRef.current?.cancel();
952
+ const ms = provider.debounceMs;
953
+ if (ms && ms > 0) {
954
+ setUIState((prev) => ({ ...prev, loading: true }));
955
+ const debouncedFn = debounce(() => {
956
+ fetchItems(provider, query, parent, useSearchAll);
957
+ }, ms);
958
+ debouncedFetchRef.current = debouncedFn;
959
+ debouncedFn();
960
+ } else {
961
+ fetchItems(provider, query, parent, useSearchAll);
962
+ }
963
+ },
964
+ [fetchItems]
965
+ );
603
966
  const onStart = (0, import_react3.useCallback)(
604
967
  (props) => {
605
968
  const provider = providersRef.current.find(
@@ -618,13 +981,31 @@ function useSuggestion(providers) {
618
981
  trigger: props.trigger,
619
982
  query: props.query
620
983
  });
984
+ if (!props.query.trim() && provider.getRecentItems) {
985
+ provider.getRecentItems().then((recentItems) => {
986
+ const tagged = recentItems.map((item) => ({
987
+ ...item,
988
+ group: item.group ?? "Recent"
989
+ }));
990
+ setUIState((prev) => ({
991
+ ...prev,
992
+ items: tagged,
993
+ loading: false,
994
+ state: "showing",
995
+ activeIndex: 0
996
+ }));
997
+ }).catch(() => {
998
+ scheduleFetch(provider, props.query);
999
+ });
1000
+ return;
1001
+ }
621
1002
  if (props.query.trim() && provider.searchAll) {
622
- fetchItems(provider, props.query, void 0, true);
1003
+ scheduleFetch(provider, props.query, void 0, true);
623
1004
  } else {
624
- fetchItems(provider, props.query);
1005
+ scheduleFetch(provider, props.query);
625
1006
  }
626
1007
  },
627
- [fetchItems]
1008
+ [scheduleFetch]
628
1009
  );
629
1010
  const onUpdate = (0, import_react3.useCallback)(
630
1011
  (props) => {
@@ -648,14 +1029,31 @@ function useSuggestion(providers) {
648
1029
  }));
649
1030
  }
650
1031
  if (props.query.trim() && provider.searchAll) {
651
- fetchItems(provider, props.query, void 0, true);
1032
+ scheduleFetch(provider, props.query, void 0, true);
1033
+ } else if (!props.query.trim() && provider.getRecentItems) {
1034
+ provider.getRecentItems().then((recentItems) => {
1035
+ const tagged = recentItems.map((item) => ({
1036
+ ...item,
1037
+ group: item.group ?? "Recent"
1038
+ }));
1039
+ setUIState((prev) => ({
1040
+ ...prev,
1041
+ items: tagged,
1042
+ loading: false,
1043
+ state: "showing",
1044
+ activeIndex: 0
1045
+ }));
1046
+ }).catch(() => {
1047
+ scheduleFetch(provider, props.query);
1048
+ });
652
1049
  } else {
653
- fetchItems(provider, props.query);
1050
+ scheduleFetch(provider, props.query);
654
1051
  }
655
1052
  },
656
- [fetchItems]
1053
+ [scheduleFetch]
657
1054
  );
658
1055
  const onExit = (0, import_react3.useCallback)(() => {
1056
+ debouncedFetchRef.current?.cancel();
659
1057
  providerRef.current = null;
660
1058
  commandRef.current = null;
661
1059
  setUIState(IDLE_STATE);
@@ -690,8 +1088,8 @@ function useSuggestion(providers) {
690
1088
  fetchItems(provider, "", selected);
691
1089
  return;
692
1090
  }
1091
+ const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
693
1092
  if (commandRef.current) {
694
- const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
695
1093
  commandRef.current({
696
1094
  id: selected.id,
697
1095
  label: selected.label,
@@ -699,6 +1097,11 @@ function useSuggestion(providers) {
699
1097
  rootLabel
700
1098
  });
701
1099
  }
1100
+ onMentionAddRef.current?.({
1101
+ id: selected.id,
1102
+ type: selected.type,
1103
+ label: selected.label
1104
+ });
702
1105
  },
703
1106
  [fetchItems]
704
1107
  );
@@ -723,6 +1126,7 @@ function useSuggestion(providers) {
723
1126
  });
724
1127
  }, [fetchItems]);
725
1128
  const close = (0, import_react3.useCallback)(() => {
1129
+ debouncedFetchRef.current?.cancel();
726
1130
  setUIState(IDLE_STATE);
727
1131
  }, []);
728
1132
  const searchNested = (0, import_react3.useCallback)(
@@ -732,10 +1136,10 @@ function useSuggestion(providers) {
732
1136
  const current = stateRef.current;
733
1137
  const parent = current.breadcrumbs[current.breadcrumbs.length - 1];
734
1138
  if (parent) {
735
- fetchItems(provider, query, parent);
1139
+ scheduleFetch(provider, query, parent);
736
1140
  }
737
1141
  },
738
- [fetchItems]
1142
+ [scheduleFetch]
739
1143
  );
740
1144
  const onKeyDown = (0, import_react3.useCallback)(
741
1145
  ({ event }) => {
@@ -758,6 +1162,14 @@ function useSuggestion(providers) {
758
1162
  }
759
1163
  return true;
760
1164
  }
1165
+ case "Tab": {
1166
+ event.preventDefault();
1167
+ const selectedItem = current.items[current.activeIndex];
1168
+ if (selectedItem) {
1169
+ select(selectedItem);
1170
+ }
1171
+ return true;
1172
+ }
761
1173
  case "ArrowRight": {
762
1174
  const activeItem = current.items[current.activeIndex];
763
1175
  if (activeItem?.hasChildren) {
@@ -832,13 +1244,13 @@ function optionAttrs(id, selected, index) {
832
1244
 
833
1245
  // src/components/SuggestionList.tsx
834
1246
  var import_jsx_runtime = require("react/jsx-runtime");
835
- var LISTBOX_ID = "mentions-suggestion-listbox";
836
1247
  function SuggestionList({
837
1248
  items,
838
1249
  activeIndex,
839
1250
  breadcrumbs,
840
1251
  loading,
841
1252
  trigger,
1253
+ query,
842
1254
  clientRect,
843
1255
  onSelect,
844
1256
  onHover,
@@ -847,7 +1259,12 @@ function SuggestionList({
847
1259
  onNavigateUp,
848
1260
  onNavigateDown,
849
1261
  onClose,
850
- renderItem
1262
+ onFocusEditor,
1263
+ renderItem,
1264
+ renderEmpty,
1265
+ renderLoading,
1266
+ renderGroupHeader,
1267
+ listboxId
851
1268
  }) {
852
1269
  const listRef = (0, import_react4.useRef)(null);
853
1270
  const searchInputRef = (0, import_react4.useRef)(null);
@@ -861,18 +1278,24 @@ function SuggestionList({
861
1278
  prevBreadcrumbKey.current = breadcrumbKey;
862
1279
  }
863
1280
  }, [breadcrumbKey]);
1281
+ const prevBreadcrumbsLen = (0, import_react4.useRef)(breadcrumbs.length);
864
1282
  (0, import_react4.useEffect)(() => {
865
- if (breadcrumbs.length > 0 && searchInputRef.current) {
1283
+ if (prevBreadcrumbsLen.current > 0 && breadcrumbs.length === 0) {
1284
+ onFocusEditor?.();
1285
+ } else if (breadcrumbs.length > 0 && searchInputRef.current) {
866
1286
  requestAnimationFrame(() => searchInputRef.current?.focus());
867
1287
  }
868
- }, [breadcrumbKey, breadcrumbs.length]);
1288
+ prevBreadcrumbsLen.current = breadcrumbs.length;
1289
+ }, [breadcrumbKey, breadcrumbs.length, onFocusEditor]);
869
1290
  (0, import_react4.useEffect)(() => {
870
1291
  if (!listRef.current) return;
871
1292
  const active = listRef.current.querySelector('[aria-selected="true"]');
872
1293
  active?.scrollIntoView({ block: "nearest" });
873
1294
  }, [activeIndex]);
874
- const style = usePopoverPosition(clientRect);
875
- if (items.length === 0 && !loading) return null;
1295
+ const { style, position } = usePopoverPosition(clientRect);
1296
+ const activeQuery = breadcrumbs.length > 0 ? nestedQuery : query;
1297
+ const showEmpty = !loading && items.length === 0 && activeQuery.trim().length > 0;
1298
+ if (items.length === 0 && !loading && !showEmpty) return null;
876
1299
  const handleSearchKeyDown = (e) => {
877
1300
  switch (e.key) {
878
1301
  case "ArrowDown":
@@ -892,8 +1315,23 @@ function SuggestionList({
892
1315
  }
893
1316
  case "Escape":
894
1317
  e.preventDefault();
1318
+ onFocusEditor?.();
895
1319
  onClose?.();
896
1320
  break;
1321
+ case "ArrowLeft":
1322
+ if (nestedQuery === "" || e.currentTarget.selectionStart === 0) {
1323
+ e.preventDefault();
1324
+ onGoBack();
1325
+ }
1326
+ break;
1327
+ case "ArrowRight": {
1328
+ const item = items[activeIndex];
1329
+ if (item?.hasChildren) {
1330
+ e.preventDefault();
1331
+ onSelect(item);
1332
+ }
1333
+ break;
1334
+ }
897
1335
  case "Backspace":
898
1336
  if (nestedQuery === "") {
899
1337
  e.preventDefault();
@@ -902,11 +1340,13 @@ function SuggestionList({
902
1340
  break;
903
1341
  }
904
1342
  };
1343
+ const hasGroups = items.some((item) => item.group);
905
1344
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
906
1345
  "div",
907
1346
  {
908
1347
  "data-suggestions": "",
909
1348
  "data-trigger": trigger,
1349
+ "data-suggestions-position": position,
910
1350
  style,
911
1351
  ref: listRef,
912
1352
  children: [
@@ -944,28 +1384,76 @@ function SuggestionList({
944
1384
  spellCheck: false
945
1385
  }
946
1386
  ) }),
947
- loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-loading": "", children: "Loading..." }),
948
- !loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...listboxAttrs(LISTBOX_ID, `${trigger ?? ""} suggestions`), children: items.map((item, index) => {
949
- const isActive = index === activeIndex;
950
- const itemId = `mention-option-${item.id}`;
951
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
952
- "div",
953
- {
954
- ...optionAttrs(itemId, isActive, index),
955
- "data-suggestion-item": "",
956
- "data-suggestion-item-active": isActive ? "" : void 0,
957
- "data-has-children": item.hasChildren ? "" : void 0,
958
- onMouseEnter: () => onHover(index),
959
- onClick: () => onSelect(item),
960
- children: renderItem ? renderItem(item, depth) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultSuggestionItem, { item })
961
- },
962
- item.id
963
- );
964
- }) })
1387
+ loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-loading": "", children: renderLoading ? renderLoading() : "Loading..." }),
1388
+ showEmpty && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-empty": "", children: renderEmpty ? renderEmpty(activeQuery) : "No results" }),
1389
+ !loading && items.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...listboxAttrs(listboxId, `${trigger ?? ""} suggestions`), children: hasGroups ? renderGroupedItems(items, activeIndex, depth, onSelect, onHover, renderItem, renderGroupHeader) : items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1390
+ SuggestionItem,
1391
+ {
1392
+ item,
1393
+ index,
1394
+ isActive: index === activeIndex,
1395
+ depth,
1396
+ onSelect,
1397
+ onHover,
1398
+ renderItem
1399
+ },
1400
+ item.id
1401
+ )) })
965
1402
  ]
966
1403
  }
967
1404
  );
968
1405
  }
1406
+ function renderGroupedItems(items, activeIndex, depth, onSelect, onHover, renderItem, renderGroupHeader) {
1407
+ const elements = [];
1408
+ let lastGroup;
1409
+ items.forEach((item, index) => {
1410
+ if (item.group && item.group !== lastGroup) {
1411
+ lastGroup = item.group;
1412
+ elements.push(
1413
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-group-header": "", children: renderGroupHeader ? renderGroupHeader(item.group) : item.group }, `group-${item.group}`)
1414
+ );
1415
+ }
1416
+ elements.push(
1417
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1418
+ SuggestionItem,
1419
+ {
1420
+ item,
1421
+ index,
1422
+ isActive: index === activeIndex,
1423
+ depth,
1424
+ onSelect,
1425
+ onHover,
1426
+ renderItem
1427
+ },
1428
+ item.id
1429
+ )
1430
+ );
1431
+ });
1432
+ return elements;
1433
+ }
1434
+ function SuggestionItem({
1435
+ item,
1436
+ index,
1437
+ isActive,
1438
+ depth,
1439
+ onSelect,
1440
+ onHover,
1441
+ renderItem
1442
+ }) {
1443
+ const itemId = `mention-option-${item.id}`;
1444
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1445
+ "div",
1446
+ {
1447
+ ...optionAttrs(itemId, isActive, index),
1448
+ "data-suggestion-item": "",
1449
+ "data-suggestion-item-active": isActive ? "" : void 0,
1450
+ "data-has-children": item.hasChildren ? "" : void 0,
1451
+ onMouseEnter: () => onHover(index),
1452
+ onClick: () => onSelect(item),
1453
+ children: renderItem ? renderItem(item, depth) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultSuggestionItem, { item })
1454
+ }
1455
+ );
1456
+ }
969
1457
  function DefaultSuggestionItem({ item }) {
970
1458
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
971
1459
  item.icon && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-icon": "", children: item.icon }),
@@ -974,25 +1462,48 @@ function DefaultSuggestionItem({ item }) {
974
1462
  item.hasChildren && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-chevron": "", "aria-hidden": "true", children: "\u203A" })
975
1463
  ] });
976
1464
  }
1465
+ var POPOVER_HEIGHT_ESTIMATE = 280;
1466
+ var POPOVER_WIDTH_ESTIMATE = 360;
977
1467
  function usePopoverPosition(clientRect) {
978
1468
  if (!clientRect) {
979
- return { display: "none" };
1469
+ return { style: { display: "none" }, position: "below" };
980
1470
  }
981
1471
  const rect = clientRect();
982
1472
  if (!rect) {
983
- return { display: "none" };
1473
+ return { style: { display: "none" }, position: "below" };
1474
+ }
1475
+ const viewportH = typeof window !== "undefined" ? window.innerHeight : 800;
1476
+ const viewportW = typeof window !== "undefined" ? window.innerWidth : 1200;
1477
+ const spaceBelow = viewportH - rect.bottom;
1478
+ const shouldFlip = spaceBelow < POPOVER_HEIGHT_ESTIMATE && rect.top > spaceBelow;
1479
+ let left = rect.left;
1480
+ if (left + POPOVER_WIDTH_ESTIMATE > viewportW) {
1481
+ left = Math.max(0, viewportW - POPOVER_WIDTH_ESTIMATE);
1482
+ }
1483
+ if (shouldFlip) {
1484
+ return {
1485
+ style: {
1486
+ position: "fixed",
1487
+ left: `${left}px`,
1488
+ bottom: `${viewportH - rect.top + 4}px`,
1489
+ zIndex: 50
1490
+ },
1491
+ position: "above"
1492
+ };
984
1493
  }
985
1494
  return {
986
- position: "fixed",
987
- left: `${rect.left}px`,
988
- top: `${rect.bottom + 4}px`,
989
- zIndex: 50
1495
+ style: {
1496
+ position: "fixed",
1497
+ left: `${left}px`,
1498
+ top: `${rect.bottom + 4}px`,
1499
+ zIndex: 50
1500
+ },
1501
+ position: "below"
990
1502
  };
991
1503
  }
992
1504
 
993
1505
  // src/components/MentionsInput.tsx
994
1506
  var import_jsx_runtime2 = require("react/jsx-runtime");
995
- var LISTBOX_ID2 = "mentions-suggestion-listbox";
996
1507
  var MentionsInput = (0, import_react5.forwardRef)(
997
1508
  function MentionsInput2({
998
1509
  value,
@@ -1006,10 +1517,31 @@ var MentionsInput = (0, import_react5.forwardRef)(
1006
1517
  clearOnSubmit = true,
1007
1518
  maxLength,
1008
1519
  renderItem,
1009
- renderChip
1520
+ renderChip,
1521
+ renderEmpty,
1522
+ renderLoading,
1523
+ renderGroupHeader,
1524
+ onFocus,
1525
+ onBlur,
1526
+ onMentionAdd,
1527
+ onMentionRemove,
1528
+ onMentionClick,
1529
+ onMentionHover,
1530
+ minHeight,
1531
+ maxHeight,
1532
+ submitKey = "enter",
1533
+ allowTrigger,
1534
+ validateMention,
1535
+ portalContainer,
1536
+ streaming,
1537
+ onStreamingComplete
1010
1538
  }, ref) {
1011
- const { uiState, actions, callbacksRef } = useSuggestion(providers);
1012
- const { editor, clear, setContent, focus } = useMentionsEditor({
1539
+ const instanceId = (0, import_react5.useId)();
1540
+ const listboxId = `mentions-listbox-${instanceId}`;
1541
+ const { uiState, actions, callbacksRef } = useSuggestion(providers, {
1542
+ onMentionAdd
1543
+ });
1544
+ const { editor, getOutput, clear, setContent, appendText, focus } = useMentionsEditor({
1013
1545
  providers,
1014
1546
  value,
1015
1547
  onChange,
@@ -1018,46 +1550,72 @@ var MentionsInput = (0, import_react5.forwardRef)(
1018
1550
  placeholder,
1019
1551
  autoFocus,
1020
1552
  editable: !disabled,
1021
- callbacksRef
1553
+ callbacksRef,
1554
+ onFocus,
1555
+ onBlur,
1556
+ submitKey,
1557
+ onMentionRemove,
1558
+ onMentionClick,
1559
+ onMentionHover,
1560
+ allowTrigger,
1561
+ validateMention,
1562
+ streaming,
1563
+ onStreamingComplete
1022
1564
  });
1023
1565
  (0, import_react5.useImperativeHandle)(
1024
1566
  ref,
1025
- () => ({ clear, setContent, focus }),
1026
- [clear, setContent, focus]
1567
+ () => ({ clear, setContent, appendText, focus, getOutput }),
1568
+ [clear, setContent, appendText, focus, getOutput]
1027
1569
  );
1028
1570
  const isExpanded = uiState.state !== "idle";
1029
1571
  const handleHover = (0, import_react5.useCallback)((index) => {
1030
1572
  void index;
1031
1573
  }, []);
1574
+ const handleFocusEditor = (0, import_react5.useCallback)(() => {
1575
+ editor?.commands.focus();
1576
+ }, [editor]);
1577
+ const editorStyle = {};
1578
+ if (minHeight != null) editorStyle.minHeight = `${minHeight}px`;
1579
+ if (maxHeight != null) {
1580
+ editorStyle.maxHeight = `${maxHeight}px`;
1581
+ editorStyle.overflowY = "auto";
1582
+ }
1583
+ const suggestionList = isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1584
+ SuggestionList,
1585
+ {
1586
+ items: uiState.items,
1587
+ activeIndex: uiState.activeIndex,
1588
+ breadcrumbs: uiState.breadcrumbs,
1589
+ loading: uiState.loading,
1590
+ trigger: uiState.trigger,
1591
+ query: uiState.query,
1592
+ clientRect: uiState.clientRect,
1593
+ onSelect: (item) => actions.select(item),
1594
+ onHover: handleHover,
1595
+ onGoBack: actions.goBack,
1596
+ onSearchNested: actions.searchNested,
1597
+ onNavigateUp: actions.navigateUp,
1598
+ onNavigateDown: actions.navigateDown,
1599
+ onClose: actions.close,
1600
+ onFocusEditor: handleFocusEditor,
1601
+ renderItem,
1602
+ renderEmpty,
1603
+ renderLoading,
1604
+ renderGroupHeader,
1605
+ listboxId
1606
+ }
1607
+ ) : null;
1032
1608
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1033
1609
  "div",
1034
1610
  {
1035
1611
  className,
1036
1612
  "data-mentions-input": "",
1037
1613
  "data-disabled": disabled ? "" : void 0,
1038
- ...comboboxAttrs(isExpanded, LISTBOX_ID2),
1614
+ ...comboboxAttrs(isExpanded, listboxId),
1039
1615
  "aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
1040
1616
  children: [
1041
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react6.EditorContent, { editor }),
1042
- isExpanded && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1043
- SuggestionList,
1044
- {
1045
- items: uiState.items,
1046
- activeIndex: uiState.activeIndex,
1047
- breadcrumbs: uiState.breadcrumbs,
1048
- loading: uiState.loading,
1049
- trigger: uiState.trigger,
1050
- clientRect: uiState.clientRect,
1051
- onSelect: (item) => actions.select(item),
1052
- onHover: handleHover,
1053
- onGoBack: actions.goBack,
1054
- onSearchNested: actions.searchNested,
1055
- onNavigateUp: actions.navigateUp,
1056
- onNavigateDown: actions.navigateDown,
1057
- onClose: actions.close,
1058
- renderItem
1059
- }
1060
- )
1617
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: editorStyle, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react6.EditorContent, { editor }) }),
1618
+ portalContainer ? suggestionList && (0, import_react_dom.createPortal)(suggestionList, portalContainer) : suggestionList
1061
1619
  ]
1062
1620
  }
1063
1621
  );