@dxos/react-ui-editor 0.3.11-main.f437bee → 0.3.11-main.f4c1077

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 (197) hide show
  1. package/dist/lib/browser/index.mjs +2776 -1291
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +13 -15
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +13 -10
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +4 -4
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/TextEditor/{yjs.stories.d.ts → hooks.stories.d.ts} +13 -2
  12. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/Toolbar/Toolbar.d.ts +29 -0
  14. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  15. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +25 -0
  16. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/Toolbar/index.d.ts +2 -0
  18. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
  19. package/dist/types/src/components/index.d.ts +1 -0
  20. package/dist/types/src/components/index.d.ts.map +1 -1
  21. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  23. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  24. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  25. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  26. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  28. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  29. package/dist/types/src/extensions/automerge/defs.d.ts +9 -11
  30. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  31. package/dist/types/src/extensions/automerge/index.d.ts +0 -1
  32. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
  33. package/dist/types/src/extensions/automerge/semaphore.d.ts +8 -12
  34. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  35. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  36. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  37. package/dist/types/src/extensions/awareness.d.ts +2 -2
  38. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  39. package/dist/types/src/extensions/basic.d.ts +4 -5
  40. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  41. package/dist/types/src/extensions/comments.d.ts +41 -20
  42. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  43. package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
  44. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  45. package/dist/types/src/extensions/index.d.ts +2 -7
  46. package/dist/types/src/extensions/index.d.ts.map +1 -1
  47. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  48. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  49. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  50. package/dist/types/src/extensions/markdown/formatting.d.ts +58 -0
  51. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  52. package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
  53. package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -0
  54. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  55. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  56. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  57. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  58. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  59. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  60. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  61. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  62. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  63. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  64. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  65. package/dist/types/src/extensions/outliner.d.ts +4 -0
  66. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  67. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  68. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  69. package/dist/types/src/hooks/defs.d.ts +25 -0
  70. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  71. package/dist/types/src/hooks/index.d.ts +4 -2
  72. package/dist/types/src/hooks/index.d.ts.map +1 -1
  73. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  74. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  76. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  78. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  79. package/dist/types/src/hooks/useTextModel.d.ts +14 -32
  80. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  81. package/dist/types/src/index.d.ts +1 -1
  82. package/dist/types/src/index.d.ts.map +1 -1
  83. package/dist/types/src/styles/markdown.d.ts +2 -2
  84. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  85. package/dist/types/src/themes/default.d.ts.map +1 -1
  86. package/dist/types/src/util.d.ts +7 -0
  87. package/dist/types/src/util.d.ts.map +1 -0
  88. package/package.json +48 -37
  89. package/src/components/TextEditor/MarkdownEditor.stories.tsx +83 -92
  90. package/src/components/TextEditor/TextEditor.stories.tsx +10 -43
  91. package/src/components/TextEditor/TextEditor.tsx +115 -89
  92. package/src/components/TextEditor/automerge.stories.tsx +1 -2
  93. package/src/components/TextEditor/hooks.stories.tsx +111 -0
  94. package/src/components/Toolbar/Toolbar.stories.tsx +102 -0
  95. package/src/components/Toolbar/Toolbar.tsx +264 -0
  96. package/src/components/Toolbar/index.ts +5 -0
  97. package/src/components/index.ts +1 -0
  98. package/src/extensions/autocomplete.ts +6 -3
  99. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  100. package/src/extensions/automerge/automerge.test.ts +13 -0
  101. package/src/extensions/automerge/automerge.ts +55 -46
  102. package/src/extensions/automerge/cursor.ts +5 -4
  103. package/src/extensions/automerge/defs.ts +13 -34
  104. package/src/extensions/automerge/index.ts +0 -1
  105. package/src/extensions/automerge/semaphore.ts +33 -32
  106. package/src/extensions/automerge/update-automerge.ts +14 -8
  107. package/src/extensions/automerge/update-codemirror.ts +5 -5
  108. package/src/extensions/awareness.ts +46 -49
  109. package/src/extensions/basic.ts +17 -10
  110. package/src/extensions/blast.ts +4 -1
  111. package/src/extensions/comments.ts +331 -265
  112. package/src/extensions/cursor.ts +50 -0
  113. package/src/extensions/index.ts +2 -7
  114. package/src/extensions/markdown/bundle.ts +23 -28
  115. package/src/extensions/markdown/code.ts +150 -0
  116. package/src/extensions/markdown/formatting.test.ts +482 -0
  117. package/src/extensions/markdown/formatting.ts +1237 -0
  118. package/src/extensions/markdown/heading.ts +59 -0
  119. package/src/extensions/markdown/highlight.ts +9 -23
  120. package/src/extensions/markdown/hr.ts +71 -0
  121. package/src/extensions/{image.ts → markdown/image.ts} +2 -2
  122. package/src/extensions/markdown/index.ts +8 -0
  123. package/src/extensions/{link.ts → markdown/link.ts} +20 -11
  124. package/src/extensions/{table.ts → markdown/table.ts} +2 -2
  125. package/src/extensions/markdown/tasklist.ts +110 -0
  126. package/src/extensions/mention.ts +9 -2
  127. package/src/extensions/outliner.ts +12 -0
  128. package/src/hooks/awareness-provider.ts +24 -20
  129. package/src/hooks/defs.ts +42 -0
  130. package/src/hooks/index.ts +5 -2
  131. package/src/hooks/useActionHandler.ts +93 -0
  132. package/src/hooks/useEditorView.ts +29 -0
  133. package/src/hooks/useTextEditor.ts +153 -0
  134. package/src/hooks/useTextModel.ts +66 -59
  135. package/src/index.ts +1 -1
  136. package/src/styles/markdown.ts +9 -12
  137. package/src/themes/default.ts +26 -56
  138. package/src/util.ts +41 -0
  139. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  140. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  141. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  142. package/dist/types/src/extensions/code.d.ts.map +0 -1
  143. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  144. package/dist/types/src/extensions/image.d.ts.map +0 -1
  145. package/dist/types/src/extensions/link.d.ts.map +0 -1
  146. package/dist/types/src/extensions/table.d.ts.map +0 -1
  147. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  148. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  149. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  150. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  151. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  152. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  153. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  154. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  155. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  156. package/dist/types/src/hooks/automerge.d.ts +0 -3
  157. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  158. package/dist/types/src/hooks/yjs.d.ts +0 -27
  159. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  160. package/dist/types/src/testing/index.d.ts +0 -3
  161. package/dist/types/src/testing/index.d.ts.map +0 -1
  162. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  163. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  164. package/dist/types/src/testing/proto/index.d.ts +0 -2
  165. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  166. package/dist/types/src/testing/replicator.d.ts +0 -45
  167. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  168. package/dist/types/src/util/cursor.d.ts.map +0 -1
  169. package/dist/types/src/util/index.d.ts +0 -3
  170. package/dist/types/src/util/index.d.ts.map +0 -1
  171. package/dist/types/src/util/util.d.ts +0 -5
  172. package/dist/types/src/util/util.d.ts.map +0 -1
  173. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  174. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  175. package/src/extensions/code.ts +0 -98
  176. package/src/extensions/hr.ts +0 -73
  177. package/src/extensions/tasklist.ts +0 -119
  178. package/src/extensions/yjs/cursor.ts +0 -21
  179. package/src/extensions/yjs/index.ts +0 -5
  180. package/src/extensions/yjs/yjs.test.ts +0 -35
  181. package/src/extensions/yjs/yjs.ts +0 -16
  182. package/src/hooks/automerge.ts +0 -52
  183. package/src/hooks/yjs.ts +0 -178
  184. package/src/testing/index.ts +0 -6
  185. package/src/testing/proto/gen/schema.ts +0 -49
  186. package/src/testing/proto/index.ts +0 -5
  187. package/src/testing/proto/schema.proto +0 -15
  188. package/src/testing/replicator.ts +0 -184
  189. package/src/util/cursor.ts +0 -29
  190. package/src/util/index.ts +0 -6
  191. package/src/util/util.ts +0 -18
  192. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  193. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  194. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  195. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  196. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  197. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
@@ -4,17 +4,15 @@
4
4
 
5
5
  import '@dxosTheme';
6
6
 
7
- import defaultsDeep from 'lodash.defaultsdeep';
8
7
  import React, { useState } from 'react';
9
8
 
10
9
  import { TextObject } from '@dxos/echo-schema';
11
10
  import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
12
11
  import { withTheme } from '@dxos/storybook-utils';
13
12
 
14
- import { defaultSlots, TextEditor, type TextEditorProps, type TextEditorSlots } from './TextEditor';
13
+ import { TextEditor, type TextEditorProps } from './TextEditor';
15
14
  import { listener } from '../../extensions';
16
15
  import { useTextModel } from '../../hooks';
17
- import { textTheme } from '../../themes';
18
16
 
19
17
  const initialText = [
20
18
  '# TextEditor',
@@ -30,7 +28,7 @@ const Story = ({
30
28
  text,
31
29
  automerge,
32
30
  ...props
33
- }: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'slots'>) => {
31
+ }: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'placeholder' | 'slots'>) => {
34
32
  const [item] = useState({ text: new TextObject(text, undefined, undefined, { automerge }) });
35
33
  const model = useTextModel({ text: item.text });
36
34
  if (!model) {
@@ -39,10 +37,9 @@ const Story = ({
39
37
 
40
38
  return (
41
39
  <div className={mx(fixedInsetFlexLayout, groupSurface)}>
42
- <div className='flex justify-center overflow-y-scroll'>
40
+ <div className='flex justify-center'>
43
41
  <div className='flex flex-col w-[800px] py-16'>
44
42
  <TextEditor model={model} {...props} />
45
- <div className='flex shrink-0 h-[300px]'></div>
46
43
  </div>
47
44
  </div>
48
45
  </div>
@@ -57,49 +54,19 @@ export default {
57
54
  };
58
55
 
59
56
  export const Default = {
60
- render: () => (
61
- <Story
62
- slots={defaultsDeep(
63
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
64
- defaultSlots,
65
- )}
66
- />
67
- ),
68
- };
69
-
70
- export const Listener = {
71
- render: () => (
72
- <Story
73
- slots={defaultsDeep(
74
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
75
- defaultSlots,
76
- )}
77
- extensions={[listener({ onChange: (text) => console.log(text) })]}
78
- />
79
- ),
57
+ render: () => <Story placeholder='Enter text...' />,
80
58
  };
81
59
 
82
60
  export const Text = {
83
- render: () => (
84
- <Story
85
- text={initialText}
86
- slots={defaultsDeep(
87
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
88
- defaultSlots,
89
- )}
90
- />
91
- ),
61
+ render: () => <Story text={initialText} placeholder='Enter text...' />,
92
62
  };
93
63
 
94
64
  export const Automerge = {
65
+ render: () => <Story text={initialText} placeholder='Enter text...' automerge />,
66
+ };
67
+
68
+ export const Listener = {
95
69
  render: () => (
96
- <Story
97
- automerge
98
- text={initialText}
99
- slots={defaultsDeep(
100
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
101
- defaultSlots,
102
- )}
103
- />
70
+ <Story placeholder='Enter text...' extensions={[listener({ onChange: (text) => console.log(text) })]} />
104
71
  ),
105
72
  };
@@ -2,9 +2,9 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { EditorState, type Extension } from '@codemirror/state';
5
+ import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
6
6
  import { EditorView } from '@codemirror/view';
7
- import { useFocusableGroup } from '@fluentui/react-tabster';
7
+ // import { useFocusableGroup } from '@fluentui/react-tabster';
8
8
  import { vim } from '@replit/codemirror-vim';
9
9
  import defaultsDeep from 'lodash.defaultsdeep';
10
10
  import React, {
@@ -14,19 +14,22 @@ import React, {
14
14
  useCallback,
15
15
  useEffect,
16
16
  useImperativeHandle,
17
- useState,
18
17
  useRef,
18
+ useState,
19
19
  } from 'react';
20
20
 
21
- import { generateName } from '@dxos/display-name';
21
+ import { log } from '@dxos/log';
22
22
  import { useThemeContext } from '@dxos/react-ui';
23
- import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
23
+ import { focusRing, mx } from '@dxos/react-ui-theme';
24
+ import { isNotFalsy } from '@dxos/util';
24
25
 
25
- import { basicBundle, markdownBundle, setCommentRange } from '../../extensions';
26
- import { type CommentRange, type EditorModel } from '../../hooks';
26
+ import { createBasicBundle, createMarkdownExtensions } from '../../extensions';
27
+ import { type EditorModel } from '../../hooks';
27
28
  import { type ThemeStyles } from '../../styles';
28
29
  import { defaultTheme, markdownTheme, textTheme } from '../../themes';
30
+ import { logChanges } from '../../util';
29
31
 
32
+ // TODO(burdon): Change to enum?
30
33
  export const EditorModes = ['default', 'vim'] as const;
31
34
  export type EditorMode = (typeof EditorModes)[number];
32
35
 
@@ -43,22 +46,26 @@ export type TextEditorSlots = {
43
46
  root?: Omit<ComponentProps<'div'>, 'ref'>;
44
47
  editor?: {
45
48
  className?: string;
46
- placeholder?: string;
47
- spellCheck?: boolean;
48
- tabIndex?: number;
49
- theme?: ThemeStyles;
49
+ };
50
+ content?: {
51
+ className?: string;
50
52
  };
51
53
  };
52
54
 
55
+ // TODO(burdon): Spellcheck?
53
56
  export type TextEditorProps = {
54
- model: EditorModel;
55
- focus?: boolean;
56
- selection?: { anchor: number; head?: number };
57
+ model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
57
58
  readonly?: boolean; // TODO(burdon): Move into model.
58
- comments?: CommentRange[]; // TODO(burdon): Move into extension.
59
- extensions?: Extension[];
60
- editorMode?: EditorMode;
59
+ autoFocus?: boolean;
60
+ lineWrapping?: boolean;
61
+ scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
62
+ selection?: { anchor: number; head?: number };
63
+ editorMode?: EditorMode; // TODO(burdon): Factor out.
64
+ placeholder?: string;
65
+ theme?: ThemeStyles;
61
66
  slots?: TextEditorSlots;
67
+ extensions?: Extension[];
68
+ debug?: boolean;
62
69
  };
63
70
 
64
71
  /**
@@ -66,108 +73,134 @@ export type TextEditorProps = {
66
73
  */
67
74
  export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
68
75
  (
69
- { model, focus, selection, readonly, comments, extensions = [], editorMode, slots = defaultSlots },
76
+ {
77
+ model,
78
+ readonly,
79
+ autoFocus,
80
+ scrollTo,
81
+ selection,
82
+ editorMode,
83
+ theme,
84
+ slots = defaultSlots,
85
+ extensions = [],
86
+ debug,
87
+ },
70
88
  forwardedRef,
71
89
  ) => {
72
- const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
90
+ // TODO(burdon): Hook causes error even if properties are not spread into div.
91
+ // Uncaught TypeError: Cannot read properties of undefined (reading 'relatedTarget')
92
+ // Uses event.detail, which is deprecated (not event.details). At runtime the event has a property `details`.
93
+ // https://github.com/microsoft/tabster/blob/master/src/State/FocusedElement.ts#L348 (e.detail.relatedTarget)
94
+ // https://github.com/microsoft/keyborg/blob/49e49b2c3ba0a5f6cc518ac46825d7551def8109/src/FocusEvent.ts#L58
95
+ // const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
73
96
  const { themeMode } = useThemeContext();
74
97
 
75
- // The editor view ref should only be used as an escape hatch.
76
98
  const rootRef = useRef<HTMLDivElement>(null);
77
99
  const [view, setView] = useState<EditorView | null>(null);
78
- useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
79
-
80
- // Focus.
81
- useEffect(() => {
82
- if (view && focus) {
83
- view.focus();
84
- }
85
- }, [view, focus]);
86
100
 
87
- // TODO(burdon): Factor out as extension.
88
- const { awareness, peer } = model;
89
- useEffect(() => {
90
- if (awareness && peer) {
91
- awareness.setLocalStateField('user', {
92
- name: peer.name ?? generateName(peer.id),
93
- color: getColorForValue({ value: peer.id, type: 'color' }),
94
- colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
95
- });
96
- }
97
- }, [awareness, peer, themeMode]);
101
+ // The view ref can be used to focus the editor.
102
+ // NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
103
+ useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
98
104
 
99
- // TODO(burdon): Factor out as extension.
105
+ // Set focus.
100
106
  useEffect(() => {
101
- if (view && comments?.length) {
102
- view.dispatch({
103
- effects: setCommentRange.of({ model, comments }),
104
- });
107
+ if (autoFocus && !view?.hasFocus) {
108
+ if (view?.state.selection.main?.from === 0) {
109
+ // Start at end of line.
110
+ const { to } = view.state.doc.lineAt(0);
111
+ view?.dispatch({ selection: { anchor: to } });
112
+ }
113
+ view?.focus();
105
114
  }
106
- }, [view, comments]);
115
+ }, [view, autoFocus]);
107
116
 
117
+ // Create editor state and view.
118
+ // The view is recreated if the model or extensions are changed.
108
119
  useEffect(() => {
109
120
  if (!model || !rootRef.current) {
110
121
  return;
111
122
  }
112
123
 
113
- // TODO(burdon): Remember cursor position.
124
+ //
125
+ // EditorState
114
126
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
127
+ //
115
128
  const state = EditorState.create({
116
129
  doc: model.text(),
117
130
  selection,
118
131
  extensions: [
119
- readonly && EditorState.readOnly.of(readonly),
120
-
121
- // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
122
- editorMode === 'vim' && vim(),
132
+ // TODO(burdon): Doesn't catch errors in keymap functions.
133
+ EditorView.exceptionSink.of((err) => {
134
+ log.catch(err);
135
+ }),
123
136
 
124
137
  // Theme.
125
- // TODO(burdon): Make theme configurable.
138
+ // TODO(burdon): Make configurable.
126
139
  EditorView.baseTheme(defaultTheme),
127
- EditorView.theme(slots?.editor?.theme ?? {}),
140
+ EditorView.theme(theme ?? {}),
128
141
  EditorView.darkTheme.of(themeMode === 'dark'),
142
+ EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
143
+ EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
144
+
145
+ // State.
146
+ EditorView.editable.of(!readonly),
147
+ EditorState.readOnly.of(!!readonly),
129
148
 
130
149
  // Storage and replication.
150
+ // NOTE: This must come before user extensions.
131
151
  model.extension,
132
152
 
153
+ // TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
154
+ editorMode === 'vim' && vim(),
155
+
133
156
  // Custom.
134
157
  ...extensions,
135
- ].filter(Boolean) as Extension[],
158
+ ].filter(isNotFalsy),
136
159
  });
137
160
 
138
- // NOTE: This repaints the editor.
139
- // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
140
- // Ideally this should not happen except when changing between text objects.
141
- view?.destroy();
161
+ //
162
+ // EditorView
163
+ // https://codemirror.net/docs/ref/#view.EditorViewConfig
164
+ //
142
165
  const newView = new EditorView({
143
- parent: rootRef.current,
144
166
  state,
145
- // NOTE: Uncomment to spy on all transactions.
167
+ parent: rootRef.current,
168
+ scrollTo,
169
+ // NOTE: Uncomment to debug/monitor all transactions.
146
170
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
147
- // dispatch: (transaction, view) => {
148
- // view.update([transaction]);
149
- // },
171
+ dispatchTransactions: (trs, view) => {
172
+ if (debug) {
173
+ logChanges(trs);
174
+ }
175
+ view.update(trs);
176
+ },
150
177
  });
151
178
 
179
+ view?.destroy();
152
180
  setView(newView);
181
+
153
182
  return () => {
154
183
  newView?.destroy();
155
184
  setView(null);
156
185
  };
157
186
  }, [rootRef, model, readonly, editorMode, themeMode]);
158
187
 
188
+ // Handles tab/focus.
189
+ // Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
190
+ // TODO(burdon): Convert to keymap?
159
191
  const handleKeyUp = useCallback(
160
192
  (event: KeyboardEvent) => {
161
193
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
162
194
  switch (key) {
163
- // TODO(burdon): ???
164
- // case 'Enter': {
165
- // view?.contentDOM.focus();
166
- // break;
167
- // }
195
+ case 'Enter': {
196
+ view?.focus();
197
+ break;
198
+ }
168
199
 
169
200
  case 'Escape': {
170
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && rootRef.current?.focus();
201
+ if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
202
+ view?.focus();
203
+ }
171
204
  break;
172
205
  }
173
206
  }
@@ -178,28 +211,27 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
178
211
  return (
179
212
  <div
180
213
  key={model.id}
181
- ref={rootRef}
214
+ role='none'
182
215
  tabIndex={0}
183
- {...slots?.root}
184
- {...(editorMode !== 'vim' && tabsterDOMAttribute)}
216
+ {...slots.root}
217
+ // {...(editorMode !== 'vim' && tabsterDOMAttribute)}
185
218
  onKeyUp={handleKeyUp}
219
+ ref={rootRef}
186
220
  />
187
221
  );
188
222
  },
189
223
  );
190
224
 
191
225
  export const TextEditor = forwardRef<EditorView, TextEditorProps>(
192
- ({ readonly, extensions = [], slots, ...props }, forwardedRef) => {
226
+ ({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
193
227
  const { themeMode } = useThemeContext();
194
228
  const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
195
229
  return (
196
230
  <BaseTextEditor
197
231
  ref={forwardedRef}
198
232
  readonly={readonly}
199
- extensions={[
200
- basicBundle({ readonly, themeMode, placeholder: updatedSlots?.editor?.placeholder }),
201
- ...extensions,
202
- ]}
233
+ extensions={[createBasicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
234
+ theme={theme}
203
235
  slots={updatedSlots}
204
236
  {...props}
205
237
  />
@@ -207,19 +239,16 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
207
239
  },
208
240
  );
209
241
 
210
- // TODO(burdon): Remove (Just provide bundle, slots).
211
242
  export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
212
- ({ readonly, extensions = [], slots, ...props }, forwardedRef) => {
243
+ ({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
213
244
  const { themeMode } = useThemeContext();
214
245
  const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
215
246
  return (
216
247
  <BaseTextEditor
217
248
  ref={forwardedRef}
218
249
  readonly={readonly}
219
- extensions={[
220
- markdownBundle({ readonly, themeMode, placeholder: updatedSlots?.editor?.placeholder }),
221
- ...extensions,
222
- ]}
250
+ extensions={[createMarkdownExtensions({ themeMode, placeholder }), ...extensions]}
251
+ theme={theme}
223
252
  slots={updatedSlots}
224
253
  {...props}
225
254
  />
@@ -229,20 +258,17 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
229
258
 
230
259
  export const defaultSlots: TextEditorSlots = {
231
260
  root: {
232
- className: mx('p-2', inputSurface),
261
+ className: mx('grow', focusRing),
262
+ },
263
+ editor: {
264
+ className: 'bs-full',
233
265
  },
234
266
  };
235
267
 
236
268
  export const defaultTextSlots: TextEditorSlots = {
237
269
  ...defaultSlots,
238
- editor: {
239
- theme: textTheme,
240
- },
241
270
  };
242
271
 
243
272
  export const defaultMarkdownSlots: TextEditorSlots = {
244
273
  ...defaultSlots,
245
- editor: {
246
- theme: markdownTheme,
247
- },
248
274
  };
@@ -13,7 +13,7 @@ import React, { useEffect, useRef, useState } from 'react';
13
13
 
14
14
  import { type Prop } from '@dxos/automerge/automerge';
15
15
  import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
16
- import { Filter, setGlobalAutomergePreference } from '@dxos/echo-schema';
16
+ import { Filter } from '@dxos/echo-schema';
17
17
  import { type PublicKey } from '@dxos/keys';
18
18
  import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
19
19
  import { useIdentity } from '@dxos/react-client/halo';
@@ -130,7 +130,6 @@ const EchoStory = ({ id, spaceKey }: { id: number; spaceKey: PublicKey }) => {
130
130
 
131
131
  export const WithEcho = {
132
132
  render: () => {
133
- setGlobalAutomergePreference(true);
134
133
  return (
135
134
  <ClientRepeater
136
135
  count={2}
@@ -0,0 +1,111 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import React from 'react';
8
+
9
+ import { useThemeContext } from '@dxos/react-ui';
10
+ import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
11
+ import { withTheme } from '@dxos/storybook-utils';
12
+
13
+ import { TextEditor } from './TextEditor';
14
+ import {
15
+ code,
16
+ createMarkdownExtensions,
17
+ formatting,
18
+ heading,
19
+ hr,
20
+ image,
21
+ link,
22
+ table,
23
+ tasklist,
24
+ useFormattingState,
25
+ } from '../../extensions';
26
+ import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
27
+ import { markdownTheme } from '../../themes';
28
+ import { Toolbar } from '../Toolbar';
29
+
30
+ // TODO(burdon): Demo toolbar with hooks.
31
+ // TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
32
+ // TODO(burdon): Remove BaseTextEditor.
33
+ // TODO(burdon): Move scrolling container layout/logic into TextEditor/MarkdownEditor components.
34
+ // TODO(burdon): Define keymap in composer framework format.
35
+
36
+ type StoryProps = {
37
+ autoFocus?: boolean;
38
+ placeholder?: string;
39
+ doc?: string;
40
+ readonly?: boolean;
41
+ };
42
+
43
+ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
44
+ const { themeMode } = useThemeContext();
45
+ const [formattingState, trackFormatting] = useFormattingState();
46
+ const { parentRef, view } = useTextEditor({
47
+ autoFocus,
48
+ doc,
49
+ extensions: [
50
+ //
51
+ createDataExtensions({ readonly }),
52
+ createThemeExtensions({
53
+ themeMode,
54
+ theme: markdownTheme,
55
+ slots: {
56
+ // TODO(burdon): Document classes re base theme.
57
+ // Semantic tokens (e.g., replacement of input surface).
58
+ editor: {
59
+ className: 'h-full p-4 bg-white text-black dark:bg-black dark:text-white',
60
+ },
61
+ },
62
+ }),
63
+ // TODO(burdon): Move lineWrapping.
64
+ createMarkdownExtensions({ placeholder, lineWrapping: true }),
65
+ // TODO(burdon): Move into markdown bundle (with React callbacks).
66
+ code(),
67
+ formatting(),
68
+ heading(),
69
+ hr(),
70
+ image(),
71
+ link(),
72
+ table(),
73
+ tasklist(),
74
+ trackFormatting,
75
+ ],
76
+ });
77
+
78
+ const handleAction = useActionHandler(view);
79
+
80
+ // FIXME This doesn't update the state on view changes. Also not
81
+ // sure if view is even guaranteed to exist at this point.
82
+ return (
83
+ <div className={mx(fixedInsetFlexLayout, groupSurface)}>
84
+ <div className='flex h-full justify-center'>
85
+ <div className='flex flex-col h-full w-[800px]'>
86
+ <Toolbar.Root onAction={handleAction} state={formattingState}>
87
+ <Toolbar.Markdown />
88
+ </Toolbar.Root>
89
+
90
+ {/* TODO(burdon): Handle scrolling in component wrapper (like this). */}
91
+ <div role='none' className='h-full overflow-y-auto' ref={parentRef} />
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ };
97
+
98
+ export default {
99
+ title: 'react-ui-editor/useTextEditor',
100
+ component: TextEditor,
101
+ decorators: [withTheme],
102
+ render: (args: StoryProps) => <Story {...args} />,
103
+ };
104
+
105
+ export const Default = {
106
+ args: {
107
+ autoFocus: true,
108
+ placeholder: 'Text...',
109
+ doc: '# Demo\n\nThis is a document.\n\n',
110
+ },
111
+ };
@@ -0,0 +1,102 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import React, { type FC, useMemo, useState } from 'react';
8
+
9
+ import { TextObject } from '@dxos/echo-schema';
10
+ import { PublicKey } from '@dxos/keys';
11
+ import { faker } from '@dxos/random';
12
+ import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
13
+ import { withTheme } from '@dxos/storybook-utils';
14
+
15
+ import { Toolbar } from './Toolbar';
16
+ import {
17
+ code,
18
+ comments,
19
+ formatting,
20
+ heading,
21
+ image,
22
+ table,
23
+ tasklist,
24
+ useComments,
25
+ useFormattingState,
26
+ } from '../../extensions';
27
+ import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
28
+ import { MarkdownEditor } from '../TextEditor';
29
+
30
+ faker.seed(101);
31
+
32
+ const Story: FC<{ content: string }> = ({ content }) => {
33
+ const [item] = useState({ text: new TextObject(content) });
34
+ const [_comments, setComments] = useState<Comment[]>([]);
35
+ const [editorRef, editorView] = useEditorView();
36
+ const model = useTextModel({ text: item.text });
37
+ const [formattingState, formattingObserver] = useFormattingState();
38
+ const extensions = useMemo(
39
+ () => [
40
+ code(),
41
+ comments({
42
+ onCreate: ({ cursor }) => {
43
+ const id = PublicKey.random().toHex();
44
+ setComments((comments) => [...comments, { id, cursor }]);
45
+ return id;
46
+ },
47
+ }),
48
+ formatting(),
49
+ heading(),
50
+ image(),
51
+ table(),
52
+ tasklist(),
53
+ formattingObserver,
54
+ ],
55
+ [],
56
+ );
57
+
58
+ useComments(editorView, _comments);
59
+ const handleAction = useActionHandler(editorView);
60
+
61
+ if (!model) {
62
+ return null;
63
+ }
64
+
65
+ return (
66
+ <div className={mx(fixedInsetFlexLayout, groupSurface)}>
67
+ <div className='flex h-full justify-center'>
68
+ <div className='flex flex-col h-full w-[800px]'>
69
+ <Toolbar.Root onAction={handleAction} state={formattingState}>
70
+ <Toolbar.Markdown />
71
+ <Toolbar.Separator />
72
+ <Toolbar.Extended />
73
+ </Toolbar.Root>
74
+ <MarkdownEditor ref={editorRef} model={model} extensions={extensions} />
75
+ </div>
76
+ </div>
77
+ </div>
78
+ );
79
+ };
80
+
81
+ export default {
82
+ title: 'react-ui-editor/Toolbar',
83
+ component: Toolbar,
84
+ render: (args: any) => <Story {...args} />,
85
+ decorators: [withTheme],
86
+ };
87
+
88
+ const content = [
89
+ 'Demo',
90
+ '',
91
+ 'The editor supports **Markdown** styles.',
92
+ '',
93
+ faker.lorem.paragraph({ min: 5, max: 8 }),
94
+ '',
95
+ '',
96
+ ].join('\n');
97
+
98
+ export const Default = {
99
+ args: {
100
+ content,
101
+ },
102
+ };