@relevaince/mentions 0.3.3 → 0.5.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.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 { trigger, query, from: docStartPos + i, to: cursorPos };
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) {
116
187
  return Extension.create({
117
188
  name: "mentionSuggestion",
118
189
  priority: 200,
@@ -190,6 +261,20 @@ function createSuggestionExtension(triggers, callbacksRef) {
190
261
  const cursorPos = $pos.pos;
191
262
  const match = detectTrigger(blockText, cursorPos, blockStart, triggers);
192
263
  if (match) {
264
+ if (allowTriggerRef?.current) {
265
+ const allowed = allowTriggerRef.current(match.trigger, {
266
+ textBefore: match.textBefore
267
+ });
268
+ if (!allowed) {
269
+ if (active) {
270
+ active = false;
271
+ lastQuery = null;
272
+ lastTrigger = null;
273
+ callbacksRef.current.onExit();
274
+ }
275
+ return;
276
+ }
277
+ }
193
278
  const range = { from: match.from, to: match.to };
194
279
  const props = {
195
280
  query: match.query,
@@ -359,27 +444,48 @@ function buildOutput(editor) {
359
444
  plainText: extractPlainText(json)
360
445
  };
361
446
  }
362
- function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
447
+ function collectMentionTokens(doc) {
448
+ const tokens = [];
449
+ function walk(node) {
450
+ if (node.type === "mention" && node.attrs) {
451
+ tokens.push({
452
+ id: node.attrs.id,
453
+ type: node.attrs.entityType ?? node.attrs.type,
454
+ label: node.attrs.label
455
+ });
456
+ }
457
+ if (node.content) {
458
+ for (const child of node.content) walk(child);
459
+ }
460
+ }
461
+ walk(doc);
462
+ return tokens;
463
+ }
464
+ function createSubmitExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef) {
363
465
  return Extension2.create({
364
466
  name: "submitShortcut",
365
467
  priority: 150,
366
468
  addKeyboardShortcuts() {
367
469
  return {
368
470
  "Mod-Enter": () => {
369
- if (onSubmitRef.current) {
370
- onSubmitRef.current(buildOutput(this.editor));
371
- if (clearOnSubmitRef.current) {
372
- this.editor.commands.clearContent(true);
471
+ const key = submitKeyRef.current;
472
+ if (key === "mod+enter" || key === "enter") {
473
+ if (onSubmitRef.current) {
474
+ onSubmitRef.current(buildOutput(this.editor));
475
+ if (clearOnSubmitRef.current) {
476
+ this.editor.commands.clearContent(true);
477
+ }
373
478
  }
479
+ return true;
374
480
  }
375
- return true;
481
+ return false;
376
482
  }
377
483
  };
378
484
  }
379
485
  });
380
486
  }
381
487
  var enterSubmitPluginKey = new PluginKey2("enterSubmit");
382
- function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
488
+ function createEnterExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef) {
383
489
  return Extension2.create({
384
490
  name: "enterSubmit",
385
491
  priority: 150,
@@ -391,6 +497,11 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
391
497
  props: {
392
498
  handleKeyDown(_view, event) {
393
499
  if (event.key !== "Enter") return false;
500
+ const key = submitKeyRef.current;
501
+ if (key === "none") return false;
502
+ if (key === "mod+enter") {
503
+ return false;
504
+ }
394
505
  if (event.shiftKey) {
395
506
  editor.commands.splitBlock();
396
507
  return true;
@@ -410,6 +521,33 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
410
521
  }
411
522
  });
412
523
  }
524
+ var mentionRemovePluginKey = new PluginKey2("mentionRemove");
525
+ function createMentionRemoveExtension(onMentionRemoveRef) {
526
+ return Extension2.create({
527
+ name: "mentionRemoveDetector",
528
+ priority: 100,
529
+ addProseMirrorPlugins() {
530
+ return [
531
+ new Plugin2({
532
+ key: mentionRemovePluginKey,
533
+ appendTransaction(transactions, oldState, newState) {
534
+ if (!onMentionRemoveRef.current) return null;
535
+ const oldMentions = collectMentionTokens(oldState.doc.toJSON());
536
+ const newMentions = collectMentionTokens(newState.doc.toJSON());
537
+ if (oldMentions.length <= newMentions.length) return null;
538
+ const newIds = new Set(newMentions.map((m) => m.id));
539
+ for (const m of oldMentions) {
540
+ if (!newIds.has(m.id)) {
541
+ onMentionRemoveRef.current(m);
542
+ }
543
+ }
544
+ return null;
545
+ }
546
+ })
547
+ ];
548
+ }
549
+ });
550
+ }
413
551
  function useMentionsEditor({
414
552
  providers,
415
553
  value,
@@ -419,7 +557,15 @@ function useMentionsEditor({
419
557
  placeholder,
420
558
  autoFocus = false,
421
559
  editable = true,
422
- callbacksRef
560
+ callbacksRef,
561
+ onFocus,
562
+ onBlur,
563
+ submitKey = "enter",
564
+ onMentionRemove,
565
+ onMentionClick,
566
+ onMentionHover,
567
+ allowTrigger,
568
+ validateMention
423
569
  }) {
424
570
  const onChangeRef = useRef(onChange);
425
571
  onChangeRef.current = onChange;
@@ -427,6 +573,23 @@ function useMentionsEditor({
427
573
  onSubmitRef.current = onSubmit;
428
574
  const clearOnSubmitRef = useRef(clearOnSubmit);
429
575
  clearOnSubmitRef.current = clearOnSubmit;
576
+ const onFocusRef = useRef(onFocus);
577
+ onFocusRef.current = onFocus;
578
+ const onBlurRef = useRef(onBlur);
579
+ onBlurRef.current = onBlur;
580
+ const submitKeyRef = useRef(submitKey);
581
+ submitKeyRef.current = submitKey;
582
+ const onMentionRemoveRef = useRef(onMentionRemove);
583
+ onMentionRemoveRef.current = onMentionRemove;
584
+ const onMentionClickRef = useRef(onMentionClick);
585
+ onMentionClickRef.current = onMentionClick;
586
+ const onMentionHoverRef = useRef(onMentionHover);
587
+ onMentionHoverRef.current = onMentionHover;
588
+ const allowTriggerRef = useRef(allowTrigger);
589
+ allowTriggerRef.current = allowTrigger;
590
+ const validateMentionRef = useRef(validateMention);
591
+ validateMentionRef.current = validateMention;
592
+ const internalMarkdownRef = useRef(null);
430
593
  const initialContent = useMemo(() => {
431
594
  if (!value) return void 0;
432
595
  return parseFromMarkdown(value);
@@ -438,16 +601,27 @@ function useMentionsEditor({
438
601
  [triggersKey]
439
602
  );
440
603
  const suggestionExtension = useMemo(
441
- () => createSuggestionExtension(triggers, callbacksRef),
604
+ () => createSuggestionExtension(triggers, callbacksRef, allowTriggerRef),
442
605
  // eslint-disable-next-line react-hooks/exhaustive-deps
443
606
  [triggersKey]
444
607
  );
445
608
  const submitExt = useMemo(
446
- () => createSubmitExtension(onSubmitRef, clearOnSubmitRef),
609
+ () => createSubmitExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef),
447
610
  []
448
611
  );
449
612
  const enterExt = useMemo(
450
- () => createEnterExtension(onSubmitRef, clearOnSubmitRef),
613
+ () => createEnterExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef),
614
+ []
615
+ );
616
+ const mentionRemoveExt = useMemo(
617
+ () => createMentionRemoveExtension(onMentionRemoveRef),
618
+ []
619
+ );
620
+ const mentionNodeExt = useMemo(
621
+ () => MentionNode.configure({
622
+ onClickRef: onMentionClickRef,
623
+ onHoverRef: onMentionHoverRef
624
+ }),
451
625
  []
452
626
  );
453
627
  const editor = useEditor({
@@ -466,10 +640,11 @@ function useMentionsEditor({
466
640
  placeholder: ({ editor: editor2 }) => editor2.isEmpty ? placeholder ?? "Type a message..." : "",
467
641
  showOnlyCurrent: true
468
642
  }),
469
- MentionNode,
643
+ mentionNodeExt,
470
644
  suggestionExtension,
471
645
  submitExt,
472
- enterExt
646
+ enterExt,
647
+ mentionRemoveExt
473
648
  ],
474
649
  content: initialContent,
475
650
  autofocus: autoFocus ? "end" : false,
@@ -480,7 +655,15 @@ function useMentionsEditor({
480
655
  }
481
656
  },
482
657
  onUpdate: ({ editor: editor2 }) => {
483
- onChangeRef.current?.(buildOutput(editor2));
658
+ const output = buildOutput(editor2);
659
+ internalMarkdownRef.current = output.markdown;
660
+ onChangeRef.current?.(output);
661
+ },
662
+ onFocus: () => {
663
+ onFocusRef.current?.();
664
+ },
665
+ onBlur: () => {
666
+ onBlurRef.current?.();
484
667
  }
485
668
  });
486
669
  useEffect(() => {
@@ -488,6 +671,36 @@ function useMentionsEditor({
488
671
  editor.setEditable(editable);
489
672
  }
490
673
  }, [editor, editable]);
674
+ useEffect(() => {
675
+ if (!editor || value === void 0) return;
676
+ if (value === internalMarkdownRef.current) return;
677
+ const doc = parseFromMarkdown(value);
678
+ editor.commands.setContent(doc);
679
+ internalMarkdownRef.current = value;
680
+ }, [editor, value]);
681
+ useEffect(() => {
682
+ if (!editor || !validateMention) return;
683
+ const runValidation = async () => {
684
+ const doc = editor.getJSON();
685
+ const tokens = collectMentionTokens(doc);
686
+ const invalidIds = /* @__PURE__ */ new Set();
687
+ await Promise.all(
688
+ tokens.map(async (token) => {
689
+ const valid = await validateMention(token);
690
+ if (!valid) invalidIds.add(token.id);
691
+ })
692
+ );
693
+ editor.view.dom.querySelectorAll("[data-mention]").forEach((el) => {
694
+ const id = el.getAttribute("data-id");
695
+ if (id && invalidIds.has(id)) {
696
+ el.setAttribute("data-mention-invalid", "");
697
+ } else {
698
+ el.removeAttribute("data-mention-invalid");
699
+ }
700
+ });
701
+ };
702
+ runValidation();
703
+ }, [editor, validateMention]);
491
704
  const clear = useCallback(() => {
492
705
  editor?.commands.clearContent(true);
493
706
  }, [editor]);
@@ -496,6 +709,7 @@ function useMentionsEditor({
496
709
  if (!editor) return;
497
710
  const doc = parseFromMarkdown(markdown);
498
711
  editor.commands.setContent(doc);
712
+ internalMarkdownRef.current = markdown;
499
713
  },
500
714
  [editor]
501
715
  );
@@ -510,7 +724,28 @@ function useMentionsEditor({
510
724
  }
511
725
 
512
726
  // src/hooks/useSuggestion.ts
513
- import { useCallback as useCallback2, useRef as useRef2, useState } from "react";
727
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState } from "react";
728
+
729
+ // src/utils/debounce.ts
730
+ function debounce(fn, ms) {
731
+ let timer = null;
732
+ const debounced = ((...args) => {
733
+ if (timer != null) clearTimeout(timer);
734
+ timer = setTimeout(() => {
735
+ timer = null;
736
+ fn(...args);
737
+ }, ms);
738
+ });
739
+ debounced.cancel = () => {
740
+ if (timer != null) {
741
+ clearTimeout(timer);
742
+ timer = null;
743
+ }
744
+ };
745
+ return debounced;
746
+ }
747
+
748
+ // src/hooks/useSuggestion.ts
514
749
  var IDLE_STATE = {
515
750
  state: "idle",
516
751
  items: [],
@@ -521,16 +756,24 @@ var IDLE_STATE = {
521
756
  trigger: null,
522
757
  query: ""
523
758
  };
524
- function useSuggestion(providers) {
759
+ function useSuggestion(providers, options = {}) {
525
760
  const [uiState, setUIState] = useState(IDLE_STATE);
526
761
  const stateRef = useRef2(uiState);
527
762
  stateRef.current = uiState;
528
763
  const providersRef = useRef2(providers);
529
764
  providersRef.current = providers;
765
+ const onMentionAddRef = useRef2(options.onMentionAdd);
766
+ onMentionAddRef.current = options.onMentionAdd;
530
767
  const commandRef = useRef2(
531
768
  null
532
769
  );
533
770
  const providerRef = useRef2(null);
771
+ const debouncedFetchRef = useRef2(null);
772
+ useEffect2(() => {
773
+ return () => {
774
+ debouncedFetchRef.current?.cancel();
775
+ };
776
+ }, []);
534
777
  const fetchItems = useCallback2(
535
778
  async (provider, query, parent, useSearchAll) => {
536
779
  setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
@@ -561,6 +804,23 @@ function useSuggestion(providers) {
561
804
  },
562
805
  []
563
806
  );
807
+ const scheduleFetch = useCallback2(
808
+ (provider, query, parent, useSearchAll) => {
809
+ debouncedFetchRef.current?.cancel();
810
+ const ms = provider.debounceMs;
811
+ if (ms && ms > 0) {
812
+ setUIState((prev) => ({ ...prev, loading: true }));
813
+ const debouncedFn = debounce(() => {
814
+ fetchItems(provider, query, parent, useSearchAll);
815
+ }, ms);
816
+ debouncedFetchRef.current = debouncedFn;
817
+ debouncedFn();
818
+ } else {
819
+ fetchItems(provider, query, parent, useSearchAll);
820
+ }
821
+ },
822
+ [fetchItems]
823
+ );
564
824
  const onStart = useCallback2(
565
825
  (props) => {
566
826
  const provider = providersRef.current.find(
@@ -579,13 +839,31 @@ function useSuggestion(providers) {
579
839
  trigger: props.trigger,
580
840
  query: props.query
581
841
  });
842
+ if (!props.query.trim() && provider.getRecentItems) {
843
+ provider.getRecentItems().then((recentItems) => {
844
+ const tagged = recentItems.map((item) => ({
845
+ ...item,
846
+ group: item.group ?? "Recent"
847
+ }));
848
+ setUIState((prev) => ({
849
+ ...prev,
850
+ items: tagged,
851
+ loading: false,
852
+ state: "showing",
853
+ activeIndex: 0
854
+ }));
855
+ }).catch(() => {
856
+ scheduleFetch(provider, props.query);
857
+ });
858
+ return;
859
+ }
582
860
  if (props.query.trim() && provider.searchAll) {
583
- fetchItems(provider, props.query, void 0, true);
861
+ scheduleFetch(provider, props.query, void 0, true);
584
862
  } else {
585
- fetchItems(provider, props.query);
863
+ scheduleFetch(provider, props.query);
586
864
  }
587
865
  },
588
- [fetchItems]
866
+ [scheduleFetch]
589
867
  );
590
868
  const onUpdate = useCallback2(
591
869
  (props) => {
@@ -609,14 +887,31 @@ function useSuggestion(providers) {
609
887
  }));
610
888
  }
611
889
  if (props.query.trim() && provider.searchAll) {
612
- fetchItems(provider, props.query, void 0, true);
890
+ scheduleFetch(provider, props.query, void 0, true);
891
+ } else if (!props.query.trim() && provider.getRecentItems) {
892
+ provider.getRecentItems().then((recentItems) => {
893
+ const tagged = recentItems.map((item) => ({
894
+ ...item,
895
+ group: item.group ?? "Recent"
896
+ }));
897
+ setUIState((prev) => ({
898
+ ...prev,
899
+ items: tagged,
900
+ loading: false,
901
+ state: "showing",
902
+ activeIndex: 0
903
+ }));
904
+ }).catch(() => {
905
+ scheduleFetch(provider, props.query);
906
+ });
613
907
  } else {
614
- fetchItems(provider, props.query);
908
+ scheduleFetch(provider, props.query);
615
909
  }
616
910
  },
617
- [fetchItems]
911
+ [scheduleFetch]
618
912
  );
619
913
  const onExit = useCallback2(() => {
914
+ debouncedFetchRef.current?.cancel();
620
915
  providerRef.current = null;
621
916
  commandRef.current = null;
622
917
  setUIState(IDLE_STATE);
@@ -651,8 +946,8 @@ function useSuggestion(providers) {
651
946
  fetchItems(provider, "", selected);
652
947
  return;
653
948
  }
949
+ const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
654
950
  if (commandRef.current) {
655
- const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
656
951
  commandRef.current({
657
952
  id: selected.id,
658
953
  label: selected.label,
@@ -660,6 +955,11 @@ function useSuggestion(providers) {
660
955
  rootLabel
661
956
  });
662
957
  }
958
+ onMentionAddRef.current?.({
959
+ id: selected.id,
960
+ type: selected.type,
961
+ label: selected.label
962
+ });
663
963
  },
664
964
  [fetchItems]
665
965
  );
@@ -684,6 +984,7 @@ function useSuggestion(providers) {
684
984
  });
685
985
  }, [fetchItems]);
686
986
  const close = useCallback2(() => {
987
+ debouncedFetchRef.current?.cancel();
687
988
  setUIState(IDLE_STATE);
688
989
  }, []);
689
990
  const searchNested = useCallback2(
@@ -693,10 +994,10 @@ function useSuggestion(providers) {
693
994
  const current = stateRef.current;
694
995
  const parent = current.breadcrumbs[current.breadcrumbs.length - 1];
695
996
  if (parent) {
696
- fetchItems(provider, query, parent);
997
+ scheduleFetch(provider, query, parent);
697
998
  }
698
999
  },
699
- [fetchItems]
1000
+ [scheduleFetch]
700
1001
  );
701
1002
  const onKeyDown = useCallback2(
702
1003
  ({ event }) => {
@@ -719,6 +1020,14 @@ function useSuggestion(providers) {
719
1020
  }
720
1021
  return true;
721
1022
  }
1023
+ case "Tab": {
1024
+ event.preventDefault();
1025
+ const selectedItem = current.items[current.activeIndex];
1026
+ if (selectedItem) {
1027
+ select(selectedItem);
1028
+ }
1029
+ return true;
1030
+ }
722
1031
  case "ArrowRight": {
723
1032
  const activeItem = current.items[current.activeIndex];
724
1033
  if (activeItem?.hasChildren) {
@@ -764,7 +1073,7 @@ function useSuggestion(providers) {
764
1073
  }
765
1074
 
766
1075
  // src/components/SuggestionList.tsx
767
- import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
1076
+ import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
768
1077
 
769
1078
  // src/utils/ariaHelpers.ts
770
1079
  function comboboxAttrs(expanded, listboxId) {
@@ -793,13 +1102,13 @@ function optionAttrs(id, selected, index) {
793
1102
 
794
1103
  // src/components/SuggestionList.tsx
795
1104
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
796
- var LISTBOX_ID = "mentions-suggestion-listbox";
797
1105
  function SuggestionList({
798
1106
  items,
799
1107
  activeIndex,
800
1108
  breadcrumbs,
801
1109
  loading,
802
1110
  trigger,
1111
+ query,
803
1112
  clientRect,
804
1113
  onSelect,
805
1114
  onHover,
@@ -808,7 +1117,12 @@ function SuggestionList({
808
1117
  onNavigateUp,
809
1118
  onNavigateDown,
810
1119
  onClose,
811
- renderItem
1120
+ onFocusEditor,
1121
+ renderItem,
1122
+ renderEmpty,
1123
+ renderLoading,
1124
+ renderGroupHeader,
1125
+ listboxId
812
1126
  }) {
813
1127
  const listRef = useRef3(null);
814
1128
  const searchInputRef = useRef3(null);
@@ -816,24 +1130,30 @@ function SuggestionList({
816
1130
  const [nestedQuery, setNestedQuery] = useState2("");
817
1131
  const breadcrumbKey = breadcrumbs.map((b) => b.id).join("/");
818
1132
  const prevBreadcrumbKey = useRef3(breadcrumbKey);
819
- useEffect2(() => {
1133
+ useEffect3(() => {
820
1134
  if (prevBreadcrumbKey.current !== breadcrumbKey) {
821
1135
  setNestedQuery("");
822
1136
  prevBreadcrumbKey.current = breadcrumbKey;
823
1137
  }
824
1138
  }, [breadcrumbKey]);
825
- useEffect2(() => {
826
- if (breadcrumbs.length > 0 && searchInputRef.current) {
1139
+ const prevBreadcrumbsLen = useRef3(breadcrumbs.length);
1140
+ useEffect3(() => {
1141
+ if (prevBreadcrumbsLen.current > 0 && breadcrumbs.length === 0) {
1142
+ onFocusEditor?.();
1143
+ } else if (breadcrumbs.length > 0 && searchInputRef.current) {
827
1144
  requestAnimationFrame(() => searchInputRef.current?.focus());
828
1145
  }
829
- }, [breadcrumbKey, breadcrumbs.length]);
830
- useEffect2(() => {
1146
+ prevBreadcrumbsLen.current = breadcrumbs.length;
1147
+ }, [breadcrumbKey, breadcrumbs.length, onFocusEditor]);
1148
+ useEffect3(() => {
831
1149
  if (!listRef.current) return;
832
1150
  const active = listRef.current.querySelector('[aria-selected="true"]');
833
1151
  active?.scrollIntoView({ block: "nearest" });
834
1152
  }, [activeIndex]);
835
- const style = usePopoverPosition(clientRect);
836
- if (items.length === 0 && !loading) return null;
1153
+ const { style, position } = usePopoverPosition(clientRect);
1154
+ const activeQuery = breadcrumbs.length > 0 ? nestedQuery : query;
1155
+ const showEmpty = !loading && items.length === 0 && activeQuery.trim().length > 0;
1156
+ if (items.length === 0 && !loading && !showEmpty) return null;
837
1157
  const handleSearchKeyDown = (e) => {
838
1158
  switch (e.key) {
839
1159
  case "ArrowDown":
@@ -853,8 +1173,23 @@ function SuggestionList({
853
1173
  }
854
1174
  case "Escape":
855
1175
  e.preventDefault();
1176
+ onFocusEditor?.();
856
1177
  onClose?.();
857
1178
  break;
1179
+ case "ArrowLeft":
1180
+ if (nestedQuery === "" || e.currentTarget.selectionStart === 0) {
1181
+ e.preventDefault();
1182
+ onGoBack();
1183
+ }
1184
+ break;
1185
+ case "ArrowRight": {
1186
+ const item = items[activeIndex];
1187
+ if (item?.hasChildren) {
1188
+ e.preventDefault();
1189
+ onSelect(item);
1190
+ }
1191
+ break;
1192
+ }
858
1193
  case "Backspace":
859
1194
  if (nestedQuery === "") {
860
1195
  e.preventDefault();
@@ -863,11 +1198,13 @@ function SuggestionList({
863
1198
  break;
864
1199
  }
865
1200
  };
1201
+ const hasGroups = items.some((item) => item.group);
866
1202
  return /* @__PURE__ */ jsxs(
867
1203
  "div",
868
1204
  {
869
1205
  "data-suggestions": "",
870
1206
  "data-trigger": trigger,
1207
+ "data-suggestions-position": position,
871
1208
  style,
872
1209
  ref: listRef,
873
1210
  children: [
@@ -905,28 +1242,76 @@ function SuggestionList({
905
1242
  spellCheck: false
906
1243
  }
907
1244
  ) }),
908
- loading && /* @__PURE__ */ jsx("div", { "data-suggestion-loading": "", children: "Loading..." }),
909
- !loading && /* @__PURE__ */ jsx("div", { ...listboxAttrs(LISTBOX_ID, `${trigger ?? ""} suggestions`), children: items.map((item, index) => {
910
- const isActive = index === activeIndex;
911
- const itemId = `mention-option-${item.id}`;
912
- return /* @__PURE__ */ jsx(
913
- "div",
914
- {
915
- ...optionAttrs(itemId, isActive, index),
916
- "data-suggestion-item": "",
917
- "data-suggestion-item-active": isActive ? "" : void 0,
918
- "data-has-children": item.hasChildren ? "" : void 0,
919
- onMouseEnter: () => onHover(index),
920
- onClick: () => onSelect(item),
921
- children: renderItem ? renderItem(item, depth) : /* @__PURE__ */ jsx(DefaultSuggestionItem, { item })
922
- },
923
- item.id
924
- );
925
- }) })
1245
+ loading && /* @__PURE__ */ jsx("div", { "data-suggestion-loading": "", children: renderLoading ? renderLoading() : "Loading..." }),
1246
+ showEmpty && /* @__PURE__ */ jsx("div", { "data-suggestion-empty": "", children: renderEmpty ? renderEmpty(activeQuery) : "No results" }),
1247
+ !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(
1248
+ SuggestionItem,
1249
+ {
1250
+ item,
1251
+ index,
1252
+ isActive: index === activeIndex,
1253
+ depth,
1254
+ onSelect,
1255
+ onHover,
1256
+ renderItem
1257
+ },
1258
+ item.id
1259
+ )) })
926
1260
  ]
927
1261
  }
928
1262
  );
929
1263
  }
1264
+ function renderGroupedItems(items, activeIndex, depth, onSelect, onHover, renderItem, renderGroupHeader) {
1265
+ const elements = [];
1266
+ let lastGroup;
1267
+ items.forEach((item, index) => {
1268
+ if (item.group && item.group !== lastGroup) {
1269
+ lastGroup = item.group;
1270
+ elements.push(
1271
+ /* @__PURE__ */ jsx("div", { "data-suggestion-group-header": "", children: renderGroupHeader ? renderGroupHeader(item.group) : item.group }, `group-${item.group}`)
1272
+ );
1273
+ }
1274
+ elements.push(
1275
+ /* @__PURE__ */ jsx(
1276
+ SuggestionItem,
1277
+ {
1278
+ item,
1279
+ index,
1280
+ isActive: index === activeIndex,
1281
+ depth,
1282
+ onSelect,
1283
+ onHover,
1284
+ renderItem
1285
+ },
1286
+ item.id
1287
+ )
1288
+ );
1289
+ });
1290
+ return elements;
1291
+ }
1292
+ function SuggestionItem({
1293
+ item,
1294
+ index,
1295
+ isActive,
1296
+ depth,
1297
+ onSelect,
1298
+ onHover,
1299
+ renderItem
1300
+ }) {
1301
+ const itemId = `mention-option-${item.id}`;
1302
+ return /* @__PURE__ */ jsx(
1303
+ "div",
1304
+ {
1305
+ ...optionAttrs(itemId, isActive, index),
1306
+ "data-suggestion-item": "",
1307
+ "data-suggestion-item-active": isActive ? "" : void 0,
1308
+ "data-has-children": item.hasChildren ? "" : void 0,
1309
+ onMouseEnter: () => onHover(index),
1310
+ onClick: () => onSelect(item),
1311
+ children: renderItem ? renderItem(item, depth) : /* @__PURE__ */ jsx(DefaultSuggestionItem, { item })
1312
+ }
1313
+ );
1314
+ }
930
1315
  function DefaultSuggestionItem({ item }) {
931
1316
  return /* @__PURE__ */ jsxs(Fragment, { children: [
932
1317
  item.icon && /* @__PURE__ */ jsx("span", { "data-suggestion-item-icon": "", children: item.icon }),
@@ -935,25 +1320,48 @@ function DefaultSuggestionItem({ item }) {
935
1320
  item.hasChildren && /* @__PURE__ */ jsx("span", { "data-suggestion-item-chevron": "", "aria-hidden": "true", children: "\u203A" })
936
1321
  ] });
937
1322
  }
1323
+ var POPOVER_HEIGHT_ESTIMATE = 280;
1324
+ var POPOVER_WIDTH_ESTIMATE = 360;
938
1325
  function usePopoverPosition(clientRect) {
939
1326
  if (!clientRect) {
940
- return { display: "none" };
1327
+ return { style: { display: "none" }, position: "below" };
941
1328
  }
942
1329
  const rect = clientRect();
943
1330
  if (!rect) {
944
- return { display: "none" };
1331
+ return { style: { display: "none" }, position: "below" };
1332
+ }
1333
+ const viewportH = typeof window !== "undefined" ? window.innerHeight : 800;
1334
+ const viewportW = typeof window !== "undefined" ? window.innerWidth : 1200;
1335
+ const spaceBelow = viewportH - rect.bottom;
1336
+ const shouldFlip = spaceBelow < POPOVER_HEIGHT_ESTIMATE && rect.top > spaceBelow;
1337
+ let left = rect.left;
1338
+ if (left + POPOVER_WIDTH_ESTIMATE > viewportW) {
1339
+ left = Math.max(0, viewportW - POPOVER_WIDTH_ESTIMATE);
1340
+ }
1341
+ if (shouldFlip) {
1342
+ return {
1343
+ style: {
1344
+ position: "fixed",
1345
+ left: `${left}px`,
1346
+ bottom: `${viewportH - rect.top + 4}px`,
1347
+ zIndex: 50
1348
+ },
1349
+ position: "above"
1350
+ };
945
1351
  }
946
1352
  return {
947
- position: "fixed",
948
- left: `${rect.left}px`,
949
- top: `${rect.bottom + 4}px`,
950
- zIndex: 50
1353
+ style: {
1354
+ position: "fixed",
1355
+ left: `${left}px`,
1356
+ top: `${rect.bottom + 4}px`,
1357
+ zIndex: 50
1358
+ },
1359
+ position: "below"
951
1360
  };
952
1361
  }
953
1362
 
954
1363
  // src/components/MentionsInput.tsx
955
1364
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
956
- var LISTBOX_ID2 = "mentions-suggestion-listbox";
957
1365
  var MentionsInput = forwardRef(
958
1366
  function MentionsInput2({
959
1367
  value,
@@ -967,10 +1375,29 @@ var MentionsInput = forwardRef(
967
1375
  clearOnSubmit = true,
968
1376
  maxLength,
969
1377
  renderItem,
970
- renderChip
1378
+ renderChip,
1379
+ renderEmpty,
1380
+ renderLoading,
1381
+ renderGroupHeader,
1382
+ onFocus,
1383
+ onBlur,
1384
+ onMentionAdd,
1385
+ onMentionRemove,
1386
+ onMentionClick,
1387
+ onMentionHover,
1388
+ minHeight,
1389
+ maxHeight,
1390
+ submitKey = "enter",
1391
+ allowTrigger,
1392
+ validateMention,
1393
+ portalContainer
971
1394
  }, ref) {
972
- const { uiState, actions, callbacksRef } = useSuggestion(providers);
973
- const { editor, clear, setContent, focus } = useMentionsEditor({
1395
+ const instanceId = useId();
1396
+ const listboxId = `mentions-listbox-${instanceId}`;
1397
+ const { uiState, actions, callbacksRef } = useSuggestion(providers, {
1398
+ onMentionAdd
1399
+ });
1400
+ const { editor, getOutput, clear, setContent, focus } = useMentionsEditor({
974
1401
  providers,
975
1402
  value,
976
1403
  onChange,
@@ -979,46 +1406,70 @@ var MentionsInput = forwardRef(
979
1406
  placeholder,
980
1407
  autoFocus,
981
1408
  editable: !disabled,
982
- callbacksRef
1409
+ callbacksRef,
1410
+ onFocus,
1411
+ onBlur,
1412
+ submitKey,
1413
+ onMentionRemove,
1414
+ onMentionClick,
1415
+ onMentionHover,
1416
+ allowTrigger,
1417
+ validateMention
983
1418
  });
984
1419
  useImperativeHandle(
985
1420
  ref,
986
- () => ({ clear, setContent, focus }),
987
- [clear, setContent, focus]
1421
+ () => ({ clear, setContent, focus, getOutput }),
1422
+ [clear, setContent, focus, getOutput]
988
1423
  );
989
1424
  const isExpanded = uiState.state !== "idle";
990
1425
  const handleHover = useCallback3((index) => {
991
1426
  void index;
992
1427
  }, []);
1428
+ const handleFocusEditor = useCallback3(() => {
1429
+ editor?.commands.focus();
1430
+ }, [editor]);
1431
+ const editorStyle = {};
1432
+ if (minHeight != null) editorStyle.minHeight = `${minHeight}px`;
1433
+ if (maxHeight != null) {
1434
+ editorStyle.maxHeight = `${maxHeight}px`;
1435
+ editorStyle.overflowY = "auto";
1436
+ }
1437
+ const suggestionList = isExpanded ? /* @__PURE__ */ jsx2(
1438
+ SuggestionList,
1439
+ {
1440
+ items: uiState.items,
1441
+ activeIndex: uiState.activeIndex,
1442
+ breadcrumbs: uiState.breadcrumbs,
1443
+ loading: uiState.loading,
1444
+ trigger: uiState.trigger,
1445
+ query: uiState.query,
1446
+ clientRect: uiState.clientRect,
1447
+ onSelect: (item) => actions.select(item),
1448
+ onHover: handleHover,
1449
+ onGoBack: actions.goBack,
1450
+ onSearchNested: actions.searchNested,
1451
+ onNavigateUp: actions.navigateUp,
1452
+ onNavigateDown: actions.navigateDown,
1453
+ onClose: actions.close,
1454
+ onFocusEditor: handleFocusEditor,
1455
+ renderItem,
1456
+ renderEmpty,
1457
+ renderLoading,
1458
+ renderGroupHeader,
1459
+ listboxId
1460
+ }
1461
+ ) : null;
993
1462
  return /* @__PURE__ */ jsxs2(
994
1463
  "div",
995
1464
  {
996
1465
  className,
997
1466
  "data-mentions-input": "",
998
1467
  "data-disabled": disabled ? "" : void 0,
999
- ...comboboxAttrs(isExpanded, LISTBOX_ID2),
1468
+ ...comboboxAttrs(isExpanded, listboxId),
1000
1469
  "aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
1001
1470
  children: [
1002
- /* @__PURE__ */ jsx2(EditorContent, { editor }),
1003
- isExpanded && /* @__PURE__ */ jsx2(
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
- )
1471
+ /* @__PURE__ */ jsx2("div", { style: editorStyle, children: /* @__PURE__ */ jsx2(EditorContent, { editor }) }),
1472
+ portalContainer ? suggestionList && createPortal(suggestionList, portalContainer) : suggestionList
1022
1473
  ]
1023
1474
  }
1024
1475
  );