@dxos/react-ui-editor 0.8.4-main.5ad4a44 → 0.8.4-main.66e292d

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 (280) hide show
  1. package/dist/lib/browser/index.mjs +2979 -2478
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +2979 -2478
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Editor/Editor.d.ts +37 -21
  8. package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
  9. package/dist/types/src/components/Editor/Editor.stories.d.ts +20 -0
  10. package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -0
  11. package/dist/types/src/components/EditorContent/EditorContent.d.ts +29 -0
  12. package/dist/types/src/components/EditorContent/EditorContent.d.ts.map +1 -0
  13. package/dist/types/src/components/EditorContent/EditorContent.stories.d.ts +26 -0
  14. package/dist/types/src/components/EditorContent/EditorContent.stories.d.ts.map +1 -0
  15. package/dist/types/src/components/EditorContent/controller.d.ts +10 -0
  16. package/dist/types/src/components/EditorContent/controller.d.ts.map +1 -0
  17. package/dist/types/src/components/EditorContent/index.d.ts +3 -0
  18. package/dist/types/src/components/EditorContent/index.d.ts.map +1 -0
  19. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts +36 -0
  20. package/dist/types/src/components/EditorMenuProvider/EditorMenuProvider.d.ts.map +1 -0
  21. package/dist/types/src/components/EditorMenuProvider/index.d.ts +7 -0
  22. package/dist/types/src/components/EditorMenuProvider/index.d.ts.map +1 -0
  23. package/dist/types/src/components/EditorMenuProvider/menu-presets.d.ts +4 -0
  24. package/dist/types/src/components/EditorMenuProvider/menu-presets.d.ts.map +1 -0
  25. package/dist/types/src/components/EditorMenuProvider/menu.d.ts +28 -0
  26. package/dist/types/src/components/EditorMenuProvider/menu.d.ts.map +1 -0
  27. package/dist/types/src/components/EditorMenuProvider/popover.d.ts +47 -0
  28. package/dist/types/src/components/EditorMenuProvider/popover.d.ts.map +1 -0
  29. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts +34 -0
  30. package/dist/types/src/components/EditorMenuProvider/useEditorMenu.d.ts.map +1 -0
  31. package/dist/types/src/components/EditorMenuProvider/util.d.ts +8 -0
  32. package/dist/types/src/components/EditorMenuProvider/util.d.ts.map +1 -0
  33. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts +16 -0
  34. package/dist/types/src/components/EditorPreviewProvider/EditorPreviewProvider.d.ts.map +1 -0
  35. package/dist/types/src/components/EditorPreviewProvider/index.d.ts +2 -0
  36. package/dist/types/src/components/EditorPreviewProvider/index.d.ts.map +1 -0
  37. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +26 -2
  38. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  39. package/dist/types/src/components/EditorToolbar/actions.d.ts +39 -0
  40. package/dist/types/src/components/EditorToolbar/actions.d.ts.map +1 -0
  41. package/dist/types/src/components/EditorToolbar/blocks.d.ts +3 -3
  42. package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
  43. package/dist/types/src/components/EditorToolbar/formatting.d.ts +3 -3
  44. package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
  45. package/dist/types/src/components/EditorToolbar/headings.d.ts +3 -3
  46. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  47. package/dist/types/src/components/EditorToolbar/index.d.ts +2 -1
  48. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  49. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts +11 -0
  50. package/dist/types/src/components/EditorToolbar/useEditorToolbar.d.ts.map +1 -0
  51. package/dist/types/src/components/EditorToolbar/view-mode.d.ts +3 -3
  52. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -1
  53. package/dist/types/src/components/index.d.ts +4 -2
  54. package/dist/types/src/components/index.d.ts.map +1 -1
  55. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts +17 -0
  56. package/dist/types/src/extensions/autocomplete/autocomplete.d.ts.map +1 -0
  57. package/dist/types/src/extensions/autocomplete/index.d.ts +5 -0
  58. package/dist/types/src/extensions/autocomplete/index.d.ts.map +1 -0
  59. package/dist/types/src/extensions/autocomplete/match.d.ts +13 -0
  60. package/dist/types/src/extensions/autocomplete/match.d.ts.map +1 -0
  61. package/dist/types/src/extensions/autocomplete/placeholder.d.ts +20 -0
  62. package/dist/types/src/extensions/autocomplete/placeholder.d.ts.map +1 -0
  63. package/dist/types/src/extensions/autocomplete/typeahead.d.ts +10 -0
  64. package/dist/types/src/extensions/autocomplete/typeahead.d.ts.map +1 -0
  65. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  66. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  67. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  68. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  69. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  70. package/dist/types/src/extensions/automerge/sync.d.ts +3 -3
  71. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  72. package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
  73. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  74. package/dist/types/src/extensions/autoscroll.d.ts +14 -4
  75. package/dist/types/src/extensions/autoscroll.d.ts.map +1 -1
  76. package/dist/types/src/extensions/awareness/awareness-provider.d.ts +4 -4
  77. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  78. package/dist/types/src/extensions/blocks.d.ts +2 -0
  79. package/dist/types/src/extensions/blocks.d.ts.map +1 -0
  80. package/dist/types/src/extensions/bookmarks.d.ts +12 -0
  81. package/dist/types/src/extensions/bookmarks.d.ts.map +1 -0
  82. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  83. package/dist/types/src/extensions/factories.d.ts +7 -6
  84. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  85. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  86. package/dist/types/src/extensions/index.d.ts +8 -3
  87. package/dist/types/src/extensions/index.d.ts.map +1 -1
  88. package/dist/types/src/extensions/listener.d.ts +8 -6
  89. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  90. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  91. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -3
  92. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  93. package/dist/types/src/extensions/modal.d.ts +7 -0
  94. package/dist/types/src/extensions/modal.d.ts.map +1 -0
  95. package/dist/types/src/extensions/modes.d.ts +1 -1
  96. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  97. package/dist/types/src/extensions/outliner/menu.d.ts +8 -0
  98. package/dist/types/src/extensions/outliner/menu.d.ts.map +1 -0
  99. package/dist/types/src/extensions/preview/preview.d.ts +6 -3
  100. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -1
  101. package/dist/types/src/extensions/replacer.d.ts +21 -0
  102. package/dist/types/src/extensions/replacer.d.ts.map +1 -0
  103. package/dist/types/src/extensions/replacer.test.d.ts +2 -0
  104. package/dist/types/src/extensions/replacer.test.d.ts.map +1 -0
  105. package/dist/types/src/extensions/scrolling.d.ts +78 -0
  106. package/dist/types/src/extensions/scrolling.d.ts.map +1 -0
  107. package/dist/types/src/extensions/state.d.ts +2 -0
  108. package/dist/types/src/extensions/state.d.ts.map +1 -0
  109. package/dist/types/src/extensions/submit.d.ts +10 -0
  110. package/dist/types/src/extensions/submit.d.ts.map +1 -0
  111. package/dist/types/src/extensions/tab.d.ts +4 -0
  112. package/dist/types/src/extensions/tab.d.ts.map +1 -0
  113. package/dist/types/src/extensions/tags/xml-tags.d.ts +41 -16
  114. package/dist/types/src/extensions/tags/xml-tags.d.ts.map +1 -1
  115. package/dist/types/src/hooks/useTextEditor.d.ts +3 -7
  116. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  117. package/dist/types/src/stories/CommandDialog.stories.d.ts.map +1 -1
  118. package/dist/types/src/stories/Comments.stories.d.ts +3 -3
  119. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -1
  120. package/dist/types/src/stories/Experimental.stories.d.ts +3 -3
  121. package/dist/types/src/stories/Markdown.stories.d.ts +3 -3
  122. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -1
  123. package/dist/types/src/stories/{CommandMenu.stories.d.ts → Popover.stories.d.ts} +6 -5
  124. package/dist/types/src/stories/Popover.stories.d.ts.map +1 -0
  125. package/dist/types/src/stories/Preview.stories.d.ts +4 -3
  126. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -1
  127. package/dist/types/src/stories/Tags.stories.d.ts.map +1 -1
  128. package/dist/types/src/stories/TextEditor.stories.d.ts +3 -4
  129. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -1
  130. package/dist/types/src/stories/components/EditorStory.d.ts +11 -8
  131. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -1
  132. package/dist/types/src/stories/components/util.d.ts.map +1 -1
  133. package/dist/types/src/styles/theme.d.ts.map +1 -1
  134. package/dist/types/src/util/debug.d.ts +4 -0
  135. package/dist/types/src/util/debug.d.ts.map +1 -1
  136. package/dist/types/tsconfig.tsbuildinfo +1 -1
  137. package/package.json +44 -46
  138. package/src/components/Editor/Editor.stories.tsx +89 -0
  139. package/src/components/Editor/Editor.tsx +157 -57
  140. package/src/components/EditorContent/EditorContent.stories.tsx +70 -0
  141. package/src/components/EditorContent/EditorContent.tsx +70 -0
  142. package/src/components/EditorContent/controller.ts +50 -0
  143. package/src/components/EditorContent/index.ts +6 -0
  144. package/src/components/EditorMenuProvider/EditorMenuProvider.tsx +233 -0
  145. package/src/components/EditorMenuProvider/index.ts +11 -0
  146. package/src/components/EditorMenuProvider/menu-presets.ts +123 -0
  147. package/src/components/EditorMenuProvider/menu.ts +71 -0
  148. package/src/components/EditorMenuProvider/popover.ts +287 -0
  149. package/src/components/EditorMenuProvider/useEditorMenu.ts +175 -0
  150. package/src/components/EditorMenuProvider/util.ts +31 -0
  151. package/src/components/EditorPreviewProvider/EditorPreviewProvider.tsx +82 -0
  152. package/src/components/EditorPreviewProvider/index.ts +5 -0
  153. package/src/components/EditorToolbar/EditorToolbar.tsx +101 -92
  154. package/src/components/EditorToolbar/{lists.ts → actions.ts} +46 -16
  155. package/src/components/EditorToolbar/blocks.ts +2 -1
  156. package/src/components/EditorToolbar/formatting.ts +2 -1
  157. package/src/components/EditorToolbar/headings.ts +8 -5
  158. package/src/components/EditorToolbar/image.ts +1 -1
  159. package/src/components/EditorToolbar/index.ts +3 -7
  160. package/src/components/EditorToolbar/search.ts +1 -1
  161. package/src/components/EditorToolbar/useEditorToolbar.ts +20 -0
  162. package/src/components/EditorToolbar/view-mode.ts +2 -1
  163. package/src/components/index.ts +8 -2
  164. package/src/extensions/{autocomplete.ts → autocomplete/autocomplete.ts} +8 -76
  165. package/src/extensions/autocomplete/index.ts +8 -0
  166. package/src/extensions/autocomplete/match.ts +46 -0
  167. package/src/extensions/{command-menu → autocomplete}/placeholder.ts +21 -17
  168. package/src/extensions/{command-dialog → autocomplete}/typeahead.ts +6 -48
  169. package/src/extensions/automerge/automerge.stories.tsx +4 -3
  170. package/src/extensions/automerge/automerge.ts +28 -9
  171. package/src/extensions/automerge/cursor.ts +1 -1
  172. package/src/extensions/automerge/sync.ts +8 -4
  173. package/src/extensions/automerge/update-automerge.ts +1 -1
  174. package/src/extensions/autoscroll.ts +75 -69
  175. package/src/extensions/awareness/awareness-provider.ts +12 -12
  176. package/src/extensions/blocks.ts +131 -0
  177. package/src/extensions/bookmarks.ts +75 -0
  178. package/src/extensions/comments.ts +2 -1
  179. package/src/extensions/factories.ts +22 -24
  180. package/src/extensions/folding.tsx +1 -2
  181. package/src/extensions/index.ts +8 -3
  182. package/src/extensions/listener.ts +14 -20
  183. package/src/extensions/markdown/bundle.ts +14 -2
  184. package/src/extensions/markdown/decorate.ts +8 -8
  185. package/src/extensions/markdown/formatting.ts +8 -10
  186. package/src/extensions/markdown/highlight.ts +1 -1
  187. package/src/extensions/markdown/image.ts +2 -2
  188. package/src/extensions/markdown/table.ts +6 -6
  189. package/src/extensions/modal.ts +24 -0
  190. package/src/extensions/modes.ts +2 -2
  191. package/src/extensions/{floating-menu.ts → outliner/menu.ts} +7 -5
  192. package/src/extensions/outliner/outliner.test.ts +1 -1
  193. package/src/extensions/outliner/outliner.ts +3 -3
  194. package/src/extensions/outliner/tree.test.ts +1 -1
  195. package/src/extensions/preview/index.ts +1 -1
  196. package/src/extensions/preview/preview.ts +14 -11
  197. package/src/extensions/replacer.test.ts +75 -0
  198. package/src/extensions/replacer.ts +93 -0
  199. package/src/extensions/scrolling.ts +189 -0
  200. package/src/extensions/selection.ts +1 -1
  201. package/src/extensions/state.ts +7 -0
  202. package/src/extensions/submit.ts +62 -0
  203. package/src/extensions/tab.ts +29 -0
  204. package/src/extensions/tags/extended-markdown.test.ts +2 -1
  205. package/src/extensions/tags/xml-tags.ts +310 -203
  206. package/src/extensions/typewriter.ts +1 -1
  207. package/src/hooks/useTextEditor.ts +25 -25
  208. package/src/stories/CommandDialog.stories.tsx +13 -19
  209. package/src/stories/Comments.stories.tsx +3 -4
  210. package/src/stories/EditorToolbar.stories.tsx +8 -8
  211. package/src/stories/Experimental.stories.tsx +1 -1
  212. package/src/stories/Markdown.stories.tsx +1 -1
  213. package/src/stories/Outliner.stories.tsx +15 -15
  214. package/src/stories/Popover.stories.tsx +162 -0
  215. package/src/stories/Preview.stories.tsx +41 -37
  216. package/src/stories/Tags.stories.tsx +19 -5
  217. package/src/stories/TextEditor.stories.tsx +4 -30
  218. package/src/stories/components/EditorStory.tsx +31 -20
  219. package/src/stories/components/util.tsx +40 -8
  220. package/src/styles/markdown.ts +1 -1
  221. package/src/styles/theme.ts +2 -1
  222. package/src/util/debug.ts +5 -0
  223. package/dist/lib/browser/testing/index.mjs +0 -76
  224. package/dist/lib/browser/testing/index.mjs.map +0 -7
  225. package/dist/lib/node-esm/testing/index.mjs +0 -78
  226. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  227. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts +0 -38
  228. package/dist/types/src/components/CommandMenu/CommandMenu.d.ts.map +0 -1
  229. package/dist/types/src/components/CommandMenu/index.d.ts +0 -2
  230. package/dist/types/src/components/CommandMenu/index.d.ts.map +0 -1
  231. package/dist/types/src/components/EditorToolbar/lists.d.ts +0 -19
  232. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +0 -1
  233. package/dist/types/src/components/EditorToolbar/util.d.ts +0 -51
  234. package/dist/types/src/components/EditorToolbar/util.d.ts.map +0 -1
  235. package/dist/types/src/extensions/autocomplete.d.ts +0 -26
  236. package/dist/types/src/extensions/autocomplete.d.ts.map +0 -1
  237. package/dist/types/src/extensions/command-dialog/action.d.ts +0 -17
  238. package/dist/types/src/extensions/command-dialog/action.d.ts.map +0 -1
  239. package/dist/types/src/extensions/command-dialog/command-dialog.d.ts +0 -6
  240. package/dist/types/src/extensions/command-dialog/command-dialog.d.ts.map +0 -1
  241. package/dist/types/src/extensions/command-dialog/hint.d.ts +0 -19
  242. package/dist/types/src/extensions/command-dialog/hint.d.ts.map +0 -1
  243. package/dist/types/src/extensions/command-dialog/index.d.ts +0 -4
  244. package/dist/types/src/extensions/command-dialog/index.d.ts.map +0 -1
  245. package/dist/types/src/extensions/command-dialog/state.d.ts +0 -16
  246. package/dist/types/src/extensions/command-dialog/state.d.ts.map +0 -1
  247. package/dist/types/src/extensions/command-dialog/typeahead.d.ts +0 -22
  248. package/dist/types/src/extensions/command-dialog/typeahead.d.ts.map +0 -1
  249. package/dist/types/src/extensions/command-menu/command-menu.d.ts +0 -20
  250. package/dist/types/src/extensions/command-menu/command-menu.d.ts.map +0 -1
  251. package/dist/types/src/extensions/command-menu/index.d.ts +0 -3
  252. package/dist/types/src/extensions/command-menu/index.d.ts.map +0 -1
  253. package/dist/types/src/extensions/command-menu/placeholder.d.ts +0 -10
  254. package/dist/types/src/extensions/command-menu/placeholder.d.ts.map +0 -1
  255. package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts +0 -24
  256. package/dist/types/src/extensions/command-menu/useCommandMenu.d.ts.map +0 -1
  257. package/dist/types/src/extensions/floating-menu.d.ts +0 -7
  258. package/dist/types/src/extensions/floating-menu.d.ts.map +0 -1
  259. package/dist/types/src/stories/CommandMenu.stories.d.ts.map +0 -1
  260. package/dist/types/src/testing/PreviewPopover.d.ts +0 -20
  261. package/dist/types/src/testing/PreviewPopover.d.ts.map +0 -1
  262. package/dist/types/src/testing/index.d.ts +0 -3
  263. package/dist/types/src/testing/index.d.ts.map +0 -1
  264. package/dist/types/src/testing/util.d.ts +0 -3
  265. package/dist/types/src/testing/util.d.ts.map +0 -1
  266. package/src/components/CommandMenu/CommandMenu.tsx +0 -348
  267. package/src/components/CommandMenu/index.ts +0 -5
  268. package/src/components/EditorToolbar/util.ts +0 -76
  269. package/src/extensions/command-dialog/action.ts +0 -55
  270. package/src/extensions/command-dialog/command-dialog.ts +0 -34
  271. package/src/extensions/command-dialog/hint.ts +0 -103
  272. package/src/extensions/command-dialog/index.ts +0 -7
  273. package/src/extensions/command-dialog/state.ts +0 -90
  274. package/src/extensions/command-menu/command-menu.ts +0 -210
  275. package/src/extensions/command-menu/index.ts +0 -6
  276. package/src/extensions/command-menu/useCommandMenu.ts +0 -134
  277. package/src/stories/CommandMenu.stories.tsx +0 -158
  278. package/src/testing/PreviewPopover.tsx +0 -78
  279. package/src/testing/index.ts +0 -6
  280. package/src/testing/util.ts +0 -7
