@dxos/react-ui-editor 0.3.11-main.fbbdc2a → 0.3.11-main.fc97a54

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 (202) hide show
  1. package/dist/lib/browser/index.mjs +2904 -1394
  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/hooks.stories.d.ts +23 -0
  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/semaphore.d.ts +8 -12
  32. package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
  33. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
  34. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  35. package/dist/types/src/extensions/awareness.d.ts +2 -2
  36. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  37. package/dist/types/src/extensions/basic.d.ts +4 -5
  38. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  39. package/dist/types/src/extensions/comments.d.ts +41 -20
  40. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  41. package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
  42. package/dist/types/src/extensions/cursor.d.ts.map +1 -0
  43. package/dist/types/src/extensions/index.d.ts +2 -8
  44. package/dist/types/src/extensions/index.d.ts.map +1 -1
  45. package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
  46. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  47. package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
  48. package/dist/types/src/extensions/markdown/formatting.d.ts +58 -0
  49. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
  50. package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
  51. package/dist/types/src/extensions/markdown/formatting.test.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 +14 -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 +85 -92
  90. package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
  91. package/src/components/TextEditor/TextEditor.tsx +135 -95
  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 +23 -19
  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/semaphore.ts +33 -32
  105. package/src/extensions/automerge/update-automerge.ts +14 -8
  106. package/src/extensions/automerge/update-codemirror.ts +5 -5
  107. package/src/extensions/awareness.ts +46 -49
  108. package/src/extensions/basic.ts +17 -10
  109. package/src/extensions/blast.ts +4 -1
  110. package/src/extensions/comments.ts +332 -267
  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.test.ts +482 -0
  116. package/src/extensions/markdown/formatting.ts +1237 -0
  117. package/src/extensions/markdown/heading.ts +59 -0
  118. package/src/extensions/markdown/highlight.ts +11 -26
  119. package/src/extensions/markdown/hr.ts +71 -0
  120. package/src/extensions/markdown/image.ts +87 -0
  121. package/src/extensions/markdown/index.ts +8 -0
  122. package/src/extensions/{link.ts → markdown/link.ts} +90 -83
  123. package/src/extensions/{table.ts → markdown/table.ts} +10 -4
  124. package/src/extensions/markdown/tasklist.ts +110 -0
  125. package/src/extensions/mention.ts +9 -2
  126. package/src/extensions/outliner.ts +12 -0
  127. package/src/hooks/awareness-provider.ts +24 -20
  128. package/src/hooks/defs.ts +42 -0
  129. package/src/hooks/index.ts +5 -2
  130. package/src/hooks/useActionHandler.ts +93 -0
  131. package/src/hooks/useEditorView.ts +29 -0
  132. package/src/hooks/useTextEditor.ts +153 -0
  133. package/src/hooks/useTextModel.ts +67 -60
  134. package/src/index.ts +3 -0
  135. package/src/styles/markdown.ts +9 -12
  136. package/src/styles/tokens.ts +3 -0
  137. package/src/themes/default.ts +59 -83
  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 +0 -12
  142. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  143. package/dist/types/src/extensions/code.d.ts.map +0 -1
  144. package/dist/types/src/extensions/hr.d.ts.map +0 -1
  145. package/dist/types/src/extensions/image.d.ts.map +0 -1
  146. package/dist/types/src/extensions/link.d.ts.map +0 -1
  147. package/dist/types/src/extensions/mermaid.d.ts +0 -3
  148. package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
  149. package/dist/types/src/extensions/table.d.ts.map +0 -1
  150. package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
  151. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  152. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  153. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  154. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  155. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  156. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  157. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  158. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  159. package/dist/types/src/hooks/automerge.d.ts +0 -3
  160. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  161. package/dist/types/src/hooks/yjs.d.ts +0 -27
  162. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  163. package/dist/types/src/testing/index.d.ts +0 -3
  164. package/dist/types/src/testing/index.d.ts.map +0 -1
  165. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  166. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  167. package/dist/types/src/testing/proto/index.d.ts +0 -2
  168. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  169. package/dist/types/src/testing/replicator.d.ts +0 -45
  170. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  171. package/dist/types/src/util/cursor.d.ts.map +0 -1
  172. package/dist/types/src/util/index.d.ts +0 -3
  173. package/dist/types/src/util/index.d.ts.map +0 -1
  174. package/dist/types/src/util/util.d.ts +0 -5
  175. package/dist/types/src/util/util.d.ts.map +0 -1
  176. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  177. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  178. package/src/extensions/code.ts +0 -99
  179. package/src/extensions/hr.ts +0 -74
  180. package/src/extensions/image.ts +0 -69
  181. package/src/extensions/mermaid.ts +0 -11
  182. package/src/extensions/tasklist.ts +0 -121
  183. package/src/extensions/yjs/cursor.ts +0 -21
  184. package/src/extensions/yjs/index.ts +0 -5
  185. package/src/extensions/yjs/yjs.test.ts +0 -35
  186. package/src/extensions/yjs/yjs.ts +0 -16
  187. package/src/hooks/automerge.ts +0 -52
  188. package/src/hooks/yjs.ts +0 -178
  189. package/src/testing/index.ts +0 -6
  190. package/src/testing/proto/gen/schema.ts +0 -49
  191. package/src/testing/proto/index.ts +0 -5
  192. package/src/testing/proto/schema.proto +0 -15
  193. package/src/testing/replicator.ts +0 -184
  194. package/src/util/cursor.ts +0 -29
  195. package/src/util/index.ts +0 -6
  196. package/src/util/util.ts +0 -18
  197. /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
  198. /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
  199. /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
  200. /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
  201. /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
  202. /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
@@ -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 { focusRing, 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,17 @@ export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
215
258
 
216
259
  export const defaultSlots: TextEditorSlots = {
217
260
  root: {
218
- className: mx('p-2', inputSurface),
261
+ className: mx('grow', focusRing),
262
+ },
263
+ editor: {
264
+ className: 'bs-full',
219
265
  },
220
266
  };
221
267
 
222
268
  export const defaultTextSlots: TextEditorSlots = {
223
269
  ...defaultSlots,
224
- editor: {
225
- theme: textTheme,
226
- },
227
270
  };
228
271
 
229
272
  export const defaultMarkdownSlots: TextEditorSlots = {
230
273
  ...defaultSlots,
231
- editor: {
232
- theme: markdownTheme,
233
- },
234
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
+ };