@relevaince/mentions 0.3.2 → 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,
@@ -286,11 +371,12 @@ function serializeParagraph(node) {
286
371
  if (!node.content) return "";
287
372
  return node.content.map((child) => {
288
373
  if (child.type === "mention") {
289
- const { id, label, rootLabel } = child.attrs ?? {};
374
+ const { id, label, entityType, rootLabel } = child.attrs ?? {};
375
+ const idPart = entityType && entityType !== "unknown" ? `${entityType}:${id}` : id;
290
376
  if (rootLabel != null && rootLabel !== "") {
291
- return `@${rootLabel}[${label}](${id})`;
377
+ return `@${rootLabel}[${label}](${idPart})`;
292
378
  }
293
- return `@[${label}](${id})`;
379
+ return `@[${label}](${idPart})`;
294
380
  }
295
381
  return child.text ?? "";
296
382
  }).join("");
@@ -335,7 +421,7 @@ function extractParagraphText(node) {
335
421
  }
336
422
 
337
423
  // src/core/markdownParser.ts
338
- var MENTION_RE = /@(?:([^\[]+)\[)?\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
424
+ var MENTION_RE = /@([^\[@]*)\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
339
425
  function parseFromMarkdown(markdown) {
340
426
  const lines = markdown.split("\n");
341
427
  const content = lines.map((line) => {
@@ -351,7 +437,7 @@ function parseLine(line) {
351
437
  let match;
352
438
  while ((match = MENTION_RE.exec(line)) !== null) {
353
439
  const fullMatch = match[0];
354
- const rootLabel = match[1] ?? null;
440
+ const rootLabel = match[1] || null;
355
441
  const label = match[2];
356
442
  const entityType = match[3] ?? "unknown";
357
443
  const id = match[4];
@@ -397,27 +483,48 @@ function buildOutput(editor) {
397
483
  plainText: extractPlainText(json)
398
484
  };
399
485
  }
400
- 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) {
401
504
  return import_core3.Extension.create({
402
505
  name: "submitShortcut",
403
506
  priority: 150,
404
507
  addKeyboardShortcuts() {
405
508
  return {
406
509
  "Mod-Enter": () => {
407
- if (onSubmitRef.current) {
408
- onSubmitRef.current(buildOutput(this.editor));
409
- if (clearOnSubmitRef.current) {
410
- 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
+ }
411
517
  }
518
+ return true;
412
519
  }
413
- return true;
520
+ return false;
414
521
  }
415
522
  };
416
523
  }
417
524
  });
418
525
  }
419
526
  var enterSubmitPluginKey = new import_state2.PluginKey("enterSubmit");
420
- function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
527
+ function createEnterExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef) {
421
528
  return import_core3.Extension.create({
422
529
  name: "enterSubmit",
423
530
  priority: 150,
@@ -429,6 +536,11 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
429
536
  props: {
430
537
  handleKeyDown(_view, event) {
431
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
+ }
432
544
  if (event.shiftKey) {
433
545
  editor.commands.splitBlock();
434
546
  return true;
@@ -448,6 +560,33 @@ function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
448
560
  }
449
561
  });
450
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
+ }
451
590
  function useMentionsEditor({
452
591
  providers,
453
592
  value,
@@ -457,7 +596,15 @@ function useMentionsEditor({
457
596
  placeholder,
458
597
  autoFocus = false,
459
598
  editable = true,
460
- callbacksRef
599
+ callbacksRef,
600
+ onFocus,
601
+ onBlur,
602
+ submitKey = "enter",
603
+ onMentionRemove,
604
+ onMentionClick,
605
+ onMentionHover,
606
+ allowTrigger,
607
+ validateMention
461
608
  }) {
462
609
  const onChangeRef = (0, import_react.useRef)(onChange);
463
610
  onChangeRef.current = onChange;
@@ -465,6 +612,23 @@ function useMentionsEditor({
465
612
  onSubmitRef.current = onSubmit;
466
613
  const clearOnSubmitRef = (0, import_react.useRef)(clearOnSubmit);
467
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);
468
632
  const initialContent = (0, import_react.useMemo)(() => {
469
633
  if (!value) return void 0;
470
634
  return parseFromMarkdown(value);
@@ -476,16 +640,27 @@ function useMentionsEditor({
476
640
  [triggersKey]
477
641
  );
478
642
  const suggestionExtension = (0, import_react.useMemo)(
479
- () => createSuggestionExtension(triggers, callbacksRef),
643
+ () => createSuggestionExtension(triggers, callbacksRef, allowTriggerRef),
480
644
  // eslint-disable-next-line react-hooks/exhaustive-deps
481
645
  [triggersKey]
482
646
  );
483
647
  const submitExt = (0, import_react.useMemo)(
484
- () => createSubmitExtension(onSubmitRef, clearOnSubmitRef),
648
+ () => createSubmitExtension(onSubmitRef, clearOnSubmitRef, submitKeyRef),
485
649
  []
486
650
  );
487
651
  const enterExt = (0, import_react.useMemo)(
488
- () => 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
+ }),
489
664
  []
490
665
  );
491
666
  const editor = (0, import_react2.useEditor)({
@@ -504,10 +679,11 @@ function useMentionsEditor({
504
679
  placeholder: ({ editor: editor2 }) => editor2.isEmpty ? placeholder ?? "Type a message..." : "",
505
680
  showOnlyCurrent: true
506
681
  }),
507
- MentionNode,
682
+ mentionNodeExt,
508
683
  suggestionExtension,
509
684
  submitExt,
510
- enterExt
685
+ enterExt,
686
+ mentionRemoveExt
511
687
  ],
512
688
  content: initialContent,
513
689
  autofocus: autoFocus ? "end" : false,
@@ -518,7 +694,15 @@ function useMentionsEditor({
518
694
  }
519
695
  },
520
696
  onUpdate: ({ editor: editor2 }) => {
521
- 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?.();
522
706
  }
523
707
  });
524
708
  (0, import_react.useEffect)(() => {
@@ -526,6 +710,36 @@ function useMentionsEditor({
526
710
  editor.setEditable(editable);
527
711
  }
528
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]);
529
743
  const clear = (0, import_react.useCallback)(() => {
530
744
  editor?.commands.clearContent(true);
531
745
  }, [editor]);
@@ -534,6 +748,7 @@ function useMentionsEditor({
534
748
  if (!editor) return;
535
749
  const doc = parseFromMarkdown(markdown);
536
750
  editor.commands.setContent(doc);
751
+ internalMarkdownRef.current = markdown;
537
752
  },
538
753
  [editor]
539
754
  );
@@ -549,6 +764,27 @@ function useMentionsEditor({
549
764
 
550
765
  // src/hooks/useSuggestion.ts
551
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
552
788
  var IDLE_STATE = {
553
789
  state: "idle",
554
790
  items: [],
@@ -559,16 +795,24 @@ var IDLE_STATE = {
559
795
  trigger: null,
560
796
  query: ""
561
797
  };
562
- function useSuggestion(providers) {
798
+ function useSuggestion(providers, options = {}) {
563
799
  const [uiState, setUIState] = (0, import_react3.useState)(IDLE_STATE);
564
800
  const stateRef = (0, import_react3.useRef)(uiState);
565
801
  stateRef.current = uiState;
566
802
  const providersRef = (0, import_react3.useRef)(providers);
567
803
  providersRef.current = providers;
804
+ const onMentionAddRef = (0, import_react3.useRef)(options.onMentionAdd);
805
+ onMentionAddRef.current = options.onMentionAdd;
568
806
  const commandRef = (0, import_react3.useRef)(
569
807
  null
570
808
  );
571
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
+ }, []);
572
816
  const fetchItems = (0, import_react3.useCallback)(
573
817
  async (provider, query, parent, useSearchAll) => {
574
818
  setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
@@ -599,6 +843,23 @@ function useSuggestion(providers) {
599
843
  },
600
844
  []
601
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
+ );
602
863
  const onStart = (0, import_react3.useCallback)(
603
864
  (props) => {
604
865
  const provider = providersRef.current.find(
@@ -617,13 +878,31 @@ function useSuggestion(providers) {
617
878
  trigger: props.trigger,
618
879
  query: props.query
619
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
+ }
620
899
  if (props.query.trim() && provider.searchAll) {
621
- fetchItems(provider, props.query, void 0, true);
900
+ scheduleFetch(provider, props.query, void 0, true);
622
901
  } else {
623
- fetchItems(provider, props.query);
902
+ scheduleFetch(provider, props.query);
624
903
  }
625
904
  },
626
- [fetchItems]
905
+ [scheduleFetch]
627
906
  );
628
907
  const onUpdate = (0, import_react3.useCallback)(
629
908
  (props) => {
@@ -647,14 +926,31 @@ function useSuggestion(providers) {
647
926
  }));
648
927
  }
649
928
  if (props.query.trim() && provider.searchAll) {
650
- 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
+ });
651
946
  } else {
652
- fetchItems(provider, props.query);
947
+ scheduleFetch(provider, props.query);
653
948
  }
654
949
  },
655
- [fetchItems]
950
+ [scheduleFetch]
656
951
  );
657
952
  const onExit = (0, import_react3.useCallback)(() => {
953
+ debouncedFetchRef.current?.cancel();
658
954
  providerRef.current = null;
659
955
  commandRef.current = null;
660
956
  setUIState(IDLE_STATE);
@@ -689,8 +985,8 @@ function useSuggestion(providers) {
689
985
  fetchItems(provider, "", selected);
690
986
  return;
691
987
  }
988
+ const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
692
989
  if (commandRef.current) {
693
- const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
694
990
  commandRef.current({
695
991
  id: selected.id,
696
992
  label: selected.label,
@@ -698,6 +994,11 @@ function useSuggestion(providers) {
698
994
  rootLabel
699
995
  });
700
996
  }
997
+ onMentionAddRef.current?.({
998
+ id: selected.id,
999
+ type: selected.type,
1000
+ label: selected.label
1001
+ });
701
1002
  },
702
1003
  [fetchItems]
703
1004
  );
@@ -722,6 +1023,7 @@ function useSuggestion(providers) {
722
1023
  });
723
1024
  }, [fetchItems]);
724
1025
  const close = (0, import_react3.useCallback)(() => {
1026
+ debouncedFetchRef.current?.cancel();
725
1027
  setUIState(IDLE_STATE);
726
1028
  }, []);
727
1029
  const searchNested = (0, import_react3.useCallback)(
@@ -731,10 +1033,10 @@ function useSuggestion(providers) {
731
1033
  const current = stateRef.current;
732
1034
  const parent = current.breadcrumbs[current.breadcrumbs.length - 1];
733
1035
  if (parent) {
734
- fetchItems(provider, query, parent);
1036
+ scheduleFetch(provider, query, parent);
735
1037
  }
736
1038
  },
737
- [fetchItems]
1039
+ [scheduleFetch]
738
1040
  );
739
1041
  const onKeyDown = (0, import_react3.useCallback)(
740
1042
  ({ event }) => {
@@ -757,6 +1059,14 @@ function useSuggestion(providers) {
757
1059
  }
758
1060
  return true;
759
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
+ }
760
1070
  case "ArrowRight": {
761
1071
  const activeItem = current.items[current.activeIndex];
762
1072
  if (activeItem?.hasChildren) {
@@ -831,13 +1141,13 @@ function optionAttrs(id, selected, index) {
831
1141
 
832
1142
  // src/components/SuggestionList.tsx
833
1143
  var import_jsx_runtime = require("react/jsx-runtime");
834
- var LISTBOX_ID = "mentions-suggestion-listbox";
835
1144
  function SuggestionList({
836
1145
  items,
837
1146
  activeIndex,
838
1147
  breadcrumbs,
839
1148
  loading,
840
1149
  trigger,
1150
+ query,
841
1151
  clientRect,
842
1152
  onSelect,
843
1153
  onHover,
@@ -846,7 +1156,12 @@ function SuggestionList({
846
1156
  onNavigateUp,
847
1157
  onNavigateDown,
848
1158
  onClose,
849
- renderItem
1159
+ onFocusEditor,
1160
+ renderItem,
1161
+ renderEmpty,
1162
+ renderLoading,
1163
+ renderGroupHeader,
1164
+ listboxId
850
1165
  }) {
851
1166
  const listRef = (0, import_react4.useRef)(null);
852
1167
  const searchInputRef = (0, import_react4.useRef)(null);
@@ -860,18 +1175,24 @@ function SuggestionList({
860
1175
  prevBreadcrumbKey.current = breadcrumbKey;
861
1176
  }
862
1177
  }, [breadcrumbKey]);
1178
+ const prevBreadcrumbsLen = (0, import_react4.useRef)(breadcrumbs.length);
863
1179
  (0, import_react4.useEffect)(() => {
864
- if (breadcrumbs.length > 0 && searchInputRef.current) {
1180
+ if (prevBreadcrumbsLen.current > 0 && breadcrumbs.length === 0) {
1181
+ onFocusEditor?.();
1182
+ } else if (breadcrumbs.length > 0 && searchInputRef.current) {
865
1183
  requestAnimationFrame(() => searchInputRef.current?.focus());
866
1184
  }
867
- }, [breadcrumbKey, breadcrumbs.length]);
1185
+ prevBreadcrumbsLen.current = breadcrumbs.length;
1186
+ }, [breadcrumbKey, breadcrumbs.length, onFocusEditor]);
868
1187
  (0, import_react4.useEffect)(() => {
869
1188
  if (!listRef.current) return;
870
1189
  const active = listRef.current.querySelector('[aria-selected="true"]');
871
1190
  active?.scrollIntoView({ block: "nearest" });
872
1191
  }, [activeIndex]);
873
- const style = usePopoverPosition(clientRect);
874
- 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;
875
1196
  const handleSearchKeyDown = (e) => {
876
1197
  switch (e.key) {
877
1198
  case "ArrowDown":
@@ -891,8 +1212,23 @@ function SuggestionList({
891
1212
  }
892
1213
  case "Escape":
893
1214
  e.preventDefault();
1215
+ onFocusEditor?.();
894
1216
  onClose?.();
895
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
+ }
896
1232
  case "Backspace":
897
1233
  if (nestedQuery === "") {
898
1234
  e.preventDefault();
@@ -901,11 +1237,13 @@ function SuggestionList({
901
1237
  break;
902
1238
  }
903
1239
  };
1240
+ const hasGroups = items.some((item) => item.group);
904
1241
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
905
1242
  "div",
906
1243
  {
907
1244
  "data-suggestions": "",
908
1245
  "data-trigger": trigger,
1246
+ "data-suggestions-position": position,
909
1247
  style,
910
1248
  ref: listRef,
911
1249
  children: [
@@ -943,28 +1281,76 @@ function SuggestionList({
943
1281
  spellCheck: false
944
1282
  }
945
1283
  ) }),
946
- loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-suggestion-loading": "", children: "Loading..." }),
947
- !loading && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { ...listboxAttrs(LISTBOX_ID, `${trigger ?? ""} suggestions`), children: items.map((item, index) => {
948
- const isActive = index === activeIndex;
949
- const itemId = `mention-option-${item.id}`;
950
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
951
- "div",
952
- {
953
- ...optionAttrs(itemId, isActive, index),
954
- "data-suggestion-item": "",
955
- "data-suggestion-item-active": isActive ? "" : void 0,
956
- "data-has-children": item.hasChildren ? "" : void 0,
957
- onMouseEnter: () => onHover(index),
958
- onClick: () => onSelect(item),
959
- children: renderItem ? renderItem(item, depth) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DefaultSuggestionItem, { item })
960
- },
961
- item.id
962
- );
963
- }) })
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
+ )) })
964
1299
  ]
965
1300
  }
966
1301
  );
967
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
+ }
968
1354
  function DefaultSuggestionItem({ item }) {
969
1355
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
970
1356
  item.icon && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-icon": "", children: item.icon }),
@@ -973,25 +1359,48 @@ function DefaultSuggestionItem({ item }) {
973
1359
  item.hasChildren && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-suggestion-item-chevron": "", "aria-hidden": "true", children: "\u203A" })
974
1360
  ] });
975
1361
  }
1362
+ var POPOVER_HEIGHT_ESTIMATE = 280;
1363
+ var POPOVER_WIDTH_ESTIMATE = 360;
976
1364
  function usePopoverPosition(clientRect) {
977
1365
  if (!clientRect) {
978
- return { display: "none" };
1366
+ return { style: { display: "none" }, position: "below" };
979
1367
  }
980
1368
  const rect = clientRect();
981
1369
  if (!rect) {
982
- 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
+ };
983
1390
  }
984
1391
  return {
985
- position: "fixed",
986
- left: `${rect.left}px`,
987
- top: `${rect.bottom + 4}px`,
988
- zIndex: 50
1392
+ style: {
1393
+ position: "fixed",
1394
+ left: `${left}px`,
1395
+ top: `${rect.bottom + 4}px`,
1396
+ zIndex: 50
1397
+ },
1398
+ position: "below"
989
1399
  };
990
1400
  }
991
1401
 
992
1402
  // src/components/MentionsInput.tsx
993
1403
  var import_jsx_runtime2 = require("react/jsx-runtime");
994
- var LISTBOX_ID2 = "mentions-suggestion-listbox";
995
1404
  var MentionsInput = (0, import_react5.forwardRef)(
996
1405
  function MentionsInput2({
997
1406
  value,
@@ -1005,10 +1414,29 @@ var MentionsInput = (0, import_react5.forwardRef)(
1005
1414
  clearOnSubmit = true,
1006
1415
  maxLength,
1007
1416
  renderItem,
1008
- 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
1009
1433
  }, ref) {
1010
- const { uiState, actions, callbacksRef } = useSuggestion(providers);
1011
- 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({
1012
1440
  providers,
1013
1441
  value,
1014
1442
  onChange,
@@ -1017,46 +1445,70 @@ var MentionsInput = (0, import_react5.forwardRef)(
1017
1445
  placeholder,
1018
1446
  autoFocus,
1019
1447
  editable: !disabled,
1020
- callbacksRef
1448
+ callbacksRef,
1449
+ onFocus,
1450
+ onBlur,
1451
+ submitKey,
1452
+ onMentionRemove,
1453
+ onMentionClick,
1454
+ onMentionHover,
1455
+ allowTrigger,
1456
+ validateMention
1021
1457
  });
1022
1458
  (0, import_react5.useImperativeHandle)(
1023
1459
  ref,
1024
- () => ({ clear, setContent, focus }),
1025
- [clear, setContent, focus]
1460
+ () => ({ clear, setContent, focus, getOutput }),
1461
+ [clear, setContent, focus, getOutput]
1026
1462
  );
1027
1463
  const isExpanded = uiState.state !== "idle";
1028
1464
  const handleHover = (0, import_react5.useCallback)((index) => {
1029
1465
  void index;
1030
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;
1031
1501
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1032
1502
  "div",
1033
1503
  {
1034
1504
  className,
1035
1505
  "data-mentions-input": "",
1036
1506
  "data-disabled": disabled ? "" : void 0,
1037
- ...comboboxAttrs(isExpanded, LISTBOX_ID2),
1507
+ ...comboboxAttrs(isExpanded, listboxId),
1038
1508
  "aria-activedescendant": isExpanded && uiState.items[uiState.activeIndex] ? `mention-option-${uiState.items[uiState.activeIndex].id}` : void 0,
1039
1509
  children: [
1040
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react6.EditorContent, { editor }),
1041
- isExpanded && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1042
- SuggestionList,
1043
- {
1044
- items: uiState.items,
1045
- activeIndex: uiState.activeIndex,
1046
- breadcrumbs: uiState.breadcrumbs,
1047
- loading: uiState.loading,
1048
- trigger: uiState.trigger,
1049
- clientRect: uiState.clientRect,
1050
- onSelect: (item) => actions.select(item),
1051
- onHover: handleHover,
1052
- onGoBack: actions.goBack,
1053
- onSearchNested: actions.searchNested,
1054
- onNavigateUp: actions.navigateUp,
1055
- onNavigateDown: actions.navigateDown,
1056
- onClose: actions.close,
1057
- renderItem
1058
- }
1059
- )
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
1060
1512
  ]
1061
1513
  }
1062
1514
  );