@dxos/react-ui-editor 0.14.0 → 0.16.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.
Files changed (25) hide show
  1. package/dist/lib/index.mjs +82 -70
  2. package/dist/lib/index.mjs.map +1 -1
  3. package/dist/types/src/components/EditorMenuProvider/menu.d.ts +2 -0
  4. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -1
  5. package/dist/types/src/components/EditorMenuProvider/popover.d.ts +7 -1
  6. package/dist/types/src/components/EditorMenuProvider/popover.d.ts.map +1 -1
  7. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts +2 -2
  8. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -1
  9. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.test.d.ts +2 -0
  10. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.test.d.ts.map +1 -0
  11. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  12. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  13. package/dist/types/src/stories/Widgets.stories.d.ts +5 -0
  14. package/dist/types/src/stories/Widgets.stories.d.ts.map +1 -1
  15. package/dist/types/tsconfig.tsbuildinfo +1 -1
  16. package/package.json +33 -33
  17. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +1 -1
  18. package/src/components/EditorMenuProvider/menu.ts +2 -0
  19. package/src/components/EditorMenuProvider/popover.ts +12 -3
  20. package/src/components/EditorMenuProvider/useEditorMenu.test.tsx +125 -0
  21. package/src/components/EditorMenuProvider/useEditorMenu.ts +79 -56
  22. package/src/hooks/useTextEditor.test.tsx +45 -4
  23. package/src/hooks/useTextEditor.ts +37 -9
  24. package/src/stories/Comments.stories.tsx +25 -21
  25. package/src/stories/Widgets.stories.tsx +59 -0
@@ -115,7 +115,7 @@ var popover = (options = {}) => {
115
115
  (options.activateOnTyping || options.activateOnDelimiters?.length) && popoverAutoActivate(options),
116
116
  popoverAnchorDecoration(options),
117
117
  modalStateField,
118
- options.trigger && placeholder({
118
+ options.trigger && options.placeholder !== false && placeholder({
119
119
  content: `Press '${Array.isArray(options.trigger) ? options.trigger[0] : options.trigger}' for commands`,
120
120
  focusOnly: true,
121
121
  ...options.placeholder
@@ -129,8 +129,9 @@ var popoverTriggerListener = (options) => EditorView$1.updateListener.of(({ view
129
129
  const { range: activeRange, trigger } = view.state.field(popoverStateField) ?? {};
130
130
  if (!activeRange) return;
131
131
  const text = view.state.doc.sliceString(activeRange.from, activeRange.to);
132
+ const query = trigger ? text.slice(1) : text;
132
133
  const selection = view.state.selection.main;
133
- const shouldClose = (trigger ? trigger !== text[0] : false) || /\s/.test(trigger ? text.slice(1) : text) || selection.head < activeRange.from || selection.head > activeRange.to + 1;
134
+ const shouldClose = (trigger ? trigger !== text[0] : false) || (trigger && options.multiWordTriggers?.includes(trigger) ? /^\s|[^\S ]| {2}/.test(query) : /\s/.test(query)) || selection.head < activeRange.from || selection.head > activeRange.to + 1;
134
135
  const nextRange = shouldClose ? null : docChanged ? {
135
136
  from: activeRange.from,
136
137
  to: selection.head
@@ -596,7 +597,10 @@ var MenuItem = ({ item, current, onSelect }) => {
596
597
  className: "dx-menu-item",
597
598
  "data-highlighted": current ? "" : void 0,
598
599
  onClick: handleSelect,
599
- children: [item.icon && /* @__PURE__ */ jsx(Icon.Icon, { icon: item.icon }), /* @__PURE__ */ jsx("span", {
600
+ children: [item.icon && /* @__PURE__ */ jsx(Icon.Icon, {
601
+ icon: item.icon,
602
+ hue: item.iconHue
603
+ }), /* @__PURE__ */ jsx("span", {
600
604
  className: "dx-menu-item-text",
601
605
  children: Theme.toLocalizedString(item.label, t)
602
606
  })]
@@ -616,7 +620,7 @@ var __dxlog_file$2 = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/components/
616
620
  * );
617
621
  * ```
618
622
  */
619
- var useEditorMenu = ({ trigger, triggerKey, placeholder, activateOnTyping, activateOnDelimiters, filter = true, searchTriggers, getMenu }) => {
623
+ var useEditorMenu = ({ trigger, triggerKey, placeholder, activateOnTyping, activateOnDelimiters, multiWordTriggers, filter = true, searchTriggers, getMenu }) => {
620
624
  const groupsRef = useRef([]);
621
625
  const currentRef = useRef(null);
622
626
  const [currentItem, setCurrentItem] = useState();
@@ -626,17 +630,44 @@ var useEditorMenu = ({ trigger, triggerKey, placeholder, activateOnTyping, activ
626
630
  const [_, refresh] = useState({});
627
631
  const contextRef = useRef(null);
628
632
  const requestRef = useRef(0);
633
+ const getMenuRef = useRef(getMenu);
634
+ useLayoutEffect(() => {
635
+ getMenuRef.current = getMenu;
636
+ }, [getMenu]);
629
637
  /**
630
638
  * Get filtered options.
631
639
  */
632
640
  const getMenuOptions = useCallback(async ({ text, trigger, ...props }) => {
633
- const groups = await getMenu?.({
641
+ const groups = await getMenuRef.current?.({
634
642
  text,
635
643
  trigger,
636
644
  ...props
637
645
  }) ?? [];
638
646
  return filter && trigger !== "@" ? filterMenuGroups(groups, (item) => text ? item.label.toLowerCase().startsWith(text.toLowerCase()) : true) : groups;
639
- }, [getMenu, filter]);
647
+ }, [filter]);
648
+ const handleOpenChange = useCallback(async ({ view, open }) => {
649
+ invariant(view, void 0, {
650
+ "~LogMeta": "~LogMeta",
651
+ F: __dxlog_file$2,
652
+ L: 128,
653
+ S: void 0,
654
+ A: ["view", ""]
655
+ });
656
+ setOpen(open);
657
+ if (!open) {
658
+ setCurrentItem(void 0);
659
+ setSearch(false);
660
+ setQuery("");
661
+ contextRef.current = null;
662
+ requestRef.current++;
663
+ groupsRef.current = [];
664
+ currentRef.current = null;
665
+ view.dispatch({ effects: [popoverRangeEffect.of(null)] });
666
+ }
667
+ requestAnimationFrame(() => {
668
+ view.dispatch({ effects: [modalStateEffect.of(open)] });
669
+ });
670
+ }, [getMenuOptions]);
640
671
  /**
641
672
  * Recompute the menu and preselect the first item.
642
673
  */
@@ -660,29 +691,6 @@ var useEditorMenu = ({ trigger, triggerKey, placeholder, activateOnTyping, activ
660
691
  }
661
692
  refresh({});
662
693
  }, [getMenuOptions]);
663
- const handleOpenChange = useCallback(async ({ view, open }) => {
664
- invariant(view, void 0, {
665
- "~LogMeta": "~LogMeta",
666
- F: __dxlog_file$2,
667
- L: 137,
668
- S: void 0,
669
- A: ["view", ""]
670
- });
671
- setOpen(open);
672
- if (!open) {
673
- setCurrentItem(void 0);
674
- setSearch(false);
675
- setQuery("");
676
- contextRef.current = null;
677
- requestRef.current++;
678
- groupsRef.current = [];
679
- currentRef.current = null;
680
- view.dispatch({ effects: [popoverRangeEffect.of(null)] });
681
- }
682
- requestAnimationFrame(() => {
683
- view.dispatch({ effects: [modalStateEffect.of(open)] });
684
- });
685
- }, [getMenuOptions]);
686
694
  const handleActivate = useCallback(async ({ view, trigger }) => {
687
695
  const item = getMenuItem(groupsRef.current, currentItem);
688
696
  if (item) currentRef.current = item;
@@ -707,7 +715,7 @@ var useEditorMenu = ({ trigger, triggerKey, placeholder, activateOnTyping, activ
707
715
  }, []);
708
716
  const handleCancel = useCallback(({ view }) => {
709
717
  const { range, trigger } = view.state.field(popoverStateField) ?? {};
710
- if (range && trigger) view.dispatch({ changes: {
718
+ if (range && trigger && !contextRef.current?.multiWord) view.dispatch({ changes: {
711
719
  ...range,
712
720
  insert: ""
713
721
  } });
@@ -728,18 +736,23 @@ var useEditorMenu = ({ trigger, triggerKey, placeholder, activateOnTyping, activ
728
736
  return next.id;
729
737
  });
730
738
  }, []);
731
- const serializedTrigger = Array.isArray(trigger) ? trigger.join(",") : trigger;
732
- const serializedSearchTriggers = searchTriggers?.join(",");
733
- const serializedDelimiters = activateOnDelimiters?.join(",");
739
+ const arraysKey = JSON.stringify({
740
+ trigger,
741
+ searchTriggers,
742
+ activateOnDelimiters,
743
+ multiWordTriggers
744
+ });
734
745
  return {
735
746
  groupsRef,
736
747
  extension: useMemo(() => {
748
+ const arrays = JSON.parse(arraysKey);
737
749
  return popover({
738
- trigger,
750
+ trigger: arrays.trigger,
739
751
  triggerKey,
740
752
  placeholder,
741
753
  activateOnTyping,
742
- activateOnDelimiters,
754
+ activateOnDelimiters: arrays.activateOnDelimiters,
755
+ multiWordTriggers: arrays.multiWordTriggers,
743
756
  onClose: ({ view }) => handleOpenChange({
744
757
  view,
745
758
  open: false
@@ -757,29 +770,25 @@ var useEditorMenu = ({ trigger, triggerKey, placeholder, activateOnTyping, activ
757
770
  onArrowUp: () => handleNavigate("up"),
758
771
  onArrowDown: () => handleNavigate("down"),
759
772
  onTextChange: async ({ view, pos, text, trigger }) => {
760
- contextRef.current = {
773
+ const context = {
761
774
  view,
762
775
  pos,
763
776
  text,
764
- trigger
777
+ trigger,
778
+ multiWord: !!trigger && !!arrays.multiWordTriggers?.includes(trigger)
765
779
  };
766
- setSearch(!!trigger && !!searchTriggers?.includes(trigger));
780
+ contextRef.current = context;
781
+ setSearch(!!trigger && !!arrays.searchTriggers?.includes(trigger));
767
782
  setQuery("");
768
- await updateGroups({
769
- view,
770
- pos,
771
- text,
772
- trigger
773
- });
783
+ await updateGroups(context);
774
784
  }
775
785
  });
776
786
  }, [
777
787
  handleOpenChange,
778
788
  handleNavigate,
779
789
  updateGroups,
780
- serializedTrigger,
781
- serializedSearchTriggers,
782
- serializedDelimiters,
790
+ arraysKey,
791
+ triggerKey,
783
792
  placeholder,
784
793
  activateOnTyping
785
794
  ]),
@@ -1090,6 +1099,8 @@ var useBasicMarkdownExtensions = ({ placeholder, decorate = true, readonly = fal
1090
1099
  //#region src/hooks/useTextEditor.ts
1091
1100
  var __dxlog_file$1 = "/__w/dxos/dxos/packages/ui/react-ui-editor/src/hooks/useTextEditor.ts";
1092
1101
  var instanceCount = 0;
1102
+ /** Whether every position in a recorded state lies within a document of `length`. */
1103
+ var fitsDocument = ({ scrollTo, selection }, length) => (scrollTo ?? 0) <= length && (selection?.anchor ?? 0) <= length && (selection?.head ?? 0) <= length;
1093
1104
  /**
1094
1105
  * Creates codemirror text editor.
1095
1106
  */
@@ -1108,7 +1119,7 @@ var useTextEditor = (props = {}, deps = []) => {
1108
1119
  }, {
1109
1120
  "~LogMeta": "~LogMeta",
1110
1121
  F: __dxlog_file$1,
1111
- L: 82,
1122
+ L: 87,
1112
1123
  S: void 0
1113
1124
  });
1114
1125
  const length = (doc ?? initialValue)?.length ?? 0;
@@ -1125,7 +1136,7 @@ var useTextEditor = (props = {}, deps = []) => {
1125
1136
  log.catch(err, void 0, {
1126
1137
  "~LogMeta": "~LogMeta",
1127
1138
  F: __dxlog_file$1,
1128
- L: 103,
1139
+ L: 108,
1129
1140
  S: void 0
1130
1141
  });
1131
1142
  })
@@ -1150,35 +1161,36 @@ var useTextEditor = (props = {}, deps = []) => {
1150
1161
  log("destroy", { id }, {
1151
1162
  "~LogMeta": "~LogMeta",
1152
1163
  F: __dxlog_file$1,
1153
- L: 127,
1164
+ L: 132,
1154
1165
  S: void 0
1155
1166
  });
1156
1167
  view?.destroy();
1157
1168
  };
1158
1169
  }, deps);
1159
1170
  useEffect(() => {
1160
- if (view) {
1161
- if (scrollTo != null || selection) {
1162
- if (selection && selection.anchor > view.state.doc.length) {
1163
- log.warn("invalid selection", {
1164
- length: view.state.doc.length,
1165
- scrollTo,
1166
- selection
1167
- }, {
1168
- "~LogMeta": "~LogMeta",
1169
- F: __dxlog_file$1,
1170
- L: 136,
1171
- S: void 0
1172
- });
1173
- return;
1174
- }
1175
- restoreEditorState(view, {
1176
- scrollTo,
1177
- scrollOffset,
1178
- selection
1171
+ if (!view || scrollTo == null && !selection) return;
1172
+ const state = {
1173
+ scrollTo,
1174
+ scrollOffset,
1175
+ selection
1176
+ };
1177
+ if (fitsDocument(state, view.state.doc.length)) {
1178
+ restoreEditorState(view, state);
1179
+ return;
1180
+ }
1181
+ let active = true;
1182
+ let pending = true;
1183
+ view.dispatch({ effects: StateEffect.appendConfig.of(EditorView$1.updateListener.of((update) => {
1184
+ if (pending && update.docChanged && fitsDocument(state, update.state.doc.length)) {
1185
+ pending = false;
1186
+ queueMicrotask(() => {
1187
+ if (active) restoreEditorState(update.view, state);
1179
1188
  });
1180
1189
  }
1181
- }
1190
+ })) });
1191
+ return () => {
1192
+ active = false;
1193
+ };
1182
1194
  }, [
1183
1195
  view,
1184
1196
  scrollTo,