@dxos/react-ui-editor 0.4.8-main.0602afb → 0.4.8-main.2d8e727

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 (108) hide show
  1. package/dist/lib/browser/index.mjs +939 -777
  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 +103 -0
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +34 -18
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +5 -79
  9. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  10. package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
  11. package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
  12. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -1
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  14. package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
  15. package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
  16. package/dist/types/src/extensions/automerge/defs.d.ts +15 -1
  17. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/index.d.ts +1 -0
  19. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/sync.d.ts +1 -2
  21. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -2
  23. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  24. package/dist/types/src/extensions/awareness.d.ts.map +1 -0
  25. package/dist/types/src/extensions/comments.d.ts +2 -2
  26. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  27. package/dist/types/src/extensions/cursor-line-margin.d.ts +7 -6
  28. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
  29. package/dist/types/src/extensions/cursor.d.ts +1 -1
  30. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  31. package/dist/types/src/extensions/index.d.ts +1 -3
  32. package/dist/types/src/extensions/index.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/bundle.d.ts +4 -5
  34. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  35. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  36. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
  37. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  38. package/dist/types/src/extensions/markdown/table.d.ts +2 -2
  39. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
  40. package/dist/types/src/extensions/outliner.d.ts +4 -0
  41. package/dist/types/src/extensions/outliner.d.ts.map +1 -0
  42. package/dist/types/src/{extensions/awareness → hooks}/awareness-provider.d.ts +1 -1
  43. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
  44. package/dist/types/src/hooks/defs.d.ts +20 -0
  45. package/dist/types/src/hooks/defs.d.ts.map +1 -0
  46. package/dist/types/src/hooks/index.d.ts +4 -1
  47. package/dist/types/src/hooks/index.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useEditorView.d.ts +20 -0
  49. package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
  50. package/dist/types/src/hooks/useTextEditor.d.ts +44 -3
  51. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useTextEditor.stories.d.ts +0 -6
  53. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useTextModel.d.ts +33 -0
  55. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -0
  56. package/dist/types/src/themes/default.d.ts +3 -0
  57. package/dist/types/src/themes/default.d.ts.map +1 -1
  58. package/package.json +24 -24
  59. package/src/components/TextEditor/MarkdownEditor.stories.tsx +524 -0
  60. package/src/components/TextEditor/TextEditor.stories.tsx +23 -508
  61. package/src/components/TextEditor/TextEditor.tsx +149 -37
  62. package/src/components/Toolbar/Toolbar.stories.tsx +42 -42
  63. package/src/extensions/automerge/automerge.stories.tsx +17 -16
  64. package/src/extensions/automerge/automerge.ts +1 -2
  65. package/src/extensions/automerge/cursor.ts +1 -1
  66. package/src/extensions/automerge/defs.ts +22 -1
  67. package/src/extensions/automerge/index.ts +1 -0
  68. package/src/extensions/automerge/sync.ts +9 -2
  69. package/src/extensions/automerge/update-automerge.ts +1 -2
  70. package/src/extensions/{awareness/awareness.ts → awareness.ts} +1 -1
  71. package/src/extensions/command/state.ts +1 -1
  72. package/src/extensions/comments.ts +5 -9
  73. package/src/extensions/cursor-line-margin.ts +18 -17
  74. package/src/extensions/cursor.ts +1 -1
  75. package/src/extensions/index.ts +1 -3
  76. package/src/extensions/markdown/bundle.ts +28 -11
  77. package/src/extensions/markdown/decorate.ts +3 -1
  78. package/src/extensions/markdown/formatting.ts +10 -15
  79. package/src/extensions/markdown/image.ts +0 -1
  80. package/src/extensions/markdown/table.ts +3 -11
  81. package/src/extensions/outliner.ts +12 -0
  82. package/src/{extensions/awareness → hooks}/awareness-provider.ts +1 -1
  83. package/src/hooks/defs.ts +30 -0
  84. package/src/hooks/index.ts +5 -1
  85. package/src/hooks/useEditorView.ts +32 -0
  86. package/src/hooks/useTextEditor.stories.tsx +34 -39
  87. package/src/hooks/useTextEditor.ts +120 -11
  88. package/src/hooks/useTextModel.ts +121 -0
  89. package/src/index.ts +1 -1
  90. package/src/themes/default.ts +28 -1
  91. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +0 -1
  92. package/dist/types/src/extensions/awareness/awareness.d.ts.map +0 -1
  93. package/dist/types/src/extensions/awareness/index.d.ts +0 -3
  94. package/dist/types/src/extensions/awareness/index.d.ts.map +0 -1
  95. package/dist/types/src/extensions/doc.d.ts +0 -3
  96. package/dist/types/src/extensions/doc.d.ts.map +0 -1
  97. package/dist/types/src/extensions/factories.d.ts +0 -49
  98. package/dist/types/src/extensions/factories.d.ts.map +0 -1
  99. package/dist/types/src/extensions/types.d.ts +0 -9
  100. package/dist/types/src/extensions/types.d.ts.map +0 -1
  101. package/dist/types/src/hooks/useDocAccessor.d.ts +0 -7
  102. package/dist/types/src/hooks/useDocAccessor.d.ts.map +0 -1
  103. package/src/extensions/awareness/index.ts +0 -6
  104. package/src/extensions/doc.ts +0 -14
  105. package/src/extensions/factories.ts +0 -170
  106. package/src/extensions/types.ts +0 -16
  107. package/src/hooks/useDocAccessor.ts +0 -21
  108. /package/dist/types/src/extensions/{awareness/awareness.d.ts → awareness.d.ts} +0 -0
@@ -2,10 +2,12 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { EditorState, type EditorStateConfig, type StateEffect } from '@codemirror/state';
6
- import { EditorView } from '@codemirror/view';
5
+ import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
6
+ import { EditorView, scrollPastEnd } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
+ import defaultsDeep from 'lodash.defaultsdeep';
8
9
  import React, {
10
+ type ComponentProps,
9
11
  type KeyboardEventHandler,
10
12
  forwardRef,
11
13
  useCallback,
@@ -13,12 +15,19 @@ import React, {
13
15
  useImperativeHandle,
14
16
  useRef,
15
17
  useState,
18
+ useMemo,
16
19
  } from 'react';
17
20
 
21
+ import { DocAccessor } from '@dxos/echo-schema';
18
22
  import { log } from '@dxos/log';
23
+ import { useThemeContext } from '@dxos/react-ui';
24
+ import { focusRing } from '@dxos/react-ui-theme';
19
25
  import { isNotFalsy } from '@dxos/util';
20
26
 
21
- import { documentId, editorMode } from '../../extensions';
27
+ import { createMarkdownExtensions, editorMode } from '../../extensions';
28
+ import { createBasicExtensions, createThemeExtensions, type EditorModel } from '../../hooks';
29
+ import { type ThemeStyles } from '../../styles';
30
+ import { defaultTheme, markdownTheme, textTheme } from '../../themes';
22
31
  import { logChanges } from '../../util';
23
32
 
24
33
  export type CursorInfo = {
@@ -30,49 +39,61 @@ export type CursorInfo = {
30
39
  after?: string;
31
40
  };
32
41
 
33
- export type TextEditorProps = Pick<EditorStateConfig, 'doc' | 'selection' | 'extensions'> & {
34
- id?: string;
35
- className?: string;
42
+ export type TextEditorSlots = {
43
+ root?: Omit<ComponentProps<'div'>, 'ref'>;
44
+ editor?: {
45
+ className?: string;
46
+ };
47
+ content?: {
48
+ className?: string;
49
+ };
50
+ };
51
+
52
+ export type TextEditorProps = {
53
+ model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
54
+ readonly?: boolean; // TODO(burdon): Move into model.
36
55
  autoFocus?: boolean;
37
- scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
56
+ scrollPastEnd?: boolean;
38
57
  moveToEndOfLine?: boolean;
58
+ lineWrapping?: boolean;
59
+ scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
60
+ selection?: { anchor: number; head?: number };
61
+ placeholder?: string;
62
+ theme?: ThemeStyles;
63
+ slots?: TextEditorSlots;
64
+ extensions?: Extension[];
39
65
  debug?: boolean;
40
- dataTestId?: string;
41
66
  };
42
67
 
43
- let instanceCount = 0;
44
-
45
68
  /**
46
- * Thin wrapper for text editor.
47
- * Handles tabster and focus management.
69
+ * Base text editor.
48
70
  */
49
- // TODO(burdon): Use useTextEditor internally.
50
- export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
71
+ // TODO(burdon): Replace with useTextEditor.
72
+ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
51
73
  (
52
74
  {
53
- id,
54
- doc,
55
- selection,
56
- extensions,
57
- className,
75
+ model,
76
+ readonly,
58
77
  autoFocus,
59
78
  scrollTo = EditorView.scrollIntoView(0),
60
79
  moveToEndOfLine,
80
+ selection,
81
+ theme,
82
+ slots = defaultSlots,
83
+ extensions = [],
84
+ scrollPastEnd: _scrollPastEnd,
61
85
  debug,
62
- dataTestId,
63
86
  },
64
87
  forwardedRef,
65
88
  ) => {
66
- // TODO(burdon): Increments by 2!
67
- const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
68
-
69
- // TODO(burdon): Make tabster optional.
70
89
  const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
90
+ const { themeMode } = useThemeContext();
91
+
71
92
  const rootRef = useRef<HTMLDivElement>(null);
72
93
  const [view, setView] = useState<EditorView | null>(null);
73
94
 
74
95
  // The view ref can be used to focus the editor.
75
- // NOTE: Ref updates do not cause the parent to re-render; also the ref is not available immediately.
96
+ // NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
76
97
  useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
77
98
 
78
99
  // Set focus.
@@ -85,22 +106,34 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
85
106
  // Create editor state and view.
86
107
  // The view is recreated if the model or extensions are changed.
87
108
  useEffect(() => {
88
- log('updating', { id, instanceId });
109
+ if (!model || !rootRef.current) {
110
+ return;
111
+ }
89
112
 
90
113
  //
91
114
  // EditorState
92
115
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
93
116
  //
94
117
  const state = EditorState.create({
95
- doc,
118
+ doc: typeof model.content === 'string' ? model.content : DocAccessor.getValue(model.content),
96
119
  selection,
97
120
  extensions: [
98
- id && documentId.of(id),
99
- // TODO(burdon): NOTE: Doesn't catch errors in keymap functions.
121
+ // TODO(burdon): Doesn't catch errors in keymap functions.
100
122
  EditorView.exceptionSink.of((err) => {
101
123
  log.catch(err);
102
124
  }),
103
125
 
126
+ // Theme.
127
+ // TODO(burdon): Make base theme configurable.
128
+ EditorView.baseTheme(defaultTheme),
129
+ theme && EditorView.theme(theme),
130
+ EditorView.darkTheme.of(themeMode === 'dark'),
131
+ slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
132
+ slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
133
+
134
+ // NOTE: Assumes default line height.
135
+ _scrollPastEnd && scrollPastEnd(),
136
+
104
137
  // Focus.
105
138
  EditorView.updateListener.of((update) => {
106
139
  update.transactions.forEach((transaction) => {
@@ -110,7 +143,16 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
110
143
  });
111
144
  }),
112
145
 
113
- extensions,
146
+ // State.
147
+ EditorView.editable.of(!readonly),
148
+ EditorState.readOnly.of(!!readonly),
149
+
150
+ // Storage and replication.
151
+ // NOTE: This must come before user extensions.
152
+ model.extension,
153
+
154
+ // Custom.
155
+ ...extensions,
114
156
  ].filter(isNotFalsy),
115
157
  });
116
158
 
@@ -120,7 +162,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
120
162
  //
121
163
  const view = new EditorView({
122
164
  state,
123
- parent: rootRef.current!,
165
+ parent: rootRef.current,
124
166
  scrollTo,
125
167
  // NOTE: Uncomment to debug/monitor all transactions.
126
168
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
@@ -134,7 +176,6 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
134
176
 
135
177
  setView(view);
136
178
 
137
- // Position cursor at end of line.
138
179
  if (moveToEndOfLine) {
139
180
  const { to } = view.state.doc.lineAt(0);
140
181
  view?.dispatch({ selection: { anchor: to } });
@@ -142,14 +183,15 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
142
183
 
143
184
  // Remove tabster attribute (rely on custom keymap).
144
185
  if (state.facet(editorMode).noTabster) {
145
- rootRef.current?.removeAttribute('data-tabster');
186
+ rootRef.current.removeAttribute('data-tabster');
146
187
  }
147
188
 
148
189
  return () => {
149
190
  view?.destroy();
150
191
  setView(null);
151
192
  };
152
- }, [doc, selection, extensions]);
193
+ // TODO(wittjosiah): Does `rootRef` ever change? Only `.current` changes?
194
+ }, [rootRef, extensions, model, readonly, themeMode]);
153
195
 
154
196
  // Focus editor on Enter (e.g., when tabbing to this component).
155
197
  const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
@@ -169,12 +211,82 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
169
211
  <div
170
212
  role='none'
171
213
  ref={rootRef}
214
+ key={model.id} // TODO(burdon): Remove?
172
215
  tabIndex={0}
173
- className={className}
174
- data-testid={dataTestId}
175
- {...tabsterDOMAttribute}
176
216
  onKeyUp={handleKeyUp}
217
+ {...slots.root}
218
+ {...tabsterDOMAttribute}
177
219
  />
178
220
  );
179
221
  },
180
222
  );
223
+
224
+ // TODO(burdon): Replace with hooks.
225
+ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
226
+ (
227
+ { readonly, placeholder, lineWrapping = true, theme = textTheme, slots, extensions: _extensions, ...props },
228
+ forwardedRef,
229
+ ) => {
230
+ const { themeMode } = useThemeContext();
231
+ const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
232
+ const extensions = useMemo(
233
+ () => [
234
+ createBasicExtensions({ lineWrapping, placeholder }),
235
+ createThemeExtensions({ themeMode }),
236
+ ...(_extensions ?? []),
237
+ ],
238
+ [themeMode, placeholder, lineWrapping, _extensions],
239
+ );
240
+
241
+ return (
242
+ <BaseTextEditor
243
+ ref={forwardedRef}
244
+ readonly={readonly}
245
+ extensions={extensions}
246
+ theme={theme}
247
+ slots={updatedSlots}
248
+ {...props}
249
+ />
250
+ );
251
+ },
252
+ );
253
+
254
+ // TODO(burdon): Replace with hooks.
255
+ export const MarkdownEditor = forwardRef<EditorView | null, TextEditorProps>(
256
+ ({ readonly, placeholder, theme = markdownTheme, slots, extensions: _extensions, ...props }, forwardedRef) => {
257
+ const { themeMode } = useThemeContext();
258
+ const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
259
+ const extensions = useMemo(
260
+ () => [createMarkdownExtensions({ themeMode, placeholder }), ...(_extensions ?? [])],
261
+ [themeMode, placeholder, _extensions],
262
+ );
263
+
264
+ return (
265
+ <BaseTextEditor
266
+ ref={forwardedRef}
267
+ readonly={readonly}
268
+ extensions={extensions}
269
+ theme={theme}
270
+ slots={updatedSlots}
271
+ {...props}
272
+ />
273
+ );
274
+ },
275
+ );
276
+
277
+ export const defaultSlots: TextEditorSlots = {
278
+ root: {
279
+ className: focusRing,
280
+ },
281
+ editor: {
282
+ className: 'bs-full',
283
+ },
284
+ };
285
+
286
+ export const defaultTextSlots: TextEditorSlots = {
287
+ ...defaultSlots,
288
+ };
289
+
290
+ export const defaultMarkdownSlots: TextEditorSlots = {
291
+ ...defaultSlots,
292
+ };
@@ -4,70 +4,61 @@
4
4
 
5
5
  import '@dxosTheme';
6
6
 
7
- import React, { type FC, useState } from 'react';
7
+ import React, { type FC, useMemo, useState } from 'react';
8
8
 
9
9
  import { TextObject } from '@dxos/echo-schema';
10
10
  import { PublicKey } from '@dxos/keys';
11
11
  import { faker } from '@dxos/random';
12
- import { Tooltip, useThemeContext } from '@dxos/react-ui';
13
- import { textBlockWidth } from '@dxos/react-ui-theme';
12
+ import { Tooltip } from '@dxos/react-ui';
13
+ import { mx, textBlockWidth } from '@dxos/react-ui-theme';
14
14
  import { withTheme } from '@dxos/storybook-utils';
15
15
 
16
16
  import { Toolbar } from './Toolbar';
17
17
  import {
18
- type Comment,
19
18
  decorateMarkdown,
20
19
  comments,
21
- createBasicExtensions,
22
- createDataExtensions,
23
- createThemeExtensions,
24
- formattingKeymap,
20
+ formatting,
21
+ image,
25
22
  table,
26
23
  useComments,
27
24
  useFormattingState,
28
- createMarkdownExtensions,
29
- image,
30
25
  } from '../../extensions';
31
- import { useActionHandler, useDocAccessor, useTextEditor } from '../../hooks';
26
+ import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
32
27
  import translations from '../../translations';
28
+ import { MarkdownEditor } from '../TextEditor';
33
29
 
34
30
  faker.seed(101);
35
31
 
36
- const Story: FC<{ content: string }> = ({ content }) => {
37
- const { themeMode } = useThemeContext();
32
+ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) => {
38
33
  const [item] = useState({ text: new TextObject(content) });
39
- const { id, doc, accessor } = useDocAccessor(item.text);
34
+ const [_comments, setComments] = useState<Comment[]>([]);
35
+ const [editorRef, viewInvalidated] = useEditorView(id);
36
+ const model = useTextModel({ text: item.text });
40
37
  const [formattingState, formattingObserver] = useFormattingState();
41
- const { parentRef, view } = useTextEditor(
42
- () => ({
43
- id,
44
- doc,
45
- extensions: [
46
- createBasicExtensions(),
47
- createMarkdownExtensions({ themeMode }),
48
- createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2' } } }),
49
- createDataExtensions({ id, text: accessor }),
50
- comments({
51
- onCreate: ({ cursor }) => {
52
- const id = PublicKey.random().toHex();
53
- setComments((comments) => [...comments, { id, cursor }]);
54
- return id;
55
- },
56
- }),
57
- decorateMarkdown(),
58
- formattingKeymap(),
59
- image(),
60
- table(),
61
- formattingObserver,
62
- ],
63
- }),
64
- [id, accessor, themeMode],
38
+ const extensions = useMemo(
39
+ () => [
40
+ decorateMarkdown(),
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
+ image(),
50
+ table(),
51
+ formattingObserver,
52
+ ],
53
+ [],
65
54
  );
66
55
 
67
- const [_comments, setComments] = useState<Comment[]>([]);
68
- useComments(view, id, _comments);
56
+ useComments(viewInvalidated ? null : editorRef.current, id, _comments);
57
+ const handleAction = useActionHandler(editorRef.current);
69
58
 
70
- const handleAction = useActionHandler(view);
59
+ if (!model) {
60
+ return null;
61
+ }
71
62
 
72
63
  return (
73
64
  <div role='none' className='fixed inset-0 flex flex-col'>
@@ -76,7 +67,16 @@ const Story: FC<{ content: string }> = ({ content }) => {
76
67
  <Toolbar.Separator />
77
68
  <Toolbar.Extended />
78
69
  </Toolbar.Root>
79
- <div role='textbox' ref={parentRef} className={textBlockWidth} />;
70
+
71
+ <MarkdownEditor
72
+ ref={editorRef}
73
+ model={model}
74
+ extensions={extensions}
75
+ slots={{
76
+ root: { className: mx(textBlockWidth) },
77
+ editor: { className: 'p-2' },
78
+ }}
79
+ />
80
80
  </div>
81
81
  );
82
82
  };
@@ -9,7 +9,7 @@ import '@preact/signals-react';
9
9
  import React, { useEffect, useMemo, useState } from 'react';
10
10
 
11
11
  import { Repo } from '@dxos/automerge/automerge-repo';
12
- import { createDocAccessor, Filter, DocAccessor } from '@dxos/echo-schema';
12
+ import { createDocAccessor, Filter } from '@dxos/echo-schema';
13
13
  import { type PublicKey } from '@dxos/keys';
14
14
  import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
15
15
  import { ClientRepeater } from '@dxos/react-client/testing';
@@ -17,9 +17,9 @@ import { useThemeContext } from '@dxos/react-ui';
17
17
  import { withTheme } from '@dxos/storybook-utils';
18
18
 
19
19
  import { automerge } from './automerge';
20
- import { useTextEditor } from '../../hooks';
20
+ import { DocAccessor } from './defs';
21
+ import { createBasicExtensions, createThemeExtensions, useTextEditor } from '../../hooks';
21
22
  import translations from '../../translations';
22
- import { createBasicExtensions, createThemeExtensions } from '../factories';
23
23
 
24
24
  const initialContent = 'Hello world!';
25
25
 
@@ -34,19 +34,21 @@ type EditorProps = {
34
34
 
35
35
  const Editor = ({ source, autoFocus }: EditorProps) => {
36
36
  const { themeMode } = useThemeContext();
37
- const { parentRef } = useTextEditor(
38
- () => ({
39
- doc: DocAccessor.getValue(source),
40
- extensions: [
41
- createBasicExtensions({ placeholder: 'Type here...' }),
42
- createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2 bg-white dark:bg-black' } } }),
43
- automerge(source),
44
- ],
45
- autoFocus,
46
- }),
47
- [source, themeMode],
37
+ const extensions = useMemo(
38
+ () => [
39
+ createBasicExtensions({ placeholder: 'Type here...' }),
40
+ createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2 bg-white dark:bg-black' } } }),
41
+ automerge(source),
42
+ ],
43
+ [themeMode, source],
48
44
  );
49
45
 
46
+ const { parentRef } = useTextEditor({
47
+ doc: DocAccessor.getValue(source),
48
+ extensions,
49
+ autoFocus,
50
+ });
51
+
50
52
  return <div ref={parentRef} />;
51
53
  };
52
54
 
@@ -112,8 +114,7 @@ const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
112
114
 
113
115
  export const Default = {};
114
116
 
115
- // TODO(burdon): Error:
116
- // chunk-6NX3RPDS.mjs:2021 ControlPipeline#5 Error: invariant violation: Feed already added
117
+ // TODO(burdon): Feed already added error.
117
118
  export const WithEcho = {
118
119
  decorators: [withTheme],
119
120
  render: () => {
@@ -8,10 +8,9 @@ import { StateField, type Extension } from '@codemirror/state';
8
8
  import { EditorView, ViewPlugin } from '@codemirror/view';
9
9
 
10
10
  import { next as A } from '@dxos/automerge/automerge';
11
- import { type DocAccessor } from '@dxos/echo-schema';
12
11
 
13
12
  import { cursorConverter } from './cursor';
14
- import { updateHeadsEffect, isReconcile, type State } from './defs';
13
+ import { updateHeadsEffect, isReconcile, type State, type DocAccessor } from './defs';
15
14
  import { Syncer } from './sync';
16
15
  import { Cursor } from '../cursor';
17
16
 
@@ -5,9 +5,9 @@
5
5
  import get from 'lodash.get';
6
6
 
7
7
  import { next as A } from '@dxos/automerge/automerge';
8
- import { type DocAccessor } from '@dxos/echo-schema';
9
8
  import { log } from '@dxos/log';
10
9
 
10
+ import { type DocAccessor } from './defs';
11
11
  import { type CursorConverter } from '../cursor';
12
12
 
13
13
  export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter => ({
@@ -5,8 +5,9 @@
5
5
  //
6
6
 
7
7
  import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
8
+ import get from 'lodash.get';
8
9
 
9
- import { type Heads, type Prop } from '@dxos/automerge/automerge';
10
+ import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
10
11
 
11
12
  export type State = {
12
13
  path: Prop[];
@@ -30,3 +31,23 @@ export const reconcileAnnotation = Annotation.define<boolean>();
30
31
  export const isReconcile = (tr: Transaction): boolean => {
31
32
  return !!tr.annotation(reconcileAnnotation);
32
33
  };
34
+
35
+ // TODO(burdon): Reconcile with echo.
36
+ export interface DocAccessor<T = any> {
37
+ handle: IDocHandle<T>;
38
+ get path(): string[]; // TODO(burdon): Getter or prop?
39
+ }
40
+
41
+ // TODO(burdon): Reconcile with echo.
42
+ export const DocAccessor = {
43
+ getValue: (accessor: DocAccessor) => get(accessor.handle, accessor.path),
44
+ };
45
+
46
+ // TODO(burdon): Reconcile with echo.
47
+ export interface IDocHandle<T = any> {
48
+ docSync(): Doc<T> | undefined;
49
+ change(callback: ChangeFn<T>, options?: ChangeOptions<T>): void;
50
+ changeAt(heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): string[] | undefined;
51
+ addListener(event: 'change', listener: () => void): void;
52
+ removeListener(event: 'change', listener: () => void): void;
53
+ }
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export { automerge } from './automerge';
6
+ export { type IDocHandle } from './defs';
@@ -8,9 +8,16 @@ import { type StateField } from '@codemirror/state';
8
8
  import { type EditorView } from '@codemirror/view';
9
9
 
10
10
  import { next as A } from '@dxos/automerge/automerge';
11
- import { type IDocHandle } from '@dxos/echo-schema';
12
11
 
13
- import { getLastHeads, getPath, type State, reconcileAnnotation, updateHeads, isReconcile } from './defs';
12
+ import {
13
+ getLastHeads,
14
+ getPath,
15
+ type IDocHandle,
16
+ type State,
17
+ reconcileAnnotation,
18
+ updateHeads,
19
+ isReconcile,
20
+ } from './defs';
14
21
  import { updateAutomerge } from './update-automerge';
15
22
  import { updateCodeMirror } from './update-codemirror';
16
23
 
@@ -7,9 +7,8 @@
7
7
  import { type EditorState, type StateField, type Transaction, type Text } from '@codemirror/state';
8
8
 
9
9
  import { next as A, type Heads } from '@dxos/automerge/automerge';
10
- import { type IDocHandle } from '@dxos/echo-schema';
11
10
 
12
- import { type State } from './defs';
11
+ import { type IDocHandle, type State } from './defs';
13
12
 
14
13
  export const updateAutomerge = (
15
14
  field: StateField<State>,
@@ -16,7 +16,7 @@ import {
16
16
  import { Event } from '@dxos/async';
17
17
  import { Context } from '@dxos/context';
18
18
 
19
- import { Cursor, type CursorConverter } from '../cursor';
19
+ import { Cursor, type CursorConverter } from './cursor';
20
20
 
21
21
  export interface AwarenessProvider {
22
22
  remoteStateChange: Event<void>;
@@ -74,7 +74,7 @@ export const commandState = StateField.define<CommandState>({
74
74
 
75
75
  return state;
76
76
  },
77
- provide: (field) => [showTooltip.from(field, (value) => value.tooltip ?? null)],
77
+ provide: (f) => [showTooltip.from(f, (value) => value.tooltip ?? null)],
78
78
  });
79
79
 
80
80
  export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
@@ -13,7 +13,7 @@ import { log } from '@dxos/log';
13
13
  import { nonNullable } from '@dxos/util';
14
14
 
15
15
  import { Cursor } from './cursor';
16
- import { type Comment, type Range } from './types';
16
+ import { type Comment, type Range } from '../hooks';
17
17
  import { getToken } from '../styles';
18
18
  import { callbackWrapper } from '../util';
19
19
 
@@ -508,16 +508,12 @@ export const focusComment = (view: EditorView, id: string, center = true) => {
508
508
  /**
509
509
  * Update comments state field.
510
510
  */
511
- export const useComments = (view: EditorView | null | undefined, id: string, comments: Comment[] = []) => {
511
+ export const useComments = (view: EditorView | null, id: string, comments: Comment[] = []) => {
512
512
  useEffect(() => {
513
513
  if (view) {
514
- // Check same document.
515
- // NOTE: Hook might be called before editor state is updated.
516
- if (id === view.state.facet(documentId)) {
517
- view.dispatch({
518
- effects: setComments.of({ id, comments }),
519
- });
520
- }
514
+ view.dispatch({
515
+ effects: setComments.of({ id, comments }),
516
+ });
521
517
  }
522
518
  }, [id, view, comments]);
523
519
  };