@relevaince/mentions 0.2.1 → 0.3.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
@@ -8,6 +8,7 @@ import { useEditor } from "@tiptap/react";
8
8
  import StarterKit from "@tiptap/starter-kit";
9
9
  import Placeholder from "@tiptap/extension-placeholder";
10
10
  import { Extension as Extension2 } from "@tiptap/core";
11
+ import { Plugin as Plugin2, PluginKey as PluginKey2 } from "@tiptap/pm/state";
11
12
 
12
13
  // src/core/mentionExtension.ts
13
14
  import { mergeAttributes, Node } from "@tiptap/core";
@@ -40,6 +41,11 @@ var MentionNode = Node.create({
40
41
  default: null,
41
42
  parseHTML: (element) => element.getAttribute("data-type"),
42
43
  renderHTML: (attributes) => ({ "data-type": attributes.entityType })
44
+ },
45
+ rootLabel: {
46
+ default: null,
47
+ parseHTML: (element) => element.getAttribute("data-root-label"),
48
+ renderHTML: (attributes) => attributes.rootLabel ? { "data-root-label": attributes.rootLabel } : {}
43
49
  }
44
50
  };
45
51
  },
@@ -50,13 +56,14 @@ var MentionNode = Node.create({
50
56
  const entityType = node.attrs.entityType;
51
57
  const label = node.attrs.label;
52
58
  const prefix = DEFAULT_PREFIXES[entityType] ?? "@";
59
+ const display = `${prefix}${label}`;
53
60
  return [
54
61
  "span",
55
62
  mergeAttributes(HTMLAttributes, {
56
63
  "data-mention": "",
57
64
  class: "mention-chip"
58
65
  }),
59
- `${prefix}${label}`
66
+ display
60
67
  ];
61
68
  },
62
69
  renderText({ node }) {
@@ -108,6 +115,7 @@ var suggestionPluginKey = new PluginKey("mentionSuggestion");
108
115
  function createSuggestionExtension(triggers, callbacksRef) {
109
116
  return Extension.create({
110
117
  name: "mentionSuggestion",
118
+ priority: 200,
111
119
  addProseMirrorPlugins() {
112
120
  const editor = this.editor;
113
121
  let active = false;
@@ -136,7 +144,8 @@ function createSuggestionExtension(triggers, callbacksRef) {
136
144
  attrs: {
137
145
  id: attrs.id,
138
146
  label: attrs.label,
139
- entityType: attrs.entityType
147
+ entityType: attrs.entityType,
148
+ rootLabel: attrs.rootLabel ?? null
140
149
  }
141
150
  },
142
151
  { type: "text", text: " " }
@@ -238,7 +247,10 @@ function serializeParagraph(node) {
238
247
  if (!node.content) return "";
239
248
  return node.content.map((child) => {
240
249
  if (child.type === "mention") {
241
- const { id, label } = child.attrs ?? {};
250
+ const { id, label, rootLabel } = child.attrs ?? {};
251
+ if (rootLabel != null && rootLabel !== "") {
252
+ return `@${rootLabel}[${label}](${id})`;
253
+ }
242
254
  return `@[${label}](${id})`;
243
255
  }
244
256
  return child.text ?? "";
@@ -284,7 +296,7 @@ function extractParagraphText(node) {
284
296
  }
285
297
 
286
298
  // src/core/markdownParser.ts
287
- var MENTION_RE = /@\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
299
+ var MENTION_RE = /@(?:([^\[]+)\[)?\[([^\]]+)\]\((?:([^:)]+):)?([^)]+)\)/g;
288
300
  function parseFromMarkdown(markdown) {
289
301
  const lines = markdown.split("\n");
290
302
  const content = lines.map((line) => ({
@@ -300,9 +312,10 @@ function parseLine(line) {
300
312
  let match;
301
313
  while ((match = MENTION_RE.exec(line)) !== null) {
302
314
  const fullMatch = match[0];
303
- const label = match[1];
304
- const entityType = match[2] ?? "unknown";
305
- const id = match[3];
315
+ const rootLabel = match[1] ?? null;
316
+ const label = match[2];
317
+ const entityType = match[3] ?? "unknown";
318
+ const id = match[4];
306
319
  if (match.index > lastIndex) {
307
320
  nodes.push({
308
321
  type: "text",
@@ -314,7 +327,8 @@ function parseLine(line) {
314
327
  attrs: {
315
328
  id,
316
329
  label,
317
- entityType
330
+ entityType,
331
+ rootLabel
318
332
  }
319
333
  });
320
334
  lastIndex = match.index + fullMatch.length;
@@ -330,6 +344,13 @@ function parseLine(line) {
330
344
  }
331
345
  return nodes;
332
346
  }
347
+ function extractFromMarkdown(markdown) {
348
+ const doc = parseFromMarkdown(markdown);
349
+ return {
350
+ tokens: extractTokens(doc),
351
+ plainText: extractPlainText(doc)
352
+ };
353
+ }
333
354
 
334
355
  // src/hooks/useMentionsEditor.ts
335
356
  function buildOutput(editor) {
@@ -343,6 +364,7 @@ function buildOutput(editor) {
343
364
  function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
344
365
  return Extension2.create({
345
366
  name: "submitShortcut",
367
+ priority: 150,
346
368
  addKeyboardShortcuts() {
347
369
  return {
348
370
  "Mod-Enter": () => {
@@ -358,22 +380,35 @@ function createSubmitExtension(onSubmitRef, clearOnSubmitRef) {
358
380
  }
359
381
  });
360
382
  }
383
+ var enterSubmitPluginKey = new PluginKey2("enterSubmit");
361
384
  function createEnterExtension(onSubmitRef, clearOnSubmitRef) {
362
385
  return Extension2.create({
363
386
  name: "enterSubmit",
364
- priority: 50,
365
- addKeyboardShortcuts() {
366
- return {
367
- Enter: () => {
368
- if (onSubmitRef.current) {
369
- onSubmitRef.current(buildOutput(this.editor));
370
- if (clearOnSubmitRef.current) {
371
- this.editor.commands.clearContent(true);
387
+ priority: 150,
388
+ addProseMirrorPlugins() {
389
+ const editor = this.editor;
390
+ return [
391
+ new Plugin2({
392
+ key: enterSubmitPluginKey,
393
+ props: {
394
+ handleKeyDown(_view, event) {
395
+ if (event.key !== "Enter") return false;
396
+ if (event.shiftKey) {
397
+ editor.commands.splitBlock();
398
+ return true;
399
+ }
400
+ if (event.metaKey || event.ctrlKey) return false;
401
+ if (onSubmitRef.current) {
402
+ onSubmitRef.current(buildOutput(editor));
403
+ if (clearOnSubmitRef.current) {
404
+ editor.commands.clearContent(true);
405
+ }
406
+ }
407
+ return true;
372
408
  }
373
409
  }
374
- return true;
375
- }
376
- };
410
+ })
411
+ ];
377
412
  }
378
413
  });
379
414
  }
@@ -426,10 +461,12 @@ function useMentionsEditor({
426
461
  bulletList: false,
427
462
  orderedList: false,
428
463
  listItem: false,
429
- horizontalRule: false
464
+ horizontalRule: false,
465
+ hardBreak: false
430
466
  }),
431
467
  Placeholder.configure({
432
- placeholder: placeholder ?? "Type a message..."
468
+ placeholder: placeholder ?? "Type a message...",
469
+ showOnlyCurrent: false
433
470
  }),
434
471
  MentionNode,
435
472
  suggestionExtension,
@@ -497,10 +534,17 @@ function useSuggestion(providers) {
497
534
  );
498
535
  const providerRef = useRef2(null);
499
536
  const fetchItems = useCallback2(
500
- async (provider, query, parent) => {
537
+ async (provider, query, parent, useSearchAll) => {
501
538
  setUIState((prev) => ({ ...prev, loading: true, state: "loading" }));
502
539
  try {
503
- const items = parent && provider.getChildren ? await provider.getChildren(parent, query) : await provider.getRootItems(query);
540
+ let items;
541
+ if (useSearchAll && provider.searchAll) {
542
+ items = await provider.searchAll(query);
543
+ } else if (parent && provider.getChildren) {
544
+ items = await provider.getChildren(parent, query);
545
+ } else {
546
+ items = await provider.getRootItems(query);
547
+ }
504
548
  setUIState((prev) => ({
505
549
  ...prev,
506
550
  items,
@@ -537,7 +581,11 @@ function useSuggestion(providers) {
537
581
  trigger: props.trigger,
538
582
  query: props.query
539
583
  });
540
- fetchItems(provider, props.query);
584
+ if (props.query.trim() && provider.searchAll) {
585
+ fetchItems(provider, props.query, void 0, true);
586
+ } else {
587
+ fetchItems(provider, props.query);
588
+ }
541
589
  },
542
590
  [fetchItems]
543
591
  );
@@ -546,12 +594,25 @@ function useSuggestion(providers) {
546
594
  const provider = providerRef.current;
547
595
  if (!provider) return;
548
596
  commandRef.current = props.command;
549
- setUIState((prev) => ({
550
- ...prev,
551
- clientRect: props.clientRect,
552
- query: props.query
553
- }));
554
- if (stateRef.current.breadcrumbs.length === 0) {
597
+ const current = stateRef.current;
598
+ if (current.breadcrumbs.length > 0) {
599
+ setUIState((prev) => ({
600
+ ...prev,
601
+ breadcrumbs: [],
602
+ clientRect: props.clientRect,
603
+ query: props.query,
604
+ activeIndex: 0
605
+ }));
606
+ } else {
607
+ setUIState((prev) => ({
608
+ ...prev,
609
+ clientRect: props.clientRect,
610
+ query: props.query
611
+ }));
612
+ }
613
+ if (props.query.trim() && provider.searchAll) {
614
+ fetchItems(provider, props.query, void 0, true);
615
+ } else {
555
616
  fetchItems(provider, props.query);
556
617
  }
557
618
  },
@@ -593,10 +654,12 @@ function useSuggestion(providers) {
593
654
  return;
594
655
  }
595
656
  if (commandRef.current) {
657
+ const rootLabel = current.breadcrumbs.length > 0 ? current.breadcrumbs[0].label : selected.rootLabel ?? null;
596
658
  commandRef.current({
597
659
  id: selected.id,
598
660
  label: selected.label,
599
- entityType: selected.type
661
+ entityType: selected.type,
662
+ rootLabel
600
663
  });
601
664
  }
602
665
  },
@@ -625,6 +688,18 @@ function useSuggestion(providers) {
625
688
  const close = useCallback2(() => {
626
689
  setUIState(IDLE_STATE);
627
690
  }, []);
691
+ const searchNested = useCallback2(
692
+ (query) => {
693
+ const provider = providerRef.current;
694
+ if (!provider) return;
695
+ const current = stateRef.current;
696
+ const parent = current.breadcrumbs[current.breadcrumbs.length - 1];
697
+ if (parent) {
698
+ fetchItems(provider, query, parent);
699
+ }
700
+ },
701
+ [fetchItems]
702
+ );
628
703
  const onKeyDown = useCallback2(
629
704
  ({ event }) => {
630
705
  const current = stateRef.current;
@@ -684,13 +759,14 @@ function useSuggestion(providers) {
684
759
  navigateDown,
685
760
  select,
686
761
  goBack,
687
- close
762
+ close,
763
+ searchNested
688
764
  };
689
765
  return { uiState, actions, callbacksRef };
690
766
  }
691
767
 
692
768
  // src/components/SuggestionList.tsx
693
- import { useEffect as useEffect2, useRef as useRef3 } from "react";
769
+ import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
694
770
 
695
771
  // src/utils/ariaHelpers.ts
696
772
  function comboboxAttrs(expanded, listboxId) {
@@ -730,10 +806,29 @@ function SuggestionList({
730
806
  onSelect,
731
807
  onHover,
732
808
  onGoBack,
809
+ onSearchNested,
810
+ onNavigateUp,
811
+ onNavigateDown,
812
+ onClose,
733
813
  renderItem
734
814
  }) {
735
815
  const listRef = useRef3(null);
816
+ const searchInputRef = useRef3(null);
736
817
  const depth = breadcrumbs.length;
818
+ const [nestedQuery, setNestedQuery] = useState2("");
819
+ const breadcrumbKey = breadcrumbs.map((b) => b.id).join("/");
820
+ const prevBreadcrumbKey = useRef3(breadcrumbKey);
821
+ useEffect2(() => {
822
+ if (prevBreadcrumbKey.current !== breadcrumbKey) {
823
+ setNestedQuery("");
824
+ prevBreadcrumbKey.current = breadcrumbKey;
825
+ }
826
+ }, [breadcrumbKey]);
827
+ useEffect2(() => {
828
+ if (breadcrumbs.length > 0 && searchInputRef.current) {
829
+ requestAnimationFrame(() => searchInputRef.current?.focus());
830
+ }
831
+ }, [breadcrumbKey, breadcrumbs.length]);
737
832
  useEffect2(() => {
738
833
  if (!listRef.current) return;
739
834
  const active = listRef.current.querySelector('[aria-selected="true"]');
@@ -741,6 +836,35 @@ function SuggestionList({
741
836
  }, [activeIndex]);
742
837
  const style = usePopoverPosition(clientRect);
743
838
  if (items.length === 0 && !loading) return null;
839
+ const handleSearchKeyDown = (e) => {
840
+ switch (e.key) {
841
+ case "ArrowDown":
842
+ e.preventDefault();
843
+ onNavigateDown?.();
844
+ break;
845
+ case "ArrowUp":
846
+ e.preventDefault();
847
+ onNavigateUp?.();
848
+ break;
849
+ case "Enter": {
850
+ e.preventDefault();
851
+ e.stopPropagation();
852
+ const selectedItem = items[activeIndex];
853
+ if (selectedItem) onSelect(selectedItem);
854
+ break;
855
+ }
856
+ case "Escape":
857
+ e.preventDefault();
858
+ onClose?.();
859
+ break;
860
+ case "Backspace":
861
+ if (nestedQuery === "") {
862
+ e.preventDefault();
863
+ onGoBack();
864
+ }
865
+ break;
866
+ }
867
+ };
744
868
  return /* @__PURE__ */ jsxs(
745
869
  "div",
746
870
  {
@@ -765,6 +889,24 @@ function SuggestionList({
765
889
  crumb.label
766
890
  ] }, crumb.id))
767
891
  ] }),
892
+ breadcrumbs.length > 0 && /* @__PURE__ */ jsx("div", { "data-suggestion-search": "", children: /* @__PURE__ */ jsx(
893
+ "input",
894
+ {
895
+ ref: searchInputRef,
896
+ type: "text",
897
+ "data-suggestion-search-input": "",
898
+ placeholder: "Search...",
899
+ value: nestedQuery,
900
+ onChange: (e) => {
901
+ const q = e.target.value;
902
+ setNestedQuery(q);
903
+ onSearchNested?.(q);
904
+ },
905
+ onKeyDown: handleSearchKeyDown,
906
+ autoComplete: "off",
907
+ spellCheck: false
908
+ }
909
+ ) }),
768
910
  loading && /* @__PURE__ */ jsx("div", { "data-suggestion-loading": "", children: "Loading..." }),
769
911
  !loading && /* @__PURE__ */ jsx("div", { ...listboxAttrs(LISTBOX_ID, `${trigger ?? ""} suggestions`), children: items.map((item, index) => {
770
912
  const isActive = index === activeIndex;
@@ -847,7 +989,8 @@ var MentionsInput = forwardRef(
847
989
  [clear, setContent, focus]
848
990
  );
849
991
  const isExpanded = uiState.state !== "idle";
850
- const handleHover = useCallback3((_index) => {
992
+ const handleHover = useCallback3((index) => {
993
+ void index;
851
994
  }, []);
852
995
  return /* @__PURE__ */ jsxs2(
853
996
  "div",
@@ -871,6 +1014,10 @@ var MentionsInput = forwardRef(
871
1014
  onSelect: (item) => actions.select(item),
872
1015
  onHover: handleHover,
873
1016
  onGoBack: actions.goBack,
1017
+ onSearchNested: actions.searchNested,
1018
+ onNavigateUp: actions.navigateUp,
1019
+ onNavigateDown: actions.navigateDown,
1020
+ onClose: actions.close,
874
1021
  renderItem
875
1022
  }
876
1023
  )
@@ -881,6 +1028,7 @@ var MentionsInput = forwardRef(
881
1028
  );
882
1029
  export {
883
1030
  MentionsInput,
1031
+ extractFromMarkdown,
884
1032
  parseFromMarkdown,
885
1033
  serializeToMarkdown
886
1034
  };