@dxos/react-ui-editor 0.3.11-next.0fb359e → 0.3.11-next.e28df4f

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 (201) hide show
  1. package/dist/lib/browser/index.mjs +1854 -1391
  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 +16 -14
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
  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/comments.stories.d.ts +26 -0
  12. package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
  13. package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
  14. package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
  15. package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
  16. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
  17. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
  18. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
  19. package/dist/types/src/components/Toolbar/index.d.ts +2 -0
  20. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
  21. package/dist/types/src/components/index.d.ts +1 -0
  22. package/dist/types/src/components/index.d.ts.map +1 -1
  23. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  24. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  25. package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
  26. package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
  27. package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
  28. package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
  29. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  30. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  31. package/dist/types/src/extensions/automerge/defs.d.ts +9 -11
  32. package/dist/types/src/extensions/automerge/defs.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 +37 -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 -8
  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 +13 -0
  51. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  52. package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
  53. package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
  54. package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
  55. package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
  56. package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
  57. package/dist/types/src/extensions/markdown/index.d.ts +8 -0
  58. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  59. package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
  60. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
  61. package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
  62. package/dist/types/src/extensions/mention.d.ts.map +1 -1
  63. package/dist/types/src/extensions/outliner.d.ts +4 -0
  64. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  65. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  66. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  67. package/dist/types/src/hooks/defs.d.ts +25 -0
  68. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  69. package/dist/types/src/hooks/index.d.ts +4 -2
  70. package/dist/types/src/hooks/index.d.ts.map +1 -1
  71. package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
  72. package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
  73. package/dist/types/src/hooks/useEditorView.d.ts +17 -0
  74. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
  76. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  77. package/dist/types/src/hooks/useTextModel.d.ts +2 -32
  78. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  79. package/dist/types/src/index.d.ts +3 -0
  80. package/dist/types/src/index.d.ts.map +1 -1
  81. package/dist/types/src/styles/markdown.d.ts +2 -2
  82. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  83. package/dist/types/src/styles/tokens.d.ts +1 -0
  84. package/dist/types/src/styles/tokens.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 +84 -91
  90. package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
  91. package/src/components/TextEditor/TextEditor.tsx +136 -95
  92. package/src/components/TextEditor/automerge.stories.tsx +1 -2
  93. package/src/components/TextEditor/comments.stories.tsx +357 -0
  94. package/src/components/TextEditor/hooks.stories.tsx +106 -0
  95. package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
  96. package/src/components/Toolbar/Toolbar.tsx +166 -0
  97. package/src/components/Toolbar/index.ts +5 -0
  98. package/src/components/index.ts +1 -0
  99. package/src/extensions/autocomplete.ts +23 -19
  100. package/src/extensions/automerge/automerge.spec.tsx +76 -0
  101. package/src/extensions/automerge/automerge.test.ts +13 -0
  102. package/src/extensions/automerge/automerge.ts +55 -46
  103. package/src/extensions/automerge/cursor.ts +5 -4
  104. package/src/extensions/automerge/defs.ts +13 -34
  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 +43 -49
  109. package/src/extensions/basic.ts +17 -10
  110. package/src/extensions/comments.ts +323 -268
  111. package/src/extensions/cursor.ts +50 -0
  112. package/src/extensions/index.ts +2 -8
  113. package/src/extensions/markdown/bundle.ts +23 -28
  114. package/src/extensions/markdown/code.ts +150 -0
  115. package/src/extensions/markdown/formatting.ts +188 -0
  116. package/src/extensions/markdown/heading.ts +59 -0
  117. package/src/extensions/markdown/highlight.ts +11 -26
  118. package/src/extensions/markdown/hr.ts +71 -0
  119. package/src/extensions/markdown/image.ts +87 -0
  120. package/src/extensions/markdown/index.ts +8 -0
  121. package/src/extensions/{link.ts → markdown/link.ts} +87 -86
  122. package/src/extensions/{table.ts → markdown/table.ts} +10 -4
  123. package/src/extensions/markdown/tasklist.ts +110 -0
  124. package/src/extensions/mention.ts +9 -2
  125. package/src/extensions/outliner.ts +12 -0
  126. package/src/hooks/awareness-provider.ts +24 -20
  127. package/src/hooks/defs.ts +42 -0
  128. package/src/hooks/index.ts +5 -2
  129. package/src/hooks/useActionHandler.ts +62 -0
  130. package/src/hooks/useEditorView.ts +29 -0
  131. package/src/hooks/useTextEditor.ts +153 -0
  132. package/src/hooks/useTextModel.ts +50 -59
  133. package/src/index.ts +3 -0
  134. package/src/styles/markdown.ts +9 -12
  135. package/src/styles/tokens.ts +3 -0
  136. package/src/themes/default.ts +53 -80
  137. package/src/util.ts +41 -0
  138. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  139. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  140. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  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/mermaid.d.ts +0 -3
  147. package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
  148. package/dist/types/src/extensions/table.d.ts.map +0 -1
  149. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  150. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  151. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  152. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  153. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  154. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  155. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  156. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  157. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  158. package/dist/types/src/hooks/automerge.d.ts +0 -3
  159. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  160. package/dist/types/src/hooks/yjs.d.ts +0 -27
  161. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  162. package/dist/types/src/testing/index.d.ts +0 -3
  163. package/dist/types/src/testing/index.d.ts.map +0 -1
  164. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  165. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  166. package/dist/types/src/testing/proto/index.d.ts +0 -2
  167. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  168. package/dist/types/src/testing/replicator.d.ts +0 -45
  169. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  170. package/dist/types/src/util/cursor.d.ts.map +0 -1
  171. package/dist/types/src/util/index.d.ts +0 -3
  172. package/dist/types/src/util/index.d.ts.map +0 -1
  173. package/dist/types/src/util/util.d.ts +0 -5
  174. package/dist/types/src/util/util.d.ts.map +0 -1
  175. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  176. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  177. package/src/extensions/code.ts +0 -99
  178. package/src/extensions/hr.ts +0 -74
  179. package/src/extensions/image.ts +0 -69
  180. package/src/extensions/mermaid.ts +0 -11
  181. package/src/extensions/tasklist.ts +0 -121
  182. package/src/extensions/yjs/cursor.ts +0 -21
  183. package/src/extensions/yjs/index.ts +0 -5
  184. package/src/extensions/yjs/yjs.test.ts +0 -35
  185. package/src/extensions/yjs/yjs.ts +0 -16
  186. package/src/hooks/automerge.ts +0 -52
  187. package/src/hooks/yjs.ts +0 -178
  188. package/src/testing/index.ts +0 -6
  189. package/src/testing/proto/gen/schema.ts +0 -49
  190. package/src/testing/proto/index.ts +0 -5
  191. package/src/testing/proto/schema.proto +0 -15
  192. package/src/testing/replicator.ts +0 -184
  193. package/src/util/cursor.ts +0 -29
  194. package/src/util/index.ts +0 -6
  195. package/src/util/util.ts +0 -18
  196. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  197. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  198. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  199. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  200. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  201. /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
- import React, { useRef, useState } from 'react';
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 TextEditorRef, 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,8 +28,7 @@ const Story = ({
30
28
  text,
31
29
  automerge,
32
30
  ...props
33
- }: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'slots'>) => {
34
- const ref = useRef<TextEditorRef>(null);
31
+ }: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'placeholder' | 'slots'>) => {
35
32
  const [item] = useState({ text: new TextObject(text, undefined, undefined, { automerge }) });
36
33
  const model = useTextModel({ text: item.text });
37
34
  if (!model) {
@@ -40,10 +37,9 @@ const Story = ({
40
37
 
41
38
  return (
42
39
  <div className={mx(fixedInsetFlexLayout, groupSurface)}>
43
- <div className='flex justify-center overflow-y-scroll'>
40
+ <div className='flex justify-center'>
44
41
  <div className='flex flex-col w-[800px] py-16'>
45
- <TextEditor ref={ref} model={model} {...props} />
46
- <div className='flex shrink-0 h-[300px]'></div>
42
+ <TextEditor model={model} {...props} />
47
43
  </div>
48
44
  </div>
49
45
  </div>
@@ -58,49 +54,19 @@ export default {
58
54
  };
59
55
 
60
56
  export const Default = {
61
- render: () => (
62
- <Story
63
- slots={defaultsDeep(
64
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
65
- defaultSlots,
66
- )}
67
- />
68
- ),
69
- };
70
-
71
- export const Listener = {
72
- render: () => (
73
- <Story
74
- slots={defaultsDeep(
75
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
76
- defaultSlots,
77
- )}
78
- extensions={[listener({ onChange: (text) => console.log(text) })]}
79
- />
80
- ),
57
+ render: () => <Story placeholder='Enter text...' />,
81
58
  };
82
59
 
83
60
  export const Text = {
84
- render: () => (
85
- <Story
86
- text={initialText}
87
- slots={defaultsDeep(
88
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
89
- defaultSlots,
90
- )}
91
- />
92
- ),
61
+ render: () => <Story text={initialText} placeholder='Enter text...' />,
93
62
  };
94
63
 
95
64
  export const Automerge = {
65
+ render: () => <Story text={initialText} placeholder='Enter text...' automerge />,
66
+ };
67
+
68
+ export const Listener = {
96
69
  render: () => (
97
- <Story
98
- automerge
99
- text={initialText}
100
- slots={defaultsDeep(
101
- { editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
102
- defaultSlots,
103
- )}
104
- />
70
+ <Story placeholder='Enter text...' extensions={[listener({ onChange: (text) => console.log(text) })]} />
105
71
  ),
106
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,18 +14,22 @@ import React, {
14
14
  useCallback,
15
15
  useEffect,
16
16
  useImperativeHandle,
17
+ useRef,
17
18
  useState,
18
19
  } from 'react';
19
20
 
20
- import { generateName } from '@dxos/display-name';
21
+ import { log } from '@dxos/log';
21
22
  import { useThemeContext } from '@dxos/react-ui';
22
- import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
23
+ import { attentionSurface, mx } from '@dxos/react-ui-theme';
24
+ import { isNotFalsy } from '@dxos/util';
23
25
 
24
- import { basicBundle, markdownBundle, setCommentRange } from '../../extensions';
25
- import { type CommentRange, type EditorModel } from '../../hooks';
26
+ import { createBasicBundle, createMarkdownExtensions } from '../../extensions';
27
+ import { type EditorModel } from '../../hooks';
26
28
  import { type ThemeStyles } from '../../styles';
27
29
  import { defaultTheme, markdownTheme, textTheme } from '../../themes';
30
+ import { logChanges } from '../../util';
28
31
 
32
+ // TODO(burdon): Change to enum?
29
33
  export const EditorModes = ['default', 'vim'] as const;
30
34
  export type EditorMode = (typeof EditorModes)[number];
31
35
 
@@ -34,133 +38,169 @@ export type CursorInfo = {
34
38
  to: number;
35
39
  line: number;
36
40
  lines: number;
41
+ length: number;
37
42
  after?: string;
38
43
  };
39
44
 
40
- export type TextEditorRef = {
41
- root: HTMLDivElement | null;
42
- state?: EditorState;
43
- view?: EditorView;
44
- };
45
-
46
45
  export type TextEditorSlots = {
47
46
  root?: Omit<ComponentProps<'div'>, 'ref'>;
48
47
  editor?: {
49
48
  className?: string;
50
- placeholder?: string;
51
- spellCheck?: boolean;
52
- tabIndex?: number;
53
- theme?: ThemeStyles;
49
+ };
50
+ content?: {
51
+ className?: string;
54
52
  };
55
53
  };
56
54
 
55
+ // TODO(burdon): Spellcheck?
57
56
  export type TextEditorProps = {
58
- model: EditorModel;
57
+ model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
59
58
  readonly?: boolean; // TODO(burdon): Move into model.
60
- comments?: CommentRange[]; // TODO(burdon): Move into extension.
61
- extensions?: Extension[];
62
- 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;
63
66
  slots?: TextEditorSlots;
67
+ extensions?: Extension[];
68
+ debug?: boolean;
64
69
  };
65
70
 
66
71
  /**
67
72
  * Base text editor.
68
- * NOTE: Rather than adding properties, try to create extensions that can be reused.
69
73
  */
70
- export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
71
- ({ model, readonly, comments, extensions = [], editorMode, slots = defaultSlots }, forwardedRef) => {
74
+ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
75
+ (
76
+ {
77
+ model,
78
+ readonly,
79
+ autoFocus,
80
+ scrollTo,
81
+ selection,
82
+ editorMode,
83
+ theme,
84
+ slots = defaultSlots,
85
+ extensions = [],
86
+ debug,
87
+ },
88
+ forwardedRef,
89
+ ) => {
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' });
72
96
  const { themeMode } = useThemeContext();
73
- const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
74
- const [root, setRoot] = useState<HTMLDivElement | null>(null);
75
- const [{ state = undefined, view = undefined } = {}, setEditor] = useState<
76
- { state?: EditorState; view?: EditorView } | undefined
77
- >();
78
- useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
79
-
80
- // TODO(burdon): Factor out as extension.
81
- const { awareness, peer } = model;
82
- useEffect(() => {
83
- if (awareness && peer) {
84
- awareness.setLocalStateField('user', {
85
- name: peer.name ?? generateName(peer.id),
86
- color: getColorForValue({ value: peer.id, type: 'color' }),
87
- colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
88
- });
89
- }
90
- }, [awareness, peer, themeMode]);
91
97
 
92
- // TODO(burdon): Factor out as extension.
98
+ const rootRef = useRef<HTMLDivElement>(null);
99
+ const [view, setView] = useState<EditorView | null>(null);
100
+
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]);
104
+
105
+ // Set focus.
93
106
  useEffect(() => {
94
- if (view && comments?.length) {
95
- view.dispatch({
96
- effects: setCommentRange.of({ model, comments }),
97
- });
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();
98
114
  }
99
- }, [view, comments]);
115
+ }, [view, autoFocus]);
100
116
 
117
+ // Create editor state and view.
118
+ // The view is recreated if the model or extensions are changed.
101
119
  useEffect(() => {
102
- if (!root) {
120
+ if (!model || !rootRef.current) {
103
121
  return;
104
122
  }
105
123
 
106
- // TODO(burdon): Remember cursor position.
124
+ //
125
+ // EditorState
107
126
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
127
+ //
108
128
  const state = EditorState.create({
109
129
  doc: model.text(),
130
+ selection,
110
131
  extensions: [
111
- readonly && EditorState.readOnly.of(readonly),
112
-
113
- // TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
114
- editorMode === 'vim' && vim(),
132
+ // TODO(burdon): Doesn't catch errors in keymap functions.
133
+ EditorView.exceptionSink.of((err) => {
134
+ log.catch(err);
135
+ }),
115
136
 
116
137
  // Theme.
117
- // TODO(burdon): Make theme configurable.
138
+ // TODO(burdon): Make configurable.
118
139
  EditorView.baseTheme(defaultTheme),
119
- EditorView.theme(slots?.editor?.theme ?? {}),
140
+ EditorView.theme(theme ?? {}),
120
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),
121
148
 
122
149
  // Storage and replication.
150
+ // NOTE: This must come before user extensions.
123
151
  model.extension,
124
152
 
153
+ // TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
154
+ editorMode === 'vim' && vim(),
155
+
125
156
  // Custom.
126
157
  ...extensions,
127
- ].filter(Boolean) as Extension[],
158
+ ].filter(isNotFalsy),
128
159
  });
129
160
 
130
- // NOTE: This repaints the editor.
131
- // If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
132
- // Ideally this should not happen except when changing between text objects.
133
- view?.destroy();
134
- setEditor({
161
+ //
162
+ // EditorView
163
+ // https://codemirror.net/docs/ref/#view.EditorViewConfig
164
+ //
165
+ const newView = new EditorView({
135
166
  state,
136
- view: new EditorView({
137
- state,
138
- parent: root,
139
- // NOTE: Uncomment to spy on all transactions.
140
- // https://codemirror.net/docs/ref/#view.EditorView.dispatch
141
- // dispatch: (transaction, view) => {
142
- // view.update([transaction]);
143
- // },
144
- }),
167
+ parent: rootRef.current,
168
+ scrollTo,
169
+ // NOTE: Uncomment to debug/monitor all transactions.
170
+ // https://codemirror.net/docs/ref/#view.EditorView.dispatch
171
+ dispatchTransactions: (trs, view) => {
172
+ if (debug) {
173
+ logChanges(trs);
174
+ }
175
+ view.update(trs);
176
+ },
145
177
  });
146
178
 
179
+ view?.destroy();
180
+ setView(newView);
181
+
147
182
  return () => {
148
- view?.destroy();
149
- setEditor(undefined);
183
+ newView?.destroy();
184
+ setView(null);
150
185
  };
151
- }, [root, model, readonly, editorMode, themeMode]);
186
+ }, [rootRef, model, readonly, editorMode, themeMode]);
152
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?
153
191
  const handleKeyUp = useCallback(
154
192
  (event: KeyboardEvent) => {
155
193
  const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
156
194
  switch (key) {
157
195
  case 'Enter': {
158
- view?.contentDOM.focus();
196
+ view?.focus();
159
197
  break;
160
198
  }
161
199
 
162
200
  case 'Escape': {
163
- editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && root?.focus();
201
+ if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
202
+ view?.focus();
203
+ }
164
204
  break;
165
205
  }
166
206
  }
@@ -171,42 +211,45 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
171
211
  return (
172
212
  <div
173
213
  key={model.id}
174
- ref={setRoot}
214
+ role='none'
175
215
  tabIndex={0}
176
- {...slots?.root}
177
- {...(editorMode !== 'vim' && tabsterDOMAttribute)}
216
+ {...slots.root}
217
+ // {...(editorMode !== 'vim' && tabsterDOMAttribute)}
178
218
  onKeyUp={handleKeyUp}
219
+ ref={rootRef}
179
220
  />
180
221
  );
181
222
  },
182
223
  );
183
224
 
184
- export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
185
- ({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
225
+ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
226
+ ({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
186
227
  const { themeMode } = useThemeContext();
187
- const slots = defaultsDeep({}, _slots, defaultTextSlots);
228
+ const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
188
229
  return (
189
230
  <BaseTextEditor
190
231
  ref={forwardedRef}
191
232
  readonly={readonly}
192
- extensions={[basicBundle({ readonly, themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
193
- slots={slots}
233
+ extensions={[createBasicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
234
+ theme={theme}
235
+ slots={updatedSlots}
194
236
  {...props}
195
237
  />
196
238
  );
197
239
  },
198
240
  );
199
241
 
200
- export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
201
- ({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
242
+ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
243
+ ({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
202
244
  const { themeMode } = useThemeContext();
203
- const slots = defaultsDeep({}, _slots, defaultMarkdownSlots);
245
+ const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
204
246
  return (
205
247
  <BaseTextEditor
206
248
  ref={forwardedRef}
207
249
  readonly={readonly}
208
- extensions={[markdownBundle({ readonly, themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
209
- slots={slots}
250
+ extensions={[createMarkdownExtensions({ themeMode, placeholder }), ...extensions]}
251
+ theme={theme}
252
+ slots={updatedSlots}
210
253
  {...props}
211
254
  />
212
255
  );
@@ -215,20 +258,18 @@ export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
215
258
 
216
259
  export const defaultSlots: TextEditorSlots = {
217
260
  root: {
218
- className: mx('p-2', inputSurface),
261
+ // TODO(burdon): Add focusRing by default/as property?
262
+ className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
263
+ },
264
+ editor: {
265
+ className: 'h-full p-2',
219
266
  },
220
267
  };
221
268
 
222
269
  export const defaultTextSlots: TextEditorSlots = {
223
270
  ...defaultSlots,
224
- editor: {
225
- theme: textTheme,
226
- },
227
271
  };
228
272
 
229
273
  export const defaultMarkdownSlots: TextEditorSlots = {
230
274
  ...defaultSlots,
231
- editor: {
232
- theme: markdownTheme,
233
- },
234
275
  };
@@ -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}