@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.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) {
155
226
  return import_core2.Extension.create({
156
227
  name: "mentionSuggestion",
157
228
  priority: 200,
@@ -229,6 +300,20 @@ function createSuggestionExtension(triggers, callbacksRef) {
229
300
  const cursorPos = $pos.pos;
230
301
  const match = detectTrigger(blockText, cursorPos, blockStart, triggers);
231
302
  if (match) {
303
+ if (allowTriggerRef?.current) {
304
+ const allowed = allowTriggerRef.current(match.trigger, {
305
+ textBefore: match.textBefore
306
+ });
307
+ if (!allowed) {
308
+ if (active) {
309
+ active = false;
310
+ lastQuery = null;
311
+ lastTrigger = null;
312
+ callbacksRef.current.onExit();
313
+ }
314
+ return;
315
+ }
316
+ }
232
317
  const range = { from: match.from, to: match.to };
233
318
  const props = {
234
319
  query: match.query,
@@ -398,27 +483,48 @@ function buildOutput(editor) {
398
483
  plainText: extractPlainText(json)
399
484
  };
400
485
  }
401
- function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
486
+ function collectMentionTokens(doc) {
487
+ const tokens = [];
488
+ function walk(node) {
489
+ if (node.type === "mention" && node.attrs) {
490
+ tokens.push({
491
+ id: node.attrs.id,
492
+ type: node.attrs.entityType ?? node.attrs.type,
493
+ label: node.attrs.label
494
+ });
495
+ }
496
+ if (node.content) {
497
+ for (const child of node.content) walk(child);
498
+ }
499
+ }
500
+ walk(doc);
501
+ return tokens;
502
+ }
503
+ function createSubmitExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef) {
402
504
  return import_core3.Extension.create({
403
505
  name: "submitShortcut",
404
506
  priority: 150,
405
507
  addKeyboardShortcuts() {
406
508
  return {
407
509
  "Mod-Enter": () => {
408
- if (onSubmitRef.current) {
409
- onSubmitRef.current(buildOutput(this.editor));
410
- if (clearOnSubmitRef.current) {
411
- this.editor.commands.clearContent(true);
510
+ const key = submitKeyRef.current;
511
+ if (key === "mod+enter" || key === "enter") {
512
+ if (onSubmitRef.current) {
513
+ onSubmitRef.current(buildOutput(this.editor));
514
+ if (clearOnSubmitRef.current) {
515
+ this.editor.commands.clearContent(true);
516
+ }
412
517
  }
518
+ return true;
413
519
  }
414
- return true;
520
+ return false;
415
521
  }
416
522
  };
417
523
  }
418
524
  });
419
525
  }
420
526
  var enterSubmitPluginKey = new import_state2.PluginKey("enterSubmit");
421
- function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
527
+ function createEnterExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef) {
422
528
  return import_core3.Extension.create({
423
529
  name: "enterSubmit",
424
530
  priority: 150,
@@ -430,6 +536,11 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
430
536
  props: {
431
537
  handleKeyDown(_view, event) {
432
538
  if (event.key !== "Enter") return false;
539
+ const key = submitKeyRef.current;
540
+ if (key === "none") return false;
541
+ if (key === "mod+enter") {
542
+ return false;
543
+ }
433
544
  if (event.shiftKey) {
434
545
  editor.commands.splitBlock();
435
546
  return true;
@@ -449,6 +560,33 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
449
560
  }
450
561
  });
451
562
  }
563
+ var mentionRemovePluginKey = new import_state2.PluginKey("mentionRemove");
564
+ function createMentionRemoveExtension(onMentionRemoveRef) {
565
+ return import_core3.Extension.create({
566
+ name: "mentionRemoveDetector",
567
+ priority: 100,
568
+ addProseMirrorPlugins() {
569
+ return [
570
+ new import_state2.Plugin({
571
+ key: mentionRemovePluginKey,
572
+ appendTransaction(transactions, oldState, newState) {
573
+ if (!onMentionRemoveRef.current) return null;
574
+ const oldMentions = collectMentionTokens(oldState.doc.toJSON());
575
+ const newMentions = collectMentionTokens(newState.doc.toJSON());
576
+ if (oldMentions.length <= newMentions.length) return null;
577
+ const newIds = new Set(newMentions.map((m) => m.id));
578
+ for (const m of oldMentions) {
579
+ if (!newIds.has(m.id)) {
580
+ onMentionRemoveRef.current(m);
581
+ }
582
+ }
583
+ return null;
584
+ }
585
+ })
586
+ ];
587
+ }
588
+ });
589
+ }
452
590
  function useMentionsEditor({
453
591
  providers,
454
592
  value,
@@ -458,7 +596,15 @@ function useMentionsEditor({
458
596
  placeholder,
459
597
  autoFocus = false,
460
598
  editable = true,
461
- callbacksRef
599
+ callbacksRef,
600
+ onFocus,
601
+ onBlur,
602
+ submitKey = "enter",
603
+ onMentionRemove,
604
+ onMentionClick,
605
+ onMentionHover,
606
+ allowTrigger,
607
+ validateMention
462
608
  }) {
463
609
  const onChangeRef = (0, import_react.useRef)(onChange);
464
610
  onChangeRef.current = onChange;
@@ -466,6 +612,23 @@ function useMentionsEditor({
466
612
  onSubmitRef.current = onSubmit;
467
613
  const clearOnSubmitRef = (0, import_react.useRef)(clearOnSubmit);
468
614
  clearOnSubmitRef.current = clearOnSubmit;
615
+ const onFocusRef = (0, import_react.useRef)(onFocus);
616
+ onFocusRef.current = onFocus;
617
+ const onBlurRef = (0, import_react.useRef)(onBlur);
618
+ onBlurRef.current = onBlur;
619
+ const submitKeyRef = (0, import_react.useRef)(submitKey);
620
+ submitKeyRef.current = submitKey;
621
+ const onMentionRemoveRef = (0, import_react.useRef)(onMentionRemove);
622
+ onMentionRemoveRef.current = onMentionRemove;
623
+ const onMentionClickRef = (0, import_react.useRef)(onMentionClick);
624
+ onMentionClickRef.current = onMentionClick;
625
+ const onMentionHoverRef = (0, import_react.useRef)(onMentionHover);
626
+ onMentionHoverRef.current = onMentionHover;
627
+ const allowTriggerRef = (0, import_react.useRef)(allowTrigger);
628
+ allowTriggerRef.current = allowTrigger;
629
+ const validateMentionRef = (0, import_react.useRef)(validateMention);
630
+ validateMentionRef.current = validateMention;
631
+ const internalMarkdownRef = (0, import_react.useRef)(null);
469
632
  const initialContent = (0, import_react.useMemo)(() => {
470
633
  if (!value) return void 0;
471
634
  return parseFromMarkdown(value);
@@ -477,16 +640,27 @@ function useMentionsEditor({
477
640
  [triggersKey]
478
641
  );
479
642
  const suggestionExtension = (0, import_react.useMemo)(
480
- () => createSuggestionExtension(triggers, callbacksRef),
643
+ () => createSuggestionExtension(triggers, callbacksRef, allowTriggerRef),
481
644
  // eslint-disable-next-line react-hooks/exhaustive-deps
482
645
  [triggersKey]
483
646
  );
484
647
  const submitExt = (0, import_react.useMemo)(
485
- () => createSubmitExtension(onSubmitRef, clearOnSubmitRef),
648
+ () => createSubmitExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef),
486
649
  []
487
650
  );
488
651
  const enterExt = (0, import_react.useMemo)(
489
- () => createEnterExtension(onSubmitRef, clearOnSubmitRef),
652
+ () => createEnterExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef),
653
+ []
654
+ );
655
+ const mentionRemoveExt = (0, import_react.useMemo)(
656
+ () => createMentionRemoveExtension(onMentionRemoveRef),
657
+ []
658
+ );
659
+ const mentionNodeExt = (0, import_react.useMemo)(
660
+ () => MentionNode.configure({
661
+ onClickRef: onMentionClickRef,
662
+ onHoverRef: onMentionHoverRef
663
+ }),
490
664
  []
491
665
  );
492
666
  const editor = (0, import_react2.useEditor)({
@@ -505,10 +679,11 @@ function useMentionsEditor({
505
679
  placeholder: ({ editor: editor2 }) => editor2.isEmpty ? placeholder ?? "Type a message..." : "",
506
680
  showOnlyCurrent: true
507
681
  }),
508
- MentionNode,
682
+ mentionNodeExt,
509
683
  suggestionExtension,
510
684
  submitExt,
511
- enterExt
685
+ enterExt,
686
+ mentionRemoveExt
512
687
  ],
513
688
  content: initialContent,
514
689
  autofocus: autoFocus ? "end" : false,
@@ -519,7 +694,15 @@ function useMentionsEditor({
519
694
  }
520
695
  },
521
696
  onUpdate: ({ editor: editor2 }) => {
522
- onChangeRef.current?.(buildOutput(editor2));
697
+ const output = buildOutput(editor2);
698
+ internalMarkdownRef.current = output.markdown;
699
+ onChangeRef.current?.(output);
700
+ },
701
+ onFocus: () => {
702
+ onFocusRef.current?.();
703
+ },
704
+ onBlur: () => {
705
+ onBlurRef.current?.();
523
706
  }
524
707
  });
525
708
  (0, import_react.useEffect)(() => {
@@ -527,6 +710,36 @@ function useMentionsEditor({
527
710
  editor.setEditable(editable);
528
711
  }
529
712
  }, [editor, editable]);
713
+ (0, import_react.useEffect)(() => {
714
+ if (!editor || value === void 0) return;
715
+ if (value === internalMarkdownRef.current) return;
716
+ const doc = parseFromMarkdown(value);
717
+ editor.commands.setContent(doc);
718
+ internalMarkdownRef.current = value;
719
+ }, [editor, value]);
720
+ (0, import_react.useEffect)(() => {
721
+ if (!editor || !validateMention) return;
722
+ const runValidation = async () => {
723
+ const doc = editor.getJSON();
724
+ const tokens = collectMentionTokens(doc);
725
+ const invalidIds = /* @__PURE__ */ new Set();
726
+ await Promise.all(
727
+ tokens.map(async (token) => {
728
+ const valid = await validateMention(token);
729
+ if (!valid) invalidIds.add(token.id);
730
+ })
731
+ );
732
+ editor.view.dom.querySelectorAll("[data-mention]").forEach((el) => {
733
+ const id = el.getAttribute("data-id");
734
+ if (id && invalidIds.has(id)) {
735
+ el.setAttribute("data-mention-invalid", "");
736
+ } else {
737
+ el.removeAttribute("data-mention-invalid");
738
+ }
739
+ });
740
+ };
741
+ runValidation();
742
+ }, [editor, validateMention]);
530
743
  const clear = (0, import_react.useCallback)(() => {
531
744
  editor?.commands.clearContent(true);
532
745
  }, [editor]);
@@ -535,6 +748,7 @@ function useMentionsEditor({
535
748
  if (!editor) return;
536
749
  const doc = parseFromMarkdown(markdown);
537
750
  editor.commands.setContent(doc);
751
+ internalMarkdownRef.current = markdown;
538
752
  },
539
753
  [editor]
540
754
  );
@@ -550,6 +764,27 @@ function useMentionsEditor({
550
764
 
551
765
  // src/hooks/useSuggestion.ts
552
766
  var import_react3 = require("react");
767
+
768
+ // src/utils/debounce.ts
769
+ function debounce(fn, ms) {
770
+ let timer = null;
771
+ const debounced = ((...args) => {
772
+ if (timer != null) clearTimeout(timer);
773
+ timer = setTimeout(() => {
774
+ timer = null;
775
+ fn(...args);
776
+ }, ms);
777
+ });
778
+ debounced.cancel = () => {
779
+ if (timer != null) {
780
+ clearTimeout(timer);
781
+ timer = null;
782
+ }
783
+ };
784
+ return debounced;
785
+ }
786
+
787
+ // src/hooks/useSuggestion.ts
553
788
  var IDLE_STATE = {
554
789
  state: "idle",
555
790
  items: [],
@@ -560,16 +795,24 @@ var IDLE_STATE = {
560
795
  trigger: null,
561
796
  query: ""
562
797
  };
563
- function useSuggestion(providers) {
798
+ function useSuggestion(providers, options = {}) {
564
799
  const [uiState, setUIState] = (0, import_react3.useState)(IDLE_STATE);
565
800
  const stateRef = (0, import_react3.useRef)(uiState);
566
801
  stateRef.current = uiState;
567
802
  const providersRef = (0, import_react3.useRef)(providers);
568
803
  providersRef.current = providers;
804
+ const onMentionAddRef = (0, import_react3.useRef)(options.onMentionAdd);
805
+ onMentionAddRef.current = options.onMentionAdd;
569
806
  const commandRef = (0, import_react3.useRef)(
570
807
  null
571
808
  );
572
809
  const providerRef = (0, import_react3.useRef)(null);
810
+ const debouncedFetchRef = (0, import_react3.useRef)(null);
811
+ (0, import_react3.useEffect)(() => {
812
+ return () => {
813
+ debouncedFetchRef.current?.cancel();
814
+ };
815
+ }, []);
573
816
  const fetchItems = (0, import_react3.useCallback)(
574
817
  async (provider, query, parent, useSearchAll) => {
575
818
  setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
@@ -600,6 +843,23 @@ function useSuggestion(providers) {
600
843
  },
601
844
  []
602
845
  );
846
+ const scheduleFetch = (0, import_react3.useCallback)(
847
+ (provider, query, parent, useSearchAll) => {
848
+ debouncedFetchRef.current?.cancel();
849
+ const ms = provider.debounceMs;
850
+ if (ms && ms > 0) {
851
+ setUIState((prev) => ({ ...prev, loading: true }));
852
+ const debouncedFn = debounce(() => {
853
+ fetchItems(provider, query, parent, useSearchAll);
854
+ }, ms);
855
+ debouncedFetchRef.current = debouncedFn;
856
+ debouncedFn();
857
+ } else {
858
+ fetchItems(provider, query, parent, useSearchAll);
859
+ }
860
+ },
861
+ [fetchItems]
862
+ );
603
863
  const onStart = (0, import_react3.useCallback)(
604
864
  (props) => {
605
865
  const provider = providersRef.current.find(
@@ -618,13 +878,31 @@ function useSuggestion(providers) {
618
878
  trigger: props.trigger,
619
879
  query: props.query
620
880
  });
881
+ if (!props.query.trim() && provider.getRecentItems) {
882
+ provider.getRecentItems().then((recentItems) => {
883
+ const tagged = recentItems.map((item) => ({
884
+ ...item,
885
+ group: item.group ?? "Recent"
886
+ }));
887
+ setUIState((prev) => ({
888
+ ...prev,
889
+ items: tagged,
890
+ loading: false,
891
+ state: "showing",
892
+ activeIndex: 0
893
+ }));
894
+ }).catch(() => {
895
+ scheduleFetch(provider, props.query);
896
+ });
897
+ return;
898
+ }
621
899
  if (props.query.trim() && provider.searchAll) {
622
- fetchItems(provider, props.query, void 0, true);
900
+ scheduleFetch(provider, props.query, void 0, true);
623
901
  } else {
624
- fetchItems(provider, props.query);
902
+ scheduleFetch(provider, props.query);
625
903
  }
626
904
  },
627
- [fetchItems]
905
+ [scheduleFetch]
628
906
  );
629
907
  const onUpdate = (0, import_react3.useCallback)(
630
908
  (props) => {
@@ -648,14 +926,31 @@ function useSuggestion(providers) {
648
926
  }));
649
927
  }
650
928
  if (props.query.trim() && provider.searchAll) {
651
- fetchItems(provider, props.query, void 0, true);
929
+ scheduleFetch(provider, props.query, void 0, true);
930
+ } else if (!props.query.trim() && provider.getRecentItems) {
931
+ provider.getRecentItems().then((recentItems) => {
932
+ const tagged = recentItems.map((item) => ({
933
+ ...item,
934
+ group: item.group ?? "Recent"
935
+ }));
936
+ setUIState((prev) => ({
937
+ ...prev,
938
+ items: tagged,
939
+ loading: false,
940
+ state: "showing",
941
+ activeIndex: 0
942
+ }));
943
+ }).catch(() => {
944
+ scheduleFetch(provider, props.query);
945
+ });
652
946
  } else {
653
- fetchItems(provider, props.query);
947
+ scheduleFetch(provider, props.query);
654
948
  }
655
949
  },
656
- [fetchItems]
950
+ [scheduleFetch]
657
951
  );
658
952
  const onExit = (0, import_react3.useCallback)(() => {
953
+ debouncedFetchRef.current?.cancel();
659
954
  providerRef.current = null;
660
955
  commandRef.current = null;
661
956
  setUIState(IDLE_STATE);
@@ -690,8 +985,8 @@ function useSuggestion(providers) {
690
985
  fetchItems(provider, "", selected);
691
986
  return;
692
987
  }
988
+ const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
693
989
  if (commandRef.current) {
694
- const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
695
990
  commandRef.current({
696
991
  id: selected.id,
697
992
  label: selected.label,
@@ -699,6 +994,11 @@ function useSuggestion(providers) {
699
994
  rootLabel
700
995
  });
701
996
  }
997
+ onMentionAddRef.current?.({
998
+ id: selected.id,
999
+ type: selected.type,
1000
+ label: selected.label
1001
+ });
702
1002
  },
703
1003
  [fetchItems]
704
1004
  );
@@ -723,6 +1023,7 @@ function useSuggestion(providers) {
723
1023
  });
724
1024
  }, [fetchItems]);
725
1025
  const close = (0, import_react3.useCallback)(() => {
1026
+ debouncedFetchRef.current?.cancel();
726
1027
  setUIState(IDLE_STATE);
727
1028
  }, []);
728
1029
  const searchNested = (0, import_react3.useCallback)(
@@ -732,10 +1033,10 @@ function useSuggestion(providers) {
732
1033
  const current = stateRef.current;
733
1034
  const parent = current.breadcrumbs[current.breadcrumbs.length - 1];
734
1035
  if (parent) {
735
- fetchItems(provider, query, parent);
1036
+ scheduleFetch(provider, query, parent);
736
1037
  }
737
1038
  },
738
- [fetchItems]
1039
+ [scheduleFetch]
739
1040
  );
740
1041
  const onKeyDown = (0, import_react3.useCallback)(
741
1042
  ({ event }) => {
@@ -758,6 +1059,14 @@ function useSuggestion(providers) {
758
1059
  }
759
1060
  return true;
760
1061
  }
1062
+ case "Tab": {
1063
+ event.preventDefault();
1064
+ const selectedItem = current.items[current.activeIndex];
1065
+ if (selectedItem) {
1066
+ select(selectedItem);
1067
+ }
1068
+ return true;
1069
+ }
761
1070
  case "ArrowRight": {
762
1071
  const activeItem = current.items[current.activeIndex];
763
1072
  if (activeItem?.hasChildren) {
@@ -832,13 +1141,13 @@ function optionAttrs(id, selected, index) {
832
1141
 
833
1142
  // src/components/SuggestionList.tsx
834
1143
  var import_jsx_runtime = require("react/jsx-runtime");
835
- var LISTBOX_ID = "mentions-suggestion-listbox";
836
1144
  function SuggestionList({
837
1145
  items,
838
1146
  activeIndex,
839
1147
  breadcrumbs,
840
1148
  loading,
841
1149
  trigger,
1150
+ query,
842
1151
  clientRect,
843
1152
  onSelect,
844
1153
  onHover,
@@ -847,7 +1156,12 @@ function SuggestionList({
847
1156
  onNavigateUp,
848
1157
  onNavigateDown,
849
1158
  onClose,
850
- renderItem
1159
+ onFocusEditor,
1160
+ renderItem,
1161
+ renderEmpty,
1162
+ renderLoading,
1163
+ renderGroupHeader,
1164
+ listboxId
851
1165
  }) {
852
1166
  const listRef = (0, import_react4.useRef)(null);
853
1167
  const searchInputRef = (0, import_react4.useRef)(null);
@@ -861,18 +1175,24 @@ function SuggestionList({
861
1175
  prevBreadcrumbKey.current = breadcrumbKey;
862
1176
  }
863
1177
  }, [breadcrumbKey]);
1178
+ const prevBreadcrumbsLen = (0, import_react4.useRef)(breadcrumbs.length);
864
1179
  (0, import_react4.useEffect)(() => {
865
- if (breadcrumbs.length > 0 && searchInputRef.current) {
1180
+ if (prevBreadcrumbsLen.current > 0 && breadcrumbs.length === 0) {
1181
+ onFocusEditor?.();
1182
+ } else if (breadcrumbs.length > 0 && searchInputRef.current) {
866
1183
  requestAnimationFrame(() => searchInputRef.current?.focus());
867
1184
  }
868
- }, [breadcrumbKey, breadcrumbs.length]);
1185
+ prevBreadcrumbsLen.current = breadcrumbs.length;
1186
+ }, [breadcrumbKey, breadcrumbs.length, onFocusEditor]);
869
1187
  (0, import_react4.useEffect)(() => {
870
1188
  if (!listRef.current) return;
871
1189
  const active = listRef.current.querySelector('[aria-selected="true"]');
872
1190
  active?.scrollIntoView({ block: "nearest" });
873
1191
  }, [activeIndex]);
874
- const style = usePopoverPosition(clientRect);
875
- if (items.length === 0 && !loading) return null;
1192
+ const { style, position } = usePopoverPosition(clientRect);
1193
+ const activeQuery = breadcrumbs.length > 0 ? nestedQuery : query;
1194
+ const showEmpty = !loading && items.length === 0 && activeQuery.trim().length > 0;
1195
+ if (items.length === 0 && !loading && !showEmpty) return null;
876
1196
  const handleSearchKeyDown = (e) => {
877
1197
  switch (e.key) {
878
1198
  case "ArrowDown":
@@ -892,8 +1212,23 @@ function SuggestionList({
892
1212
  }
893
1213
  case "Escape":
894
1214
  e.preventDefault();
1215
+ onFocusEditor?.();
895
1216
  onClose?.();
896
1217
  break;
1218
+ case "ArrowLeft":
1219
+ if (nestedQuery === "" || e.currentTarget.selectionStart === 0) {
1220
+ e.preventDefault();
1221
+ onGoBack();
1222
+ }
1223
+ break;
1224
+ case "ArrowRight": {
1225
+ const item = items[activeIndex];
1226
+ if (item?.hasChildren) {
1227
+ e.preventDefault();
1228
+ onSelect(item);
1229
+ }
1230
+ break;
1231
+ }
897
1232
  case "Backspace":
898
1233
  if (nestedQuery === "") {
899
1234
  e.preventDefault();
@@ -902,11 +1237,13 @@ function SuggestionList({
902
1237
  break;
903
1238
  }
904
1239
  };
1240
+ const hasGroups = items.some((item) => item.group);
905
1241
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
906
1242
  "div",
907
1243
  {
908
1244
  "data-suggestions": "",
909
1245
  "data-trigger": trigger,
1246
+ "data-suggestions-position": position,
910
1247
  style,
911
1248
  ref: listRef,
912
1249
  children: [
@@ -944,28 +1281,76 @@ function SuggestionList({
944
1281
  spellCheck: false
945
1282
  }
946
1283
  ) }),
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
- }) })
1284
+ loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-loading": "", children: renderLoading ? renderLoading() : "Loading..." }),
1285
+ showEmpty && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-empty": "", children: renderEmpty ? renderEmpty(activeQuery) : "No results" }),
1286
+ !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)(
1287
+ SuggestionItem,
1288
+ {
1289
+ item,
1290
+ index,
1291
+ isActive: index === activeIndex,
1292
+ depth,
1293
+ onSelect,
1294
+ onHover,
1295
+ renderItem
1296
+ },
1297
+ item.id
1298
+ )) })
965
1299
  ]
966
1300
  }
967
1301
  );
968
1302
  }
1303
+ function renderGroupedItems(items, activeIndex, depth, onSelect, onHover, renderItem, renderGroupHeader) {
1304
+ const elements = [];
1305
+ let lastGroup;
1306
+ items.forEach((item, index) => {
1307
+ if (item.group && item.group !== lastGroup) {
1308
+ lastGroup = item.group;
1309
+ elements.push(
1310
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-group-header": "", children: renderGroupHeader ? renderGroupHeader(item.group) : item.group }, `group-${item.group}`)
1311
+ );
1312
+ }
1313
+ elements.push(
1314
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1315
+ SuggestionItem,
1316
+ {
1317
+ item,
1318
+ index,
1319
+ isActive: index === activeIndex,
1320
+ depth,
1321
+ onSelect,
1322
+ onHover,
1323
+ renderItem
1324
+ },
1325
+ item.id
1326
+ )
1327
+ );
1328
+ });
1329
+ return elements;
1330
+ }
1331
+ function SuggestionItem({
1332
+ item,
1333
+ index,
1334
+ isActive,
1335
+ depth,
1336
+ onSelect,
1337
+ onHover,
1338
+ renderItem
1339
+ }) {
1340
+ const itemId = `mention-option-${item.id}`;
1341
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1342
+ "div",
1343
+ {
1344
+ ...optionAttrs(itemId, isActive, index),
1345
+ "data-suggestion-item": "",
1346
+ "data-suggestion-item-active": isActive ? "" : void 0,
1347
+ "data-has-children": item.hasChildren ? "" : void 0,
1348
+ onMouseEnter: () => onHover(index),
1349
+ onClick: () => onSelect(item),
1350
+ children: renderItem ? renderItem(item, depth) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultSuggestionItem, { item })
1351
+ }
1352
+ );
1353
+ }
969
1354
  function DefaultSuggestionItem({ item }) {
970
1355
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
971
1356
  item.icon && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-icon": "", children: item.icon }),
@@ -974,25 +1359,48 @@ function DefaultSuggestionItem({ item }) {
974
1359
  item.hasChildren && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-chevron": "", "aria-hidden": "true", children: "\u203A" })
975
1360
  ] });
976
1361
  }
1362
+ var POPOVER_HEIGHT_ESTIMATE = 280;
1363
+ var POPOVER_WIDTH_ESTIMATE = 360;
977
1364
  function usePopoverPosition(clientRect) {
978
1365
  if (!clientRect) {
979
- return { display: "none" };
1366
+ return { style: { display: "none" }, position: "below" };
980
1367
  }
981
1368
  const rect = clientRect();
982
1369
  if (!rect) {
983
- return { display: "none" };
1370
+ return { style: { display: "none" }, position: "below" };
1371
+ }
1372
+ const viewportH = typeof window !== "undefined" ? window.innerHeight : 800;
1373
+ const viewportW = typeof window !== "undefined" ? window.innerWidth : 1200;
1374
+ const spaceBelow = viewportH - rect.bottom;
1375
+ const shouldFlip = spaceBelow < POPOVER_HEIGHT_ESTIMATE && rect.top > spaceBelow;
1376
+ let left = rect.left;
1377
+ if (left + POPOVER_WIDTH_ESTIMATE > viewportW) {
1378
+ left = Math.max(0, viewportW - POPOVER_WIDTH_ESTIMATE);
1379
+ }
1380
+ if (shouldFlip) {
1381
+ return {
1382
+ style: {
1383
+ position: "fixed",
1384
+ left: `${left}px`,
1385
+ bottom: `${viewportH - rect.top + 4}px`,
1386
+ zIndex: 50
1387
+ },
1388
+ position: "above"
1389
+ };
984
1390
  }
985
1391
  return {
986
- position: "fixed",
987
- left: `${rect.left}px`,
988
- top: `${rect.bottom + 4}px`,
989
- zIndex: 50
1392
+ style: {
1393
+ position: "fixed",
1394
+ left: `${left}px`,
1395
+ top: `${rect.bottom + 4}px`,
1396
+ zIndex: 50
1397
+ },
1398
+ position: "below"
990
1399
  };
991
1400
  }
992
1401
 
993
1402
  // src/components/MentionsInput.tsx
994
1403
  var import_jsx_runtime2 = require("react/jsx-runtime");
995
- var LISTBOX_ID2 = "mentions-suggestion-listbox";
996
1404
  var MentionsInput = (0, import_react5.forwardRef)(
997
1405
  function MentionsInput2({
998
1406
  value,
@@ -1006,10 +1414,29 @@ var MentionsInput = (0, import_react5.forwardRef)(
1006
1414
  clearOnSubmit = true,
1007
1415
  maxLength,
1008
1416
  renderItem,
1009
- renderChip
1417
+ renderChip,
1418
+ renderEmpty,
1419
+ renderLoading,
1420
+ renderGroupHeader,
1421
+ onFocus,
1422
+ onBlur,
1423
+ onMentionAdd,
1424
+ onMentionRemove,
1425
+ onMentionClick,
1426
+ onMentionHover,
1427
+ minHeight,
1428
+ maxHeight,
1429
+ submitKey = "enter",
1430
+ allowTrigger,
1431
+ validateMention,
1432
+ portalContainer
1010
1433
  }, ref) {
1011
- const { uiState, actions, callbacksRef } = useSuggestion(providers);
1012
- const { editor, clear, setContent, focus } = useMentionsEditor({
1434
+ const instanceId = (0, import_react5.useId)();
1435
+ const listboxId = `mentions-listbox-${instanceId}`;
1436
+ const { uiState, actions, callbacksRef } = useSuggestion(providers, {
1437
+ onMentionAdd
1438
+ });
1439
+ const { editor, getOutput, clear, setContent, focus } = useMentionsEditor({
1013
1440
  providers,
1014
1441
  value,
1015
1442
  onChange,
@@ -1018,46 +1445,70 @@ var MentionsInput = (0, import_react5.forwardRef)(
1018
1445
  placeholder,
1019
1446
  autoFocus,
1020
1447
  editable: !disabled,
1021
- callbacksRef
1448
+ callbacksRef,
1449
+ onFocus,
1450
+ onBlur,
1451
+ submitKey,
1452
+ onMentionRemove,
1453
+ onMentionClick,
1454
+ onMentionHover,
1455
+ allowTrigger,
1456
+ validateMention
1022
1457
  });
1023
1458
  (0, import_react5.useImperativeHandle)(
1024
1459
  ref,
1025
- () => ({ clear, setContent, focus }),
1026
- [clear, setContent, focus]
1460
+ () => ({ clear, setContent, focus, getOutput }),
1461
+ [clear, setContent, focus, getOutput]
1027
1462
  );
1028
1463
  const isExpanded = uiState.state !== "idle";
1029
1464
  const handleHover = (0, import_react5.useCallback)((index) => {
1030
1465
  void index;
1031
1466
  }, []);
1467
+ const handleFocusEditor = (0, import_react5.useCallback)(() => {
1468
+ editor?.commands.focus();
1469
+ }, [editor]);
1470
+ const editorStyle = {};
1471
+ if (minHeight != null) editorStyle.minHeight = `${minHeight}px`;
1472
+ if (maxHeight != null) {
1473
+ editorStyle.maxHeight = `${maxHeight}px`;
1474
+ editorStyle.overflowY = "auto";
1475
+ }
1476
+ const suggestionList = isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1477
+ SuggestionList,
1478
+ {
1479
+ items: uiState.items,
1480
+ activeIndex: uiState.activeIndex,
1481
+ breadcrumbs: uiState.breadcrumbs,
1482
+ loading: uiState.loading,
1483
+ trigger: uiState.trigger,
1484
+ query: uiState.query,
1485
+ clientRect: uiState.clientRect,
1486
+ onSelect: (item) => actions.select(item),
1487
+ onHover: handleHover,
1488
+ onGoBack: actions.goBack,
1489
+ onSearchNested: actions.searchNested,
1490
+ onNavigateUp: actions.navigateUp,
1491
+ onNavigateDown: actions.navigateDown,
1492
+ onClose: actions.close,
1493
+ onFocusEditor: handleFocusEditor,
1494
+ renderItem,
1495
+ renderEmpty,
1496
+ renderLoading,
1497
+ renderGroupHeader,
1498
+ listboxId
1499
+ }
1500
+ ) : null;
1032
1501
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1033
1502
  "div",
1034
1503
  {
1035
1504
  className,
1036
1505
  "data-mentions-input": "",
1037
1506
  "data-disabled": disabled ? "" : void 0,
1038
- ...comboboxAttrs(isExpanded, LISTBOX_ID2),
1507
+ ...comboboxAttrs(isExpanded, listboxId),
1039
1508
  "aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
1040
1509
  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
- )
1510
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: editorStyle, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react6.EditorContent, { editor }) }),
1511
+ portalContainer ? suggestionList && (0, import_react_dom.createPortal)(suggestionList, portalContainer) : suggestionList
1061
1512
  ]
1062
1513
  }
1063
1514
  );