@@ -0,0 +1,287 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension, Prec, RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
6
+ import {
7
+ Decoration,
8
+ type DecorationSet,
9
+ EditorView,
10
+ type KeyBinding,
11
+ ViewPlugin,
12
+ type ViewUpdate,
13
+ keymap,
14
+ } from '@codemirror/view';
15
+
16
+ import { isNonNullable, isTruthy } from '@dxos/util';
17
+
18
+ import { type PlaceholderOptions, modalStateField, placeholder } from '../../extensions';
19
+ import { type Range } from '../../types';
20
+
21
+ const DELIMITERS = [' ', ':'];
22
+
23
+ export type PopoverOptions = {
24
+ trigger?: string | string[];
25
+ triggerKey?: string;
26
+ placeholder?: Partial<PlaceholderOptions>;
27
+ delimiters?: string[];
28
+
29
+ // TODO(burdon): Auto.
30
+ // activateOnTyping?: boolean;
31
+
32
+ // Trigger update.
33
+ onTextChange?: (event: { view: EditorView; pos: number; text: string; trigger?: string }) => void;
34
+ onClose?: (event: { view: EditorView }) => void;
35
+
36
+ // Menu specific.
37
+ onEnter?: (event: { view: EditorView }) => void;
38
+ onArrowUp?: (event: { view: EditorView }) => void;
39
+ onArrowDown?: (event: { view: EditorView }) => void;
40
+ };
41
+
42
+ /**
43
+ * Creates a popover that appears when the trigger character is inserted.
44
+ * This can be used for context menus or autocompletion.
45
+ */
46
+ export const popover = (options: PopoverOptions = {}): Extension => {
47
+ return [
48
+ Prec.highest(popoverKeymap(options)),
49
+ popoverStateField,
50
+ popoverTriggerListener(options),
51
+ popoverAnchorDecoration(options),
52
+ modalStateField,
53
+ options.trigger &&
54
+ placeholder({
55
+ // TODO(burdon): Translations.
56
+ content: `Press '${Array.isArray(options.trigger) ? options.trigger[0] : options.trigger}' for commands`,
57
+ ...options.placeholder,
58
+ }),
59
+ ].filter(isTruthy);
60
+ };
61
+
62
+ /**
63
+ * Listen for selection and document changes.
64
+ */
65
+ const popoverTriggerListener = (options: PopoverOptions) =>
66
+ EditorView.updateListener.of(({ view, docChanged }) => {
67
+ const { range: activeRange, trigger } = view.state.field(popoverStateField) ?? {};
68
+ if (!activeRange) {
69
+ return;
70
+ }
71
+
72
+ const text = view.state.doc.sliceString(activeRange.from, activeRange.to);
73
+ const selection = view.state.selection.main;
74
+ const shouldClose =
75
+ // Trigger deleted.
76
+ (trigger ? trigger !== text[0] : false) ||
77
+ // Whitespace in text.
78
+ /\s/.test(trigger ? text.slice(1) : text) ||
79
+ // Cursor moved before the range.
80
+ selection.head < activeRange.from ||
81
+ // Cursor moved after the range (+1 to handle selection changing before doc).
82
+ selection.head > activeRange.to + 1;
83
+
84
+ const nextRange = shouldClose ? null : docChanged ? { from: activeRange.from, to: selection.head } : activeRange;
85
+ if (nextRange !== activeRange) {
86
+ view.dispatch({
87
+ effects: popoverRangeEffect.of(nextRange ? { range: nextRange, trigger } : null),
88
+ });
89
+ }
90
+
91
+ if (shouldClose) {
92
+ options.onClose?.({ view });
93
+ }
94
+ });
95
+
96
+ /**
97
+ * Popover navigation.
98
+ */
99
+ const popoverKeymap = (options: PopoverOptions) => {
100
+ const triggers = Array.isArray(options.trigger) ? options.trigger : [options.trigger];
101
+ return keymap.of(
102
+ [
103
+ // Prefix triggers.
104
+ ...triggers.filter(isNonNullable).map((trigger) => ({
105
+ key: trigger,
106
+ run: (view: EditorView) => {
107
+ // Determine if we should trigger the popover:
108
+ // 1. Empty lines or at the beginning of a line.
109
+ // 2. When there's a preceding space.
110
+ const selection = view.state.selection.main;
111
+ const line = view.state.doc.lineAt(selection.head);
112
+ if (
113
+ line.text.trim() === '' ||
114
+ selection.head === line.from ||
115
+ (selection.head > line.from && line.text[selection.head - line.from - 1] === ' ')
116
+ ) {
117
+ // Insert and select the trigger.
118
+ view.dispatch({
119
+ changes: { from: selection.head, insert: trigger },
120
+ selection: { anchor: selection.head + 1, head: selection.head + 1 },
121
+ effects: popoverRangeEffect.of({ trigger, range: { from: selection.head, to: selection.head + 1 } }),
122
+ });
123
+
124
+ return true;
125
+ }
126
+
127
+ return false;
128
+ },
129
+ })),
130
+
131
+ //
132
+ // Custom trigger.
133
+ //
134
+ options.triggerKey &&
135
+ ({
136
+ key: options.triggerKey,
137
+ run: (view: EditorView) => {
138
+ const selection = view.state.selection.main;
139
+ const line = view.state.doc.lineAt(selection.head);
140
+
141
+ // Get last word.
142
+ let str = line.text.slice(0, selection.head - line.from);
143
+ const idx = getLastIndexOf(str, options.delimiters ?? DELIMITERS);
144
+ if (idx !== -1) {
145
+ str = str.slice(idx + 1);
146
+ }
147
+
148
+ // Create anchor even if zero length (append space).
149
+ const from = line.from + idx;
150
+ view.dispatch({
151
+ effects: popoverRangeEffect.of({ range: { from: from + 1, to: selection.head } }),
152
+ changes:
153
+ selection.head === view.state.doc.length
154
+ ? { from: from + 1, to: selection.head, insert: ' ' }
155
+ : undefined,
156
+ });
157
+ return true;
158
+ },
159
+ } satisfies KeyBinding),
160
+
161
+ //
162
+ // Nav keys.
163
+ //
164
+ {
165
+ key: 'ArrowUp',
166
+ run: (view: EditorView) => {
167
+ const range = view.state.field(popoverStateField)?.range;
168
+ if (range) {
169
+ options.onArrowUp?.({ view });
170
+ return true;
171
+ }
172
+
173
+ return false;
174
+ },
175
+ },
176
+ {
177
+ key: 'ArrowDown',
178
+ run: (view: EditorView) => {
179
+ const range = view.state.field(popoverStateField)?.range;
180
+ if (range) {
181
+ options.onArrowDown?.({ view });
182
+ return true;
183
+ }
184
+
185
+ return false;
186
+ },
187
+ },
188
+ {
189
+ key: 'Enter',
190
+ run: (view: EditorView) => {
191
+ const range = view.state.field(popoverStateField)?.range;
192
+ if (range) {
193
+ view.dispatch({ changes: { from: range.from, to: range.to, insert: '' } });
194
+ options.onEnter?.({ view });
195
+ return true;
196
+ }
197
+
198
+ return false;
199
+ },
200
+ },
201
+ ].filter(isTruthy),
202
+ );
203
+ };
204
+
205
+ /**
206
+ * Creates a <dx-anchor> tag, which is used to anchor the Popver.
207
+ */
208
+ const popoverAnchorDecoration = (options: PopoverOptions) => {
209
+ return ViewPlugin.fromClass(
210
+ class {
211
+ _decorations: DecorationSet = Decoration.none;
212
+
213
+ constructor(readonly view: EditorView) {}
214
+
215
+ // TODO(wittjosiah): The decorations are repainted on every update, this occasionally causes menu to flicker.
216
+ update({ view, transactions }: ViewUpdate) {
217
+ const builder = new RangeSetBuilder<Decoration>();
218
+ const { range, trigger } = view.state.field(popoverStateField) ?? {};
219
+ if (range) {
220
+ // Check if we should show the widget (only if cursor is within the active command range).
221
+ const selection = view.state.selection.main;
222
+ const showWidget = selection.head >= range.from && selection.head <= range.to;
223
+ if (showWidget) {
224
+ builder.add(
225
+ range.from,
226
+ range.to + 1,
227
+ Decoration.mark({
228
+ tagName: 'dx-anchor',
229
+ class: 'cm-popover-trigger',
230
+ attributes: {
231
+ 'data-visible-focus': 'false',
232
+ 'data-auto-trigger': 'true',
233
+ 'data-trigger': trigger ?? options.triggerKey ?? '',
234
+ },
235
+ }),
236
+ );
237
+ }
238
+
239
+ const rangeChanged = transactions.some((tr) => tr.effects.some((effect) => effect.is(popoverRangeEffect)));
240
+ if (rangeChanged) {
241
+ // NOTE: Content skips the trigger character.
242
+ const content = view.state.sliceDoc(range.from + (trigger ? trigger.length : 0), range.to);
243
+ options.onTextChange?.({ view, pos: selection.head, text: content, trigger });
244
+ }
245
+ }
246
+
247
+ this._decorations = builder.finish();
248
+ }
249
+ },
250
+ {
251
+ decorations: (v) => v._decorations,
252
+ },
253
+ );
254
+ };
255
+
256
+ type PopoverState = {
257
+ /**
258
+ * Trigger prefix (in document).
259
+ */
260
+ trigger?: string;
261
+
262
+ /**
263
+ * Current document completion range.
264
+ */
265
+ range: Range;
266
+ };
267
+
268
+ // State effects for managing popover state.
269
+ export const popoverRangeEffect = StateEffect.define<PopoverState | null>();
270
+
271
+ // State field to track the active popover trigger range.
272
+ export const popoverStateField = StateField.define<PopoverState | null>({
273
+ create: () => null,
274
+ update: (value, tr) => {
275
+ let newValue = value;
276
+ for (const effect of tr.effects) {
277
+ if (effect.is(popoverRangeEffect)) {
278
+ newValue = effect.value;
279
+ }
280
+ }
281
+
282
+ return newValue;
283
+ },
284
+ });
285
+
286
+ const getLastIndexOf = (str: string, delimiters: string[]) =>
287
+ Math.max(...delimiters.map((delim) => str.lastIndexOf(delim)));
@@ -0,0 +1,175 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import { type EditorState } from '@codemirror/state';
7
+ import { type RefObject, useCallback, useMemo, useRef, useState } from 'react';
8
+
9
+ import { invariant } from '@dxos/invariant';
10
+ import { type MaybePromise } from '@dxos/util';
11
+
12
+ import { modalStateEffect } from '../../extensions';
13
+
14
+ import { type EditorMenuProviderProps } from './EditorMenuProvider';
15
+ import { type EditorMenuGroup, type EditorMenuItem } from './menu';
16
+ import { filterMenuGroups, getMenuItem, getNextMenuItem, getPreviousMenuItem } from './menu';
17
+ import { type PopoverOptions, popover, popoverRangeEffect, popoverStateField } from './popover';
18
+
19
+ export type GetMenuContext = {
20
+ state: EditorState;
21
+ pos: number;
22
+ text: string;
23
+ trigger?: string;
24
+ };
25
+
26
+ export type UseEditorMenuProps = {
27
+ filter?: boolean;
28
+ getMenu?: (context: GetMenuContext) => MaybePromise<EditorMenuGroup[]>;
29
+ } & Pick<PopoverOptions, 'trigger' | 'triggerKey' | 'placeholder'>;
30
+
31
+ export type UseEditorMenu = {
32
+ groupsRef: RefObject<EditorMenuGroup[]>;
33
+ extension: Extension;
34
+ } & Pick<EditorMenuProviderProps, 'currentItem' | 'open' | 'onOpenChange' | 'onActivate' | 'onSelect' | 'onCancel'>;
35
+
36
+ /**
37
+ * ```tsx
38
+ * const { groupsRef, extension, ...menuProps } = useEditorMenu();
39
+ * const { parentRef, viewRef } = useTextEditor({ extensions: [extension] });
40
+ * return (
41
+ * <EditorMenuProvider view={viewRef.current} groups={groupsRef.current} {...menuProps}>
42
+ * <div ref={parentRef} />
43
+ * </EditorMenuProvider>
44
+ * );
45
+ * ```
46
+ */
47
+ export const useEditorMenu = ({
48
+ trigger,
49
+ triggerKey,
50
+ placeholder,
51
+ filter = true,
52
+ getMenu,
53
+ }: UseEditorMenuProps): UseEditorMenu => {
54
+ const groupsRef = useRef<EditorMenuGroup[]>([]);
55
+ const currentRef = useRef<EditorMenuItem | null>(null);
56
+ const [currentItem, setCurrentItem] = useState<string>();
57
+ const [open, setOpen] = useState(false);
58
+ const [_, refresh] = useState({});
59
+
60
+ /**
61
+ * Get filtered options.
62
+ */
63
+ const getMenuOptions = useCallback<NonNullable<UseEditorMenuProps['getMenu']>>(
64
+ async ({ text, trigger, ...props }) => {
65
+ const groups = (await getMenu?.({ text, trigger, ...props })) ?? [];
66
+ return filter
67
+ ? filterMenuGroups(groups, (item) =>
68
+ text ? (item.label as string).toLowerCase().startsWith(text.toLowerCase()) : true,
69
+ )
70
+ : groups;
71
+ },
72
+ [getMenu, filter],
73
+ );
74
+
75
+ const handleOpenChange = useCallback<NonNullable<UseEditorMenu['onOpenChange']>>(
76
+ async ({ view, open }) => {
77
+ console.log(view, open);
78
+ invariant(view);
79
+ setOpen(open);
80
+ if (!open) {
81
+ setCurrentItem(undefined);
82
+ view.dispatch({
83
+ effects: [popoverRangeEffect.of(null)],
84
+ });
85
+ }
86
+
87
+ // TODO(burdon): Possible race condition.
88
+ // useTextEditor.handleKeyDown will get called after this handler completes.
89
+ requestAnimationFrame(() => {
90
+ view.dispatch({
91
+ effects: [modalStateEffect.of(open)],
92
+ });
93
+ });
94
+ },
95
+ [getMenuOptions],
96
+ );
97
+
98
+ const handleActivate = useCallback<NonNullable<UseEditorMenu['onActivate']>>(
99
+ async ({ view, trigger }) => {
100
+ const item = getMenuItem(groupsRef.current, currentItem);
101
+ if (item) {
102
+ currentRef.current = item;
103
+ }
104
+
105
+ if (!open) {
106
+ handleOpenChange({ view, open: true, trigger });
107
+ }
108
+ },
109
+ [open, handleOpenChange],
110
+ );
111
+
112
+ const handleSelect = useCallback<NonNullable<UseEditorMenu['onSelect']>>(({ view, item }) => {
113
+ void item.onSelect?.({ view, head: view.state.selection.main.head });
114
+ }, []);
115
+
116
+ const handleCancel = useCallback<NonNullable<UseEditorMenu['onCancel']>>(({ view }) => {
117
+ // Delete trigger.
118
+ const { range, trigger } = view.state.field(popoverStateField) ?? {};
119
+ if (range && trigger) {
120
+ view.dispatch({
121
+ changes: { ...range, insert: '' },
122
+ });
123
+ }
124
+ }, []);
125
+
126
+ const serializedTrigger = Array.isArray(trigger) ? trigger.join(',') : trigger;
127
+ const extension = useMemo<Extension>(() => {
128
+ return popover({
129
+ trigger,
130
+ triggerKey,
131
+ placeholder,
132
+ onClose: ({ view }) => handleOpenChange({ view, open: false }),
133
+ onEnter: ({ view }) => {
134
+ if (currentRef.current) {
135
+ handleSelect({ view, item: currentRef.current });
136
+ }
137
+ },
138
+ onArrowUp: () => {
139
+ setCurrentItem((currentItem) => {
140
+ const previous = getPreviousMenuItem(groupsRef.current, currentItem);
141
+ currentRef.current = previous;
142
+ return previous.id;
143
+ });
144
+ },
145
+ onArrowDown: () => {
146
+ setCurrentItem((currentItem) => {
147
+ const next = getNextMenuItem(groupsRef.current, currentItem);
148
+ currentRef.current = next;
149
+ return next.id;
150
+ });
151
+ },
152
+ onTextChange: async ({ view, pos, text, trigger }) => {
153
+ groupsRef.current = (await getMenuOptions({ state: view.state, pos, text, trigger })) ?? [];
154
+ const firstItem = groupsRef.current.filter((group) => group.items.length > 0)[0]?.items[0];
155
+ if (firstItem) {
156
+ setCurrentItem(firstItem.id);
157
+ currentRef.current = firstItem;
158
+ }
159
+
160
+ refresh({});
161
+ },
162
+ });
163
+ }, [handleOpenChange, getMenuOptions, serializedTrigger, placeholder]);
164
+
165
+ return {
166
+ groupsRef,
167
+ extension,
168
+ currentItem,
169
+ open,
170
+ onOpenChange: handleOpenChange,
171
+ onActivate: handleActivate,
172
+ onSelect: handleSelect,
173
+ onCancel: handleCancel,
174
+ };
175
+ };
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+
7
+ // TODO(burdon): Factor out.
8
+
9
+ export const insertAtCursor = (view: EditorView, from: number, insert: string) => {
10
+ view.dispatch({
11
+ changes: { from, to: from, insert },
12
+ selection: { anchor: from + insert.length, head: from + insert.length },
13
+ });
14
+ };
15
+
16
+ /**
17
+ * If the cursor is at the start of a line, insert the text at the cursor.
18
+ * Otherwise, insert the text on a new line.
19
+ */
20
+ export const insertAtLineStart = (view: EditorView, from: number, insert: string) => {
21
+ const line = view.state.doc.lineAt(from);
22
+ if (line.from === from) {
23
+ insertAtCursor(view, from, insert);
24
+ } else {
25
+ insert = '\n' + insert;
26
+ view.dispatch({
27
+ changes: { from: line.to, to: line.to, insert },
28
+ selection: { anchor: line.to + insert.length, head: line.to + insert.length },
29
+ });
30
+ }
31
+ };
@@ -0,0 +1,82 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import React, { type PropsWithChildren, type RefObject, useCallback, useEffect, useRef, useState } from 'react';
7
+
8
+ import { addEventListener } from '@dxos/async';
9
+ import { DX_ANCHOR_ACTIVATE, type DxAnchorActivate, Popover } from '@dxos/react-ui';
10
+
11
+ import { type PreviewLinkRef, type PreviewLinkTarget } from '../../extensions';
12
+
13
+ type EditorPreviewPopoverValue = Partial<{
14
+ link: PreviewLinkRef;
15
+ target: PreviewLinkTarget;
16
+ pending: boolean;
17
+ }>;
18
+
19
+ const [EditorPreviewContextProvider, useEditorPreview] = createContext<EditorPreviewPopoverValue>('PreviewPopover', {});
20
+
21
+ export type EditorPreviewProviderProps = PropsWithChildren<{
22
+ onLookup?: (link: PreviewLinkRef) => Promise<PreviewLinkTarget | null | undefined>;
23
+ }>;
24
+
25
+ /**
26
+ * NOTE: In Composer, the DeckPlugin provides the Popover.Root as part of the DeckLayout.
27
+ */
28
+ // TOOD(burdon): Reconcile with PreviewPlugin.
29
+ export const EditorPreviewProvider = ({ children, onLookup }: EditorPreviewProviderProps) => {
30
+ const triggerRef = useRef<HTMLElement | null>(null);
31
+ const [value, setValue] = useState<EditorPreviewPopoverValue>({});
32
+ const [open, setOpen] = useState(false);
33
+
34
+ const handleActivate = useCallback(
35
+ (event: DxAnchorActivate) => {
36
+ const { refId, label, trigger: dxTrigger } = event;
37
+ setValue((value) => ({
38
+ ...value,
39
+ link: { label, ref: refId },
40
+ pending: true,
41
+ }));
42
+
43
+ triggerRef.current = dxTrigger;
44
+ queueMicrotask(() => setOpen(true));
45
+ void onLookup?.({ label, ref: refId }).then((target) =>
46
+ setValue((value) => ({
47
+ ...value,
48
+ target: target ?? undefined,
49
+ pending: false,
50
+ })),
51
+ );
52
+ },
53
+ [onLookup],
54
+ );
55
+
56
+ const [root, setRoot] = useState<HTMLDivElement | null>(null);
57
+ useEffect(() => {
58
+ if (!root || !handleActivate) {
59
+ return;
60
+ }
61
+
62
+ return addEventListener(root, DX_ANCHOR_ACTIVATE as any, handleActivate, {
63
+ capture: true,
64
+ passive: false,
65
+ });
66
+ }, [root, handleActivate]);
67
+
68
+ return (
69
+ <EditorPreviewContextProvider pending={value.pending} link={value.link} target={value.target}>
70
+ <Popover.Root open={open} onOpenChange={setOpen}>
71
+ <Popover.VirtualTrigger virtualRef={triggerRef as unknown as RefObject<HTMLButtonElement>} />
72
+
73
+ {/* Content */}
74
+ <div ref={setRoot} role='none' className='contents'>
75
+ {children}
76
+ </div>
77
+ </Popover.Root>
78
+ </EditorPreviewContextProvider>
79
+ );
80
+ };
81
+
82
+ export { useEditorPreview };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './EditorPreviewProvider';