@dxos/react-ui-editor 0.8.2-staging.7ac8446 → 0.8.3-main.672df60

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 (279) hide show
  1. package/dist/lib/browser/index.mjs +4505 -3151
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/testing/index.mjs +6 -0
  5. package/dist/lib/browser/testing/index.mjs.map +7 -0
  6. package/dist/lib/node/index.cjs +3360 -1998
  7. package/dist/lib/node/index.cjs.map +4 -4
  8. package/dist/lib/node/meta.json +1 -1
  9. package/dist/lib/node/testing/index.cjs +29 -0
  10. package/dist/lib/node/testing/index.cjs.map +7 -0
  11. package/dist/lib/node-esm/index.mjs +4505 -3151
  12. package/dist/lib/node-esm/index.mjs.map +4 -4
  13. package/dist/lib/node-esm/meta.json +1 -1
  14. package/dist/lib/node-esm/testing/index.mjs +8 -0
  15. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  16. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts +1 -1
  17. package/dist/types/src/components/EditorToolbar/EditorToolbar.d.ts.map +1 -1
  18. package/dist/types/src/components/EditorToolbar/blocks.d.ts +4 -3
  19. package/dist/types/src/components/EditorToolbar/blocks.d.ts.map +1 -1
  20. package/dist/types/src/components/EditorToolbar/formatting.d.ts +4 -3
  21. package/dist/types/src/components/EditorToolbar/formatting.d.ts.map +1 -1
  22. package/dist/types/src/components/EditorToolbar/headings.d.ts +4 -3
  23. package/dist/types/src/components/EditorToolbar/headings.d.ts.map +1 -1
  24. package/dist/types/src/components/EditorToolbar/{comment.d.ts → image.d.ts} +4 -5
  25. package/dist/types/src/components/EditorToolbar/image.d.ts.map +1 -0
  26. package/dist/types/src/components/EditorToolbar/index.d.ts +1 -1
  27. package/dist/types/src/components/EditorToolbar/index.d.ts.map +1 -1
  28. package/dist/types/src/components/EditorToolbar/lists.d.ts +4 -3
  29. package/dist/types/src/components/EditorToolbar/lists.d.ts.map +1 -1
  30. package/dist/types/src/components/EditorToolbar/search.d.ts +17 -0
  31. package/dist/types/src/components/EditorToolbar/search.d.ts.map +1 -0
  32. package/dist/types/src/components/EditorToolbar/util.d.ts +17 -25
  33. package/dist/types/src/components/EditorToolbar/util.d.ts.map +1 -1
  34. package/dist/types/src/components/EditorToolbar/{viewMode.d.ts → view-mode.d.ts} +5 -4
  35. package/dist/types/src/components/EditorToolbar/view-mode.d.ts.map +1 -0
  36. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts +21 -0
  37. package/dist/types/src/components/Popover/RefDropdownMenu.d.ts.map +1 -0
  38. package/dist/types/src/components/Popover/RefPopover.d.ts +21 -0
  39. package/dist/types/src/components/Popover/RefPopover.d.ts.map +1 -0
  40. package/dist/types/src/components/Popover/index.d.ts +3 -0
  41. package/dist/types/src/components/Popover/index.d.ts.map +1 -0
  42. package/dist/types/src/components/index.d.ts +1 -0
  43. package/dist/types/src/components/index.d.ts.map +1 -1
  44. package/dist/types/src/defaults.d.ts +3 -5
  45. package/dist/types/src/defaults.d.ts.map +1 -1
  46. package/dist/types/src/extensions/annotations.d.ts +4 -1
  47. package/dist/types/src/extensions/annotations.d.ts.map +1 -1
  48. package/dist/types/src/extensions/autocomplete.d.ts +1 -2
  49. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  50. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  51. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  52. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  53. package/dist/types/src/extensions/automerge/defs.d.ts +1 -1
  54. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  55. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  56. package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -1
  57. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  58. package/dist/types/src/extensions/automerge/update-codemirror.d.ts +1 -1
  59. package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -1
  60. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -1
  61. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -1
  62. package/dist/types/src/extensions/blast.d.ts.map +1 -1
  63. package/dist/types/src/extensions/command/action.d.ts +17 -0
  64. package/dist/types/src/extensions/command/action.d.ts.map +1 -0
  65. package/dist/types/src/extensions/command/command.d.ts +4 -10
  66. package/dist/types/src/extensions/command/command.d.ts.map +1 -1
  67. package/dist/types/src/extensions/command/hint.d.ts +18 -4
  68. package/dist/types/src/extensions/command/hint.d.ts.map +1 -1
  69. package/dist/types/src/extensions/command/index.d.ts +3 -0
  70. package/dist/types/src/extensions/command/index.d.ts.map +1 -1
  71. package/dist/types/src/extensions/command/menu.d.ts +6 -11
  72. package/dist/types/src/extensions/command/menu.d.ts.map +1 -1
  73. package/dist/types/src/extensions/command/state.d.ts +9 -11
  74. package/dist/types/src/extensions/command/state.d.ts.map +1 -1
  75. package/dist/types/src/extensions/command/typeahead.d.ts +17 -0
  76. package/dist/types/src/extensions/command/typeahead.d.ts.map +1 -0
  77. package/dist/types/src/extensions/comments.d.ts +9 -17
  78. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  79. package/dist/types/src/extensions/debug.d.ts.map +1 -1
  80. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  81. package/dist/types/src/extensions/factories.d.ts +4 -0
  82. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  83. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  84. package/dist/types/src/extensions/hashtag.d.ts +3 -0
  85. package/dist/types/src/extensions/hashtag.d.ts.map +1 -0
  86. package/dist/types/src/extensions/index.d.ts +4 -0
  87. package/dist/types/src/extensions/index.d.ts.map +1 -1
  88. package/dist/types/src/extensions/json.d.ts +7 -0
  89. package/dist/types/src/extensions/json.d.ts.map +1 -0
  90. package/dist/types/src/extensions/listener.d.ts.map +1 -1
  91. package/dist/types/src/extensions/markdown/{editorAction.d.ts → action.d.ts} +1 -1
  92. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
  93. package/dist/types/src/extensions/markdown/bundle.d.ts +2 -1
  94. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  95. package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -1
  96. package/dist/types/src/extensions/markdown/debug.d.ts +2 -2
  97. package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -1
  98. package/dist/types/src/extensions/markdown/decorate.d.ts +5 -1
  99. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  100. package/dist/types/src/extensions/markdown/formatting.d.ts +3 -3
  101. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  102. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  103. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
  104. package/dist/types/src/extensions/markdown/index.d.ts +1 -1
  105. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  106. package/dist/types/src/extensions/markdown/link.d.ts +4 -1
  107. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
  108. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -1
  109. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
  110. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  111. package/dist/types/src/extensions/modes.d.ts +5 -5
  112. package/dist/types/src/extensions/modes.d.ts.map +1 -1
  113. package/dist/types/src/extensions/outliner/commands.d.ts +10 -0
  114. package/dist/types/src/extensions/outliner/commands.d.ts.map +1 -0
  115. package/dist/types/src/extensions/outliner/editor.d.ts +5 -0
  116. package/dist/types/src/extensions/outliner/editor.d.ts.map +1 -0
  117. package/dist/types/src/extensions/outliner/editor.test.d.ts +2 -0
  118. package/dist/types/src/extensions/outliner/editor.test.d.ts.map +1 -0
  119. package/dist/types/src/extensions/outliner/index.d.ts +4 -0
  120. package/dist/types/src/extensions/outliner/index.d.ts.map +1 -0
  121. package/dist/types/src/extensions/outliner/outliner.d.ts +11 -0
  122. package/dist/types/src/extensions/outliner/outliner.d.ts.map +1 -0
  123. package/dist/types/src/extensions/outliner/outliner.test.d.ts +2 -0
  124. package/dist/types/src/extensions/outliner/outliner.test.d.ts.map +1 -0
  125. package/dist/types/src/extensions/outliner/selection.d.ts +12 -0
  126. package/dist/types/src/extensions/outliner/selection.d.ts.map +1 -0
  127. package/dist/types/src/extensions/outliner/tree.d.ts +79 -0
  128. package/dist/types/src/extensions/outliner/tree.d.ts.map +1 -0
  129. package/dist/types/src/extensions/outliner/tree.test.d.ts +2 -0
  130. package/dist/types/src/extensions/outliner/tree.test.d.ts.map +1 -0
  131. package/dist/types/src/extensions/preview/index.d.ts +2 -0
  132. package/dist/types/src/extensions/preview/index.d.ts.map +1 -0
  133. package/dist/types/src/extensions/preview/preview.d.ts +39 -0
  134. package/dist/types/src/extensions/preview/preview.d.ts.map +1 -0
  135. package/dist/types/src/extensions/selection.d.ts.map +1 -1
  136. package/dist/types/src/extensions/typewriter.d.ts.map +1 -1
  137. package/dist/types/src/hooks/index.d.ts +0 -1
  138. package/dist/types/src/hooks/index.d.ts.map +1 -1
  139. package/dist/types/src/hooks/useTextEditor.d.ts +2 -1
  140. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  141. package/dist/types/src/stories/Command.stories.d.ts +7 -0
  142. package/dist/types/src/stories/Command.stories.d.ts.map +1 -0
  143. package/dist/types/src/stories/Comments.stories.d.ts +13 -0
  144. package/dist/types/src/stories/Comments.stories.d.ts.map +1 -0
  145. package/dist/types/src/stories/EditorToolbar.stories.d.ts +12 -0
  146. package/dist/types/src/stories/EditorToolbar.stories.d.ts.map +1 -0
  147. package/dist/types/src/stories/Experimental.stories.d.ts +16 -0
  148. package/dist/types/src/stories/Experimental.stories.d.ts.map +1 -0
  149. package/dist/types/src/stories/Markdown.stories.d.ts +46 -0
  150. package/dist/types/src/stories/Markdown.stories.d.ts.map +1 -0
  151. package/dist/types/src/stories/Outliner.stories.d.ts +26 -0
  152. package/dist/types/src/stories/Outliner.stories.d.ts.map +1 -0
  153. package/dist/types/src/stories/Preview.stories.d.ts +10 -0
  154. package/dist/types/src/stories/Preview.stories.d.ts.map +1 -0
  155. package/dist/types/src/stories/TextEditor.stories.d.ts +55 -0
  156. package/dist/types/src/stories/TextEditor.stories.d.ts.map +1 -0
  157. package/dist/types/src/stories/components/EditorStory.d.ts +43 -0
  158. package/dist/types/src/stories/components/EditorStory.d.ts.map +1 -0
  159. package/dist/types/src/stories/components/index.d.ts +3 -0
  160. package/dist/types/src/stories/components/index.d.ts.map +1 -0
  161. package/dist/types/src/stories/components/util.d.ts +38 -0
  162. package/dist/types/src/stories/components/util.d.ts.map +1 -0
  163. package/dist/types/src/styles/theme.d.ts.map +1 -1
  164. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  165. package/dist/types/src/testing/index.d.ts +2 -0
  166. package/dist/types/src/testing/index.d.ts.map +1 -0
  167. package/dist/types/src/testing/util.d.ts +2 -0
  168. package/dist/types/src/testing/util.d.ts.map +1 -0
  169. package/dist/types/src/types.d.ts +5 -0
  170. package/dist/types/src/types.d.ts.map +1 -1
  171. package/dist/types/src/util/cursor.d.ts.map +1 -1
  172. package/dist/types/src/util/debug.d.ts.map +1 -1
  173. package/dist/types/src/util/dom.d.ts.map +1 -1
  174. package/dist/types/src/util/facet.d.ts.map +1 -1
  175. package/dist/types/src/util/react.d.ts +6 -1
  176. package/dist/types/src/util/react.d.ts.map +1 -1
  177. package/dist/types/tsconfig.tsbuildinfo +1 -1
  178. package/package.json +47 -30
  179. package/src/components/EditorToolbar/EditorToolbar.tsx +95 -72
  180. package/src/components/EditorToolbar/blocks.ts +27 -6
  181. package/src/components/EditorToolbar/formatting.ts +34 -7
  182. package/src/components/EditorToolbar/headings.ts +9 -8
  183. package/src/components/EditorToolbar/image.ts +16 -0
  184. package/src/components/EditorToolbar/index.ts +7 -1
  185. package/src/components/EditorToolbar/lists.ts +26 -7
  186. package/src/components/EditorToolbar/search.ts +19 -0
  187. package/src/components/EditorToolbar/util.ts +19 -20
  188. package/src/components/EditorToolbar/{viewMode.ts → view-mode.ts} +9 -8
  189. package/src/components/Popover/RefDropdownMenu.tsx +77 -0
  190. package/src/components/Popover/RefPopover.tsx +75 -0
  191. package/src/components/Popover/index.ts +6 -0
  192. package/src/components/index.ts +1 -0
  193. package/src/defaults.ts +12 -13
  194. package/src/extensions/annotations.ts +41 -64
  195. package/src/extensions/autocomplete.ts +5 -6
  196. package/src/extensions/automerge/automerge.stories.tsx +13 -24
  197. package/src/extensions/automerge/automerge.test.tsx +6 -5
  198. package/src/extensions/automerge/automerge.ts +2 -2
  199. package/src/extensions/automerge/defs.ts +1 -2
  200. package/src/extensions/automerge/sync.ts +7 -7
  201. package/src/extensions/automerge/update-automerge.ts +1 -1
  202. package/src/extensions/automerge/update-codemirror.ts +3 -4
  203. package/src/extensions/awareness/awareness-provider.ts +4 -4
  204. package/src/extensions/awareness/awareness.ts +7 -7
  205. package/src/extensions/blast.ts +9 -9
  206. package/src/extensions/command/action.ts +49 -0
  207. package/src/extensions/command/command.ts +7 -27
  208. package/src/extensions/command/hint.ts +36 -33
  209. package/src/extensions/command/index.ts +3 -0
  210. package/src/extensions/command/menu.ts +91 -54
  211. package/src/extensions/command/state.ts +41 -61
  212. package/src/extensions/command/typeahead.ts +116 -0
  213. package/src/extensions/comments.ts +11 -76
  214. package/src/extensions/factories.ts +13 -0
  215. package/src/extensions/folding.tsx +1 -1
  216. package/src/extensions/hashtag.tsx +68 -0
  217. package/src/extensions/index.ts +4 -0
  218. package/src/extensions/json.ts +57 -0
  219. package/src/extensions/markdown/bundle.ts +13 -9
  220. package/src/extensions/markdown/changes.ts +3 -2
  221. package/src/extensions/markdown/debug.ts +2 -2
  222. package/src/extensions/markdown/decorate.ts +19 -17
  223. package/src/extensions/markdown/formatting.ts +6 -6
  224. package/src/extensions/markdown/image.ts +14 -13
  225. package/src/extensions/markdown/index.ts +1 -1
  226. package/src/extensions/markdown/link.ts +33 -24
  227. package/src/extensions/markdown/styles.ts +4 -3
  228. package/src/extensions/markdown/table.ts +3 -3
  229. package/src/extensions/modes.ts +5 -6
  230. package/src/extensions/outliner/commands.ts +270 -0
  231. package/src/extensions/outliner/editor.test.ts +33 -0
  232. package/src/extensions/outliner/editor.ts +184 -0
  233. package/src/extensions/outliner/index.ts +7 -0
  234. package/src/extensions/outliner/outliner.test.ts +99 -0
  235. package/src/extensions/outliner/outliner.ts +169 -0
  236. package/src/extensions/outliner/selection.ts +50 -0
  237. package/src/extensions/outliner/tree.test.ts +164 -0
  238. package/src/extensions/outliner/tree.ts +315 -0
  239. package/src/extensions/preview/index.ts +5 -0
  240. package/src/extensions/preview/preview.ts +271 -0
  241. package/src/hooks/index.ts +0 -1
  242. package/src/hooks/useTextEditor.ts +4 -3
  243. package/src/stories/Command.stories.tsx +97 -0
  244. package/src/stories/Comments.stories.tsx +98 -0
  245. package/src/stories/EditorToolbar.stories.tsx +96 -0
  246. package/src/stories/Experimental.stories.tsx +86 -0
  247. package/src/stories/Markdown.stories.tsx +121 -0
  248. package/src/stories/Outliner.stories.tsx +120 -0
  249. package/src/stories/Preview.stories.tsx +149 -0
  250. package/src/stories/TextEditor.stories.tsx +256 -0
  251. package/src/stories/components/EditorStory.tsx +135 -0
  252. package/src/stories/components/index.ts +6 -0
  253. package/src/stories/components/util.tsx +231 -0
  254. package/src/styles/theme.ts +15 -5
  255. package/src/styles/tokens.ts +1 -2
  256. package/src/testing/index.ts +5 -0
  257. package/src/testing/util.ts +5 -0
  258. package/src/types.ts +7 -0
  259. package/src/util/react.tsx +20 -2
  260. package/dist/types/src/InputMode.stories.d.ts +0 -57
  261. package/dist/types/src/InputMode.stories.d.ts.map +0 -1
  262. package/dist/types/src/TextEditor.stories.d.ts +0 -115
  263. package/dist/types/src/TextEditor.stories.d.ts.map +0 -1
  264. package/dist/types/src/components/EditorToolbar/comment.d.ts.map +0 -1
  265. package/dist/types/src/components/EditorToolbar/viewMode.d.ts.map +0 -1
  266. package/dist/types/src/extensions/command/preview.d.ts +0 -12
  267. package/dist/types/src/extensions/command/preview.d.ts.map +0 -1
  268. package/dist/types/src/extensions/markdown/editorAction.d.ts.map +0 -1
  269. package/dist/types/src/fragments.d.ts +0 -3
  270. package/dist/types/src/fragments.d.ts.map +0 -1
  271. package/dist/types/src/hooks/useActionHandler.d.ts +0 -4
  272. package/dist/types/src/hooks/useActionHandler.d.ts.map +0 -1
  273. package/src/InputMode.stories.tsx +0 -124
  274. package/src/TextEditor.stories.tsx +0 -856
  275. package/src/components/EditorToolbar/comment.ts +0 -23
  276. package/src/extensions/command/preview.ts +0 -79
  277. package/src/fragments.ts +0 -19
  278. package/src/hooks/useActionHandler.ts +0 -12
  279. /package/src/extensions/markdown/{editorAction.ts → action.ts} +0 -0
@@ -2,25 +2,24 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { StateEffect, StateField } from '@codemirror/state';
6
- import {
7
- showTooltip,
8
- type Command,
9
- type EditorView,
10
- type KeyBinding,
11
- type Tooltip,
12
- type TooltipView,
13
- } from '@codemirror/view';
5
+ import { StateField } from '@codemirror/state';
6
+ import { showTooltip, type EditorView, type Tooltip, type TooltipView } from '@codemirror/view';
14
7
 
8
+ import { closeEffect, type Action, openEffect } from './action';
15
9
  import { type CommandOptions } from './command';
10
+ import { type RenderCallback } from '../../types';
16
11
  import { singleValueFacet } from '../../util';
17
12
 
13
+ export const commandConfig = singleValueFacet<CommandOptions>();
14
+
15
+ export type PopupOptions = {
16
+ renderDialog: RenderCallback<{ onAction: (action?: Action) => void }>;
17
+ };
18
+
18
19
  type CommandState = {
19
20
  tooltip?: Tooltip | null;
20
21
  };
21
22
 
22
- export const commandConfig = singleValueFacet<CommandOptions>();
23
-
24
23
  export const commandState = StateField.define<CommandState>({
25
24
  create: () => ({}),
26
25
  update: (state, tr) => {
@@ -29,8 +28,8 @@ export const commandState = StateField.define<CommandState>({
29
28
  return {};
30
29
  }
31
30
 
32
- if (effect.is(openEffect)) {
33
- const options = tr.state.facet(commandConfig);
31
+ const { renderDialog } = tr.state.facet(commandConfig);
32
+ if (effect.is(openEffect) && renderDialog) {
34
33
  const { pos, fullWidth } = effect.value;
35
34
  const tooltip: Tooltip = {
36
35
  pos,
@@ -38,38 +37,49 @@ export const commandState = StateField.define<CommandState>({
38
37
  arrow: false,
39
38
  strictSide: true,
40
39
  create: (view: EditorView) => {
41
- const dom = document.createElement('div');
40
+ const root = document.createElement('div');
41
+
42
42
  const tooltipView: TooltipView = {
43
- dom,
43
+ dom: root,
44
44
  mount: (view: EditorView) => {
45
45
  if (fullWidth) {
46
- const parent = dom.parentElement!;
46
+ const parent = root.parentElement!;
47
47
  const { paddingLeft, paddingRight } = window.getComputedStyle(parent);
48
48
  const widthWithoutPadding = parent.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
49
- dom.style.width = `${widthWithoutPadding}px`;
49
+ root.style.width = `${widthWithoutPadding}px`;
50
50
  }
51
51
 
52
52
  // Render react component.
53
- options.onRenderDialog(dom, (action) => {
54
- view.dispatch({ effects: closeEffect.of(null) });
55
- if (action?.insert?.length) {
56
- // Insert into editor.
57
- const text = action.insert + '\n';
58
- view.dispatch({
59
- changes: { from: pos, insert: text },
60
- selection: { anchor: pos + text.length },
61
- });
62
- }
63
-
64
- // NOTE: Truncates text if set focus immediately.
65
- requestAnimationFrame(() => view.focus());
66
- });
53
+ renderDialog(
54
+ root,
55
+ {
56
+ onAction: (action) => {
57
+ view.dispatch({ effects: closeEffect.of(null) });
58
+ switch (action?.type) {
59
+ case 'insert': {
60
+ // Insert into editor.
61
+ const text = action.text + '\n';
62
+ view.dispatch({
63
+ changes: { from: pos, insert: text },
64
+ selection: { anchor: pos + text.length },
65
+ });
66
+ break;
67
+ }
68
+ }
69
+
70
+ // NOTE: Truncates text if set focus immediately.
71
+ requestAnimationFrame(() => view.focus());
72
+ },
73
+ },
74
+ view,
75
+ );
67
76
  },
68
77
  };
69
78
 
70
79
  return tooltipView;
71
80
  },
72
81
  };
82
+
73
83
  return { tooltip };
74
84
  }
75
85
  }
@@ -78,33 +88,3 @@ export const commandState = StateField.define<CommandState>({
78
88
  },
79
89
  provide: (field) => [showTooltip.from(field, (value) => value.tooltip ?? null)],
80
90
  });
81
-
82
- export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
83
- export const closeEffect = StateEffect.define<null>();
84
-
85
- export const openCommand: Command = (view: EditorView) => {
86
- if (view.state.field(commandState, false)) {
87
- const selection = view.state.selection.main;
88
- const line = view.state.doc.lineAt(selection.from);
89
- if (line.from === selection.from && line.from === line.to) {
90
- view.dispatch({ effects: openEffect.of({ pos: selection.anchor, fullWidth: true }) });
91
- return true;
92
- }
93
- }
94
-
95
- return false;
96
- };
97
-
98
- export const closeCommand: Command = (view: EditorView) => {
99
- if (view.state.field(commandState, false)) {
100
- view.dispatch({ effects: closeEffect.of(null) });
101
- return true;
102
- }
103
-
104
- return false;
105
- };
106
-
107
- export const commandKeyBindings: readonly KeyBinding[] = [
108
- { key: '/', run: openCommand },
109
- { key: 'Escape', run: closeCommand },
110
- ];
@@ -0,0 +1,116 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { EditorSelection, Prec, RangeSetBuilder, type Extension } from '@codemirror/state';
6
+ import {
7
+ type Command,
8
+ Decoration,
9
+ type DecorationSet,
10
+ type EditorView,
11
+ keymap,
12
+ ViewPlugin,
13
+ type ViewUpdate,
14
+ } from '@codemirror/view';
15
+
16
+ import { Hint } from './hint';
17
+
18
+ export type TypeaheadContext = { line: string };
19
+
20
+ // TODO(burdon): Option to complete only at end of line?
21
+ export type TypeaheadOptions = {
22
+ onComplete?: (context: TypeaheadContext) => string | undefined;
23
+ };
24
+
25
+ /**
26
+ * CodeMirror extension for typeahead completion.
27
+ */
28
+ export const typeahead = ({ onComplete }: TypeaheadOptions = {}): Extension => {
29
+ let hint: string | undefined;
30
+
31
+ const complete: Command = (view: EditorView) => {
32
+ if (!hint) {
33
+ return false;
34
+ }
35
+
36
+ const selection = view.state.selection.main;
37
+ view.dispatch({
38
+ changes: [{ from: selection.from, to: selection.to, insert: hint }],
39
+ selection: EditorSelection.cursor(selection.from + hint.length),
40
+ });
41
+
42
+ return true;
43
+ };
44
+
45
+ return [
46
+ ViewPlugin.fromClass(
47
+ class {
48
+ decorations: DecorationSet = Decoration.none;
49
+ update(update: ViewUpdate) {
50
+ const builder = new RangeSetBuilder<Decoration>();
51
+ const selection = update.view.state.selection.main;
52
+ const line = update.view.state.doc.lineAt(selection.from);
53
+
54
+ // TODO(burdon): Check at end of line and matches start of previous word.
55
+ // TODO(burdon): Context grammar.
56
+ if (selection.from === selection.to && selection.from === line.to) {
57
+ const str = update.state.sliceDoc(line.from, selection.from);
58
+ hint = onComplete?.({ line: str });
59
+ if (hint) {
60
+ builder.add(selection.from, selection.to, Decoration.widget({ widget: new Hint(hint) }));
61
+ }
62
+ }
63
+
64
+ this.decorations = builder.finish();
65
+ }
66
+ },
67
+ {
68
+ decorations: (v) => v.decorations,
69
+ },
70
+ ),
71
+
72
+ // Keys.
73
+ Prec.highest(
74
+ keymap.of([
75
+ {
76
+ key: 'Tab',
77
+ preventDefault: true,
78
+ run: complete,
79
+ },
80
+ {
81
+ key: 'ArrowRight',
82
+ preventDefault: true,
83
+ run: complete,
84
+ },
85
+ ]),
86
+ ),
87
+ ];
88
+ };
89
+
90
+ /**
91
+ * Util to match current line to a static list of completions.
92
+ */
93
+ export const staticCompletion =
94
+ (completions: string[], defaultCompletion?: string) =>
95
+ ({ line }: TypeaheadContext) => {
96
+ if (line.length === 0 && defaultCompletion) {
97
+ return defaultCompletion;
98
+ }
99
+
100
+ const words = line.split(/\s+/).filter(Boolean);
101
+ if (words.length) {
102
+ const word = words.at(-1)!;
103
+ for (const completion of completions) {
104
+ const match = matchCompletion(completion, word);
105
+ if (match) {
106
+ return match;
107
+ }
108
+ }
109
+ }
110
+ };
111
+
112
+ export const matchCompletion = (completion: string, word: string): string | undefined => {
113
+ if (completion.length > word.length && completion.startsWith(word)) {
114
+ return completion.slice(word.length);
115
+ }
116
+ };
@@ -3,14 +3,7 @@
3
3
  //
4
4
 
5
5
  import { invertedEffects } from '@codemirror/commands';
6
- import {
7
- type ChangeDesc,
8
- type EditorState,
9
- type Extension,
10
- StateEffect,
11
- StateField,
12
- type Text,
13
- } from '@codemirror/state';
6
+ import { type ChangeDesc, type Extension, StateEffect, StateField, type Text } from '@codemirror/state';
14
7
  import {
15
8
  hoverTooltip,
16
9
  keymap,
@@ -22,17 +15,15 @@ import {
22
15
  ViewPlugin,
23
16
  } from '@codemirror/view';
24
17
  import sortBy from 'lodash.sortby';
25
- import { useEffect, useMemo } from 'react';
18
+ import { useEffect } from 'react';
26
19
 
27
20
  import { debounce, type CleanupFn } from '@dxos/async';
28
- import { type ReactiveObject } from '@dxos/live-object';
29
21
  import { log } from '@dxos/log';
30
22
  import { isNonNullable } from '@dxos/util';
31
23
 
32
24
  import { documentId } from './selection';
33
- import { type EditorToolbarState } from '../components';
34
- import { type Comment, type Range } from '../types';
35
- import { Cursor, overlap, singleValueFacet, callbackWrapper } from '../util';
25
+ import { type RenderCallback, type Comment, type Range } from '../types';
26
+ import { Cursor, singleValueFacet, callbackWrapper } from '../util';
36
27
 
37
28
  //
38
29
  // State management.
@@ -156,7 +147,7 @@ const commentsDecorations = EditorView.decorations.compute([commentsState], (sta
156
147
  return Decoration.set(decorations);
157
148
  });
158
149
 
159
- const commentClickedEffect = StateEffect.define<string>();
150
+ export const commentClickedEffect = StateEffect.define<string>();
160
151
 
161
152
  const handleCommentClick = EditorView.domEventHandlers({
162
153
  click: (event, view) => {
@@ -345,6 +336,10 @@ export type CommentsOptions = {
345
336
  * Key shortcut to create a new thread.
346
337
  */
347
338
  key?: string;
339
+ /**
340
+ * Called to render tooltip.
341
+ */
342
+ renderTooltip?: RenderCallback<{ shortcut: string }>;
348
343
  /**
349
344
  * Called to create a new thread and return the thread id.
350
345
  */
@@ -361,10 +356,6 @@ export type CommentsOptions = {
361
356
  * Called to notify which thread is currently closest to the cursor.
362
357
  */
363
358
  onSelect?: (state: CommentsState) => void;
364
- /**
365
- * Called to render tooltip.
366
- */
367
- onHover?: (el: Element, shortcut: string) => void;
368
359
  };
369
360
 
370
361
  const optionsFacet = singleValueFacet<CommentsOptions>();
@@ -408,7 +399,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
408
399
  // Hover tooltip (for key shortcut hints, etc.)
409
400
  // TODO(burdon): Factor out to generic hints extension for current selection/line.
410
401
  //
411
- options.onHover &&
402
+ options.renderTooltip &&
412
403
  hoverTooltip(
413
404
  (view, pos) => {
414
405
  const selection = view.state.selection.main;
@@ -419,7 +410,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
419
410
  above: true,
420
411
  create: () => {
421
412
  const el = document.createElement('div');
422
- options.onHover!(el, shortcut);
413
+ options.renderTooltip!(el, { shortcut }, view);
423
414
  return { dom: el, offset: { x: 0, y: 8 } };
424
415
  },
425
416
  };
@@ -545,30 +536,6 @@ export const scrollThreadIntoView = (view: EditorView, id: string, center = true
545
536
  }
546
537
  };
547
538
 
548
- /**
549
- * Query the editor state for the active formatting at the selection.
550
- */
551
- export const selectionOverlapsComment = (state: EditorState): boolean => {
552
- // May not be defined if thread plugin not installed.
553
- const commentState = state.field(commentsState, false);
554
- if (commentState === undefined) {
555
- return false;
556
- }
557
-
558
- const { selection } = state;
559
- for (const range of selection.ranges) {
560
- if (commentState.comments.some(({ range: commentRange }) => overlap(commentRange, range))) {
561
- return true;
562
- }
563
- }
564
-
565
- return false;
566
- };
567
-
568
- const hasActiveSelection = (state: EditorState): boolean => {
569
- return state.selection.ranges.some((range) => !range.empty);
570
- };
571
-
572
539
  /**
573
540
  * Manages external comment synchronization for the editor.
574
541
  * This class subscribes to external comment updates and applies them to the editor view.
@@ -606,19 +573,6 @@ export const createExternalCommentSync = (
606
573
  },
607
574
  );
608
575
 
609
- export const useCommentState = (state: ReactiveObject<EditorToolbarState>): Extension => {
610
- return useMemo(
611
- () =>
612
- EditorView.updateListener.of((update) => {
613
- if (update.docChanged || update.selectionSet) {
614
- state.comment = selectionOverlapsComment(update.state);
615
- state.selection = hasActiveSelection(update.state);
616
- }
617
- }),
618
- [state],
619
- );
620
- };
621
-
622
576
  /**
623
577
  * @deprecated This hook will be removed in future versions. Use the new comment sync extension instead.
624
578
  * Update comments state field.
@@ -636,22 +590,3 @@ export const useComments = (view: EditorView | null | undefined, id: string, com
636
590
  }
637
591
  });
638
592
  };
639
-
640
- /**
641
- * Hook provides an extension to listen for comment clicks and invoke a handler.
642
- */
643
- export const useCommentClickListener = (onCommentClick: (commentId: string) => void): Extension => {
644
- return useMemo(
645
- () =>
646
- EditorView.updateListener.of((update) => {
647
- update.transactions.forEach((transaction) => {
648
- transaction.effects.forEach((effect) => {
649
- if (effect.is(commentClickedEffect)) {
650
- onCommentClick(effect.value);
651
- }
652
- });
653
- });
654
- }),
655
- [onCommentClick],
656
- );
657
- };
@@ -11,6 +11,7 @@ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
11
11
  import {
12
12
  EditorView,
13
13
  type KeyBinding,
14
+ ViewPlugin,
14
15
  drawSelection,
15
16
  dropCursor,
16
17
  highlightActiveLine,
@@ -59,6 +60,7 @@ export type BasicExtensionsOptions = {
59
60
  indentWithTab?: boolean;
60
61
  keymap?: null | 'default' | 'standard';
61
62
  lineNumbers?: boolean;
63
+ /** If false then do not set a max-width or side margin on the editor. */
62
64
  lineWrapping?: boolean;
63
65
  placeholder?: string;
64
66
  /** If true user cannot edit the text, but they can still select and copy it. */
@@ -149,6 +151,9 @@ export type ThemeExtensionsOptions = {
149
151
  editor?: {
150
152
  className?: string;
151
153
  };
154
+ scroll?: {
155
+ className?: string;
156
+ };
152
157
  content?: {
153
158
  className?: string;
154
159
  };
@@ -179,6 +184,14 @@ export const createThemeExtensions = ({
179
184
  (themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle)),
180
185
  slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
181
186
  slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
187
+ slots.scroll?.className &&
188
+ ViewPlugin.fromClass(
189
+ class {
190
+ constructor(view: EditorView) {
191
+ view.scrollDOM.classList.add(slots.scroll.className);
192
+ }
193
+ },
194
+ ),
182
195
  ].filter(isNotFalsy);
183
196
  };
184
197
 
@@ -29,7 +29,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
29
29
  const el = createElement('div', { className: 'flex h-full items-center' });
30
30
  return renderRoot(
31
31
  el,
32
- <Icon icon='ph--caret-right--regular' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
32
+ <Icon icon='ph--caret-right--bold' size={3} classNames={['mx-3 cursor-pointer', open && 'rotate-90']} />,
33
33
  );
34
34
  },
35
35
  }),
@@ -0,0 +1,68 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Extension } from '@codemirror/state';
6
+ import {
7
+ Decoration,
8
+ type DecorationSet,
9
+ EditorView,
10
+ MatchDecorator,
11
+ ViewPlugin,
12
+ type ViewUpdate,
13
+ WidgetType,
14
+ } from '@codemirror/view';
15
+
16
+ import { getHashColor, mx } from '@dxos/react-ui-theme';
17
+
18
+ class TagWidget extends WidgetType {
19
+ constructor(private _text: string) {
20
+ super();
21
+ }
22
+
23
+ toDOM(): HTMLSpanElement {
24
+ const span = document.createElement('span');
25
+ span.className = mx('cm-tag', getHashColor(this._text).tag);
26
+ span.textContent = this._text;
27
+ return span;
28
+ }
29
+ }
30
+
31
+ const tagMatcher = new MatchDecorator({
32
+ regexp: /#(\w+)\W/g,
33
+ decoration: (match) =>
34
+ Decoration.replace({
35
+ widget: new TagWidget(match[1]),
36
+ }),
37
+ });
38
+
39
+ // TODO(burdon): Autocomplete from existing tags?
40
+ export const hashtag = (): Extension => [
41
+ ViewPlugin.fromClass(
42
+ class {
43
+ tags: DecorationSet;
44
+ constructor(view: EditorView) {
45
+ this.tags = tagMatcher.createDeco(view);
46
+ }
47
+
48
+ update(update: ViewUpdate) {
49
+ this.tags = tagMatcher.updateDeco(update, this.tags);
50
+ }
51
+ },
52
+ {
53
+ decorations: (instance) => instance.tags,
54
+ provide: (plugin) =>
55
+ EditorView.atomicRanges.of((view) => {
56
+ return view.plugin(plugin)?.tags || Decoration.none;
57
+ }),
58
+ },
59
+ ),
60
+
61
+ EditorView.theme({
62
+ '.cm-tag': {
63
+ borderRadius: '4px',
64
+ marginRight: '6px',
65
+ padding: '2px 6px',
66
+ },
67
+ }),
68
+ ];
@@ -14,9 +14,13 @@ export * from './dnd';
14
14
  export * from './factories';
15
15
  export * from './focus';
16
16
  export * from './folding';
17
+ export * from './hashtag';
18
+ export * from './json';
17
19
  export * from './listener';
18
20
  export * from './markdown';
19
21
  export * from './mention';
20
22
  export * from './modes';
23
+ export * from './outliner';
24
+ export * from './preview';
21
25
  export * from './selection';
22
26
  export * from './typewriter';
@@ -0,0 +1,57 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { json, jsonParseLinter } from '@codemirror/lang-json';
6
+ import { type LintSource, linter } from '@codemirror/lint';
7
+ import { type Extension } from '@codemirror/state';
8
+ import Ajv, { type ValidateFunction } from 'ajv';
9
+
10
+ import { type JsonSchemaType } from '@dxos/echo-schema';
11
+
12
+ export type JsonExtensionsOptions = {
13
+ schema?: JsonSchemaType;
14
+ };
15
+
16
+ export const createJsonExtensions = ({ schema }: JsonExtensionsOptions = {}): Extension => {
17
+ let lintSource: LintSource = jsonParseLinter();
18
+ if (schema) {
19
+ // NOTE: Relaxing strict mode to allow additional custom schema properties.
20
+ const ajv = new Ajv({ allErrors: false, strict: false });
21
+ const validate = ajv.compile(schema);
22
+ lintSource = schemaLinter(validate);
23
+ }
24
+
25
+ return [json(), linter(lintSource)];
26
+ };
27
+
28
+ const schemaLinter =
29
+ (validate: ValidateFunction): LintSource =>
30
+ (view) => {
31
+ try {
32
+ const jsonText = view.state.doc.toString();
33
+ const jsonData = JSON.parse(jsonText);
34
+ const valid = validate(jsonData);
35
+ if (valid) {
36
+ return [];
37
+ }
38
+
39
+ return (
40
+ validate.errors?.map((err: any) => ({
41
+ from: 0,
42
+ to: jsonText.length,
43
+ severity: 'error',
44
+ message: `${err.instancePath || '(root)'} ${err.message}`,
45
+ })) ?? []
46
+ );
47
+ } catch (err: unknown) {
48
+ return [
49
+ {
50
+ from: 0,
51
+ to: view.state.doc.length,
52
+ severity: 'error',
53
+ message: 'Invalid JSON: ' + (err as Error).message,
54
+ },
55
+ ];
56
+ }
57
+ };
@@ -12,11 +12,13 @@ import { type Extension } from '@codemirror/state';
12
12
  import { keymap } from '@codemirror/view';
13
13
 
14
14
  import { type ThemeMode } from '@dxos/react-ui';
15
+ import { isNotFalsy } from '@dxos/util';
15
16
 
16
17
  import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
17
18
 
18
19
  export type MarkdownBundleOptions = {
19
20
  themeMode?: ThemeMode;
21
+ indentWithTab?: boolean;
20
22
  };
21
23
 
22
24
  /**
@@ -27,7 +29,7 @@ export type MarkdownBundleOptions = {
27
29
  * https://codemirror.net/docs/community
28
30
  * https://codemirror.net/docs/ref/#codemirror.basicSetup
29
31
  */
30
- export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions = {}): Extension[] => {
32
+ export const createMarkdownExtensions = (options: MarkdownBundleOptions = {}): Extension[] => {
31
33
  return [
32
34
  // Main extension.
33
35
  // https://github.com/codemirror/lang-markdown
@@ -56,14 +58,16 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
56
58
  // Custom styles.
57
59
  syntaxHighlighting(markdownHighlightStyle()),
58
60
 
59
- keymap.of([
60
- // https://codemirror.net/docs/ref/#commands.indentWithTab
61
- indentWithTab,
61
+ keymap.of(
62
+ [
63
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
64
+ options.indentWithTab !== false && indentWithTab,
62
65
 
63
- // https://codemirror.net/docs/ref/#commands.defaultKeymap
64
- ...defaultKeymap,
65
- ...completionKeymap,
66
- ...lintKeymap,
67
- ]),
66
+ // https://codemirror.net/docs/ref/#commands.defaultKeymap
67
+ ...defaultKeymap,
68
+ ...completionKeymap,
69
+ ...lintKeymap,
70
+ ].filter(isNotFalsy),
71
+ ),
68
72
  ];
69
73
  };
@@ -56,8 +56,9 @@ export const adjustChanges = () => {
56
56
  // Check for URL.
57
57
  const url = getValidUrl(update.view.state.sliceDoc(fromB, toB));
58
58
  if (url) {
59
+ // Check if pasting inside existing link.
59
60
  const node = tree.resolveInner(fromA, -1);
60
- const invalidPositions = new Set(['Link', 'LinkMark', 'Code', 'CodeText', 'FencedCode', 'URL']);
61
+ const invalidPositions = new Set(['Code', 'CodeText', 'FencedCode', 'Link', 'LinkMark', 'URL']);
61
62
  if (!invalidPositions.has(node?.name)) {
62
63
  const replacedText = tr.startState.sliceDoc(fromA, toA);
63
64
  adjustments.push({ from: fromA, to: toB, insert: createLink(url, replacedText) });
@@ -84,7 +85,7 @@ export const adjustChanges = () => {
84
85
  }
85
86
  }
86
87
 
87
- // TODO(burdon): Is this the right way to augment changes?
88
+ // TODO(burdon): Is this the right way to augment changes? Alt: EditorState.transactionFilter
88
89
  if (adjustments.length) {
89
90
  setTimeout(() => {
90
91
  update.view.dispatch(