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

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 (110) hide show
  1. package/dist/lib/browser/index.mjs +2489 -2651
  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/TextEditor.d.ts +18 -34
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +79 -5
  7. package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +0 -1
  9. package/dist/types/src/components/Toolbar/Toolbar.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 -2
  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 +1 -15
  17. package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
  18. package/dist/types/src/extensions/automerge/index.d.ts +0 -1
  19. package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
  20. package/dist/types/src/extensions/automerge/sync.d.ts +2 -1
  21. package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
  22. package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -1
  23. package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
  24. package/dist/types/src/{hooks → extensions/awareness}/awareness-provider.d.ts +1 -1
  25. package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
  26. package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
  27. package/dist/types/src/extensions/awareness/index.d.ts +3 -0
  28. package/dist/types/src/extensions/awareness/index.d.ts.map +1 -0
  29. package/dist/types/src/extensions/comments.d.ts +2 -2
  30. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  31. package/dist/types/src/extensions/cursor-line-margin.d.ts +6 -7
  32. package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
  33. package/dist/types/src/extensions/cursor.d.ts +1 -1
  34. package/dist/types/src/extensions/cursor.d.ts.map +1 -1
  35. package/dist/types/src/extensions/doc.d.ts +3 -0
  36. package/dist/types/src/extensions/doc.d.ts.map +1 -0
  37. package/dist/types/src/extensions/factories.d.ts +49 -0
  38. package/dist/types/src/extensions/factories.d.ts.map +1 -0
  39. package/dist/types/src/extensions/index.d.ts +3 -1
  40. package/dist/types/src/extensions/index.d.ts.map +1 -1
  41. package/dist/types/src/extensions/markdown/bundle.d.ts +5 -4
  42. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  43. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  44. package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
  45. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  46. package/dist/types/src/extensions/markdown/table.d.ts +2 -2
  47. package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
  48. package/dist/types/src/extensions/types.d.ts +9 -0
  49. package/dist/types/src/extensions/types.d.ts.map +1 -0
  50. package/dist/types/src/hooks/index.d.ts +1 -4
  51. package/dist/types/src/hooks/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useDocAccessor.d.ts +7 -0
  53. package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -0
  54. package/dist/types/src/hooks/useTextEditor.d.ts +3 -44
  55. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
  56. package/dist/types/src/hooks/useTextEditor.stories.d.ts +6 -1
  57. package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
  58. package/dist/types/src/themes/default.d.ts +0 -3
  59. package/dist/types/src/themes/default.d.ts.map +1 -1
  60. package/package.json +24 -24
  61. package/src/components/TextEditor/TextEditor.stories.tsx +508 -23
  62. package/src/components/TextEditor/TextEditor.tsx +37 -149
  63. package/src/components/Toolbar/Toolbar.stories.tsx +42 -42
  64. package/src/extensions/automerge/automerge.stories.tsx +16 -17
  65. package/src/extensions/automerge/automerge.ts +2 -1
  66. package/src/extensions/automerge/cursor.ts +1 -1
  67. package/src/extensions/automerge/defs.ts +1 -22
  68. package/src/extensions/automerge/index.ts +0 -1
  69. package/src/extensions/automerge/sync.ts +2 -9
  70. package/src/extensions/automerge/update-automerge.ts +2 -1
  71. package/src/{hooks → extensions/awareness}/awareness-provider.ts +1 -1
  72. package/src/extensions/{awareness.ts → awareness/awareness.ts} +1 -1
  73. package/src/extensions/awareness/index.ts +6 -0
  74. package/src/extensions/command/state.ts +1 -1
  75. package/src/extensions/comments.ts +9 -5
  76. package/src/extensions/cursor-line-margin.ts +17 -18
  77. package/src/extensions/cursor.ts +1 -1
  78. package/src/extensions/doc.ts +14 -0
  79. package/src/extensions/factories.ts +170 -0
  80. package/src/extensions/index.ts +3 -1
  81. package/src/extensions/markdown/bundle.ts +11 -28
  82. package/src/extensions/markdown/decorate.ts +1 -3
  83. package/src/extensions/markdown/formatting.ts +15 -10
  84. package/src/extensions/markdown/image.ts +1 -0
  85. package/src/extensions/markdown/table.ts +11 -3
  86. package/src/extensions/types.ts +16 -0
  87. package/src/hooks/index.ts +1 -5
  88. package/src/hooks/useDocAccessor.ts +21 -0
  89. package/src/hooks/useTextEditor.stories.tsx +39 -34
  90. package/src/hooks/useTextEditor.ts +11 -120
  91. package/src/index.ts +1 -1
  92. package/src/themes/default.ts +1 -28
  93. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +0 -103
  94. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +0 -1
  95. package/dist/types/src/extensions/awareness.d.ts.map +0 -1
  96. package/dist/types/src/extensions/outliner.d.ts +0 -4
  97. package/dist/types/src/extensions/outliner.d.ts.map +0 -1
  98. package/dist/types/src/hooks/awareness-provider.d.ts.map +0 -1
  99. package/dist/types/src/hooks/defs.d.ts +0 -20
  100. package/dist/types/src/hooks/defs.d.ts.map +0 -1
  101. package/dist/types/src/hooks/useEditorView.d.ts +0 -20
  102. package/dist/types/src/hooks/useEditorView.d.ts.map +0 -1
  103. package/dist/types/src/hooks/useTextModel.d.ts +0 -33
  104. package/dist/types/src/hooks/useTextModel.d.ts.map +0 -1
  105. package/src/components/TextEditor/MarkdownEditor.stories.tsx +0 -524
  106. package/src/extensions/outliner.ts +0 -12
  107. package/src/hooks/defs.ts +0 -30
  108. package/src/hooks/useEditorView.ts +0 -32
  109. package/src/hooks/useTextModel.ts +0 -121
  110. /package/dist/types/src/extensions/{awareness.d.ts → awareness/awareness.d.ts} +0 -0
@@ -2,12 +2,10 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
6
- import { EditorView, scrollPastEnd } from '@codemirror/view';
5
+ import { EditorState, type EditorStateConfig, type StateEffect } from '@codemirror/state';
6
+ import { EditorView } from '@codemirror/view';
7
7
  import { useFocusableGroup } from '@fluentui/react-tabster';
8
- import defaultsDeep from 'lodash.defaultsdeep';
9
8
  import React, {
10
- type ComponentProps,
11
9
  type KeyboardEventHandler,
12
10
  forwardRef,
13
11
  useCallback,
@@ -15,19 +13,12 @@ import React, {
15
13
  useImperativeHandle,
16
14
  useRef,
17
15
  useState,
18
- useMemo,
19
16
  } from 'react';
20
17
 
21
- import { DocAccessor } from '@dxos/echo-schema';
22
18
  import { log } from '@dxos/log';
23
- import { useThemeContext } from '@dxos/react-ui';
24
- import { focusRing } from '@dxos/react-ui-theme';
25
19
  import { isNotFalsy } from '@dxos/util';
26
20
 
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';
21
+ import { documentId, editorMode } from '../../extensions';
31
22
  import { logChanges } from '../../util';
32
23
 
33
24
  export type CursorInfo = {
@@ -39,61 +30,49 @@ export type CursorInfo = {
39
30
  after?: string;
40
31
  };
41
32
 
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.
33
+ export type TextEditorProps = Pick<EditorStateConfig, 'doc' | 'selection' | 'extensions'> & {
34
+ id?: string;
35
+ className?: string;
55
36
  autoFocus?: boolean;
56
- scrollPastEnd?: boolean;
57
- moveToEndOfLine?: boolean;
58
- lineWrapping?: boolean;
59
37
  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[];
38
+ moveToEndOfLine?: boolean;
65
39
  debug?: boolean;
40
+ dataTestId?: string;
66
41
  };
67
42
 
43
+ let instanceCount = 0;
44
+
68
45
  /**
69
- * Base text editor.
46
+ * Thin wrapper for text editor.
47
+ * Handles tabster and focus management.
70
48
  */
71
- // TODO(burdon): Replace with useTextEditor.
72
- export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
49
+ // TODO(burdon): Use useTextEditor internally.
50
+ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
73
51
  (
74
52
  {
75
- model,
76
- readonly,
53
+ id,
54
+ doc,
55
+ selection,
56
+ extensions,
57
+ className,
77
58
  autoFocus,
78
59
  scrollTo = EditorView.scrollIntoView(0),
79
60
  moveToEndOfLine,
80
- selection,
81
- theme,
82
- slots = defaultSlots,
83
- extensions = [],
84
- scrollPastEnd: _scrollPastEnd,
85
61
  debug,
62
+ dataTestId,
86
63
  },
87
64
  forwardedRef,
88
65
  ) => {
89
- const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
90
- const { themeMode } = useThemeContext();
66
+ // TODO(burdon): Increments by 2!
67
+ const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
91
68
 
69
+ // TODO(burdon): Make tabster optional.
70
+ const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
92
71
  const rootRef = useRef<HTMLDivElement>(null);
93
72
  const [view, setView] = useState<EditorView | null>(null);
94
73
 
95
74
  // The view ref can be used to focus the editor.
96
- // NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
75
+ // NOTE: Ref updates do not cause the parent to re-render; also the ref is not available immediately.
97
76
  useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
98
77
 
99
78
  // Set focus.
@@ -106,34 +85,22 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
106
85
  // Create editor state and view.
107
86
  // The view is recreated if the model or extensions are changed.
108
87
  useEffect(() => {
109
- if (!model || !rootRef.current) {
110
- return;
111
- }
88
+ log('updating', { id, instanceId });
112
89
 
113
90
  //
114
91
  // EditorState
115
92
  // https://codemirror.net/docs/ref/#state.EditorStateConfig
116
93
  //
117
94
  const state = EditorState.create({
118
- doc: typeof model.content === 'string' ? model.content : DocAccessor.getValue(model.content),
95
+ doc,
119
96
  selection,
120
97
  extensions: [
121
- // TODO(burdon): Doesn't catch errors in keymap functions.
98
+ id && documentId.of(id),
99
+ // TODO(burdon): NOTE: Doesn't catch errors in keymap functions.
122
100
  EditorView.exceptionSink.of((err) => {
123
101
  log.catch(err);
124
102
  }),
125
103
 
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
-
137
104
  // Focus.
138
105
  EditorView.updateListener.of((update) => {
139
106
  update.transactions.forEach((transaction) => {
@@ -143,16 +110,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
143
110
  });
144
111
  }),
145
112
 
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,
113
+ extensions,
156
114
  ].filter(isNotFalsy),
157
115
  });
158
116
 
@@ -162,7 +120,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
162
120
  //
163
121
  const view = new EditorView({
164
122
  state,
165
- parent: rootRef.current,
123
+ parent: rootRef.current!,
166
124
  scrollTo,
167
125
  // NOTE: Uncomment to debug/monitor all transactions.
168
126
  // https://codemirror.net/docs/ref/#view.EditorView.dispatch
@@ -176,6 +134,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
176
134
 
177
135
  setView(view);
178
136
 
137
+ // Position cursor at end of line.
179
138
  if (moveToEndOfLine) {
180
139
  const { to } = view.state.doc.lineAt(0);
181
140
  view?.dispatch({ selection: { anchor: to } });
@@ -183,15 +142,14 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
183
142
 
184
143
  // Remove tabster attribute (rely on custom keymap).
185
144
  if (state.facet(editorMode).noTabster) {
186
- rootRef.current.removeAttribute('data-tabster');
145
+ rootRef.current?.removeAttribute('data-tabster');
187
146
  }
188
147
 
189
148
  return () => {
190
149
  view?.destroy();
191
150
  setView(null);
192
151
  };
193
- // TODO(wittjosiah): Does `rootRef` ever change? Only `.current` changes?
194
- }, [rootRef, extensions, model, readonly, themeMode]);
152
+ }, [doc, selection, extensions]);
195
153
 
196
154
  // Focus editor on Enter (e.g., when tabbing to this component).
197
155
  const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
@@ -211,82 +169,12 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
211
169
  <div
212
170
  role='none'
213
171
  ref={rootRef}
214
- key={model.id} // TODO(burdon): Remove?
215
172
  tabIndex={0}
216
- onKeyUp={handleKeyUp}
217
- {...slots.root}
173
+ className={className}
174
+ data-testid={dataTestId}
218
175
  {...tabsterDOMAttribute}
176
+ onKeyUp={handleKeyUp}
219
177
  />
220
178
  );
221
179
  },
222
180
  );
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,61 +4,70 @@
4
4
 
5
5
  import '@dxosTheme';
6
6
 
7
- import React, { type FC, useMemo, useState } from 'react';
7
+ import React, { type FC, 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 } from '@dxos/react-ui';
13
- import { mx, textBlockWidth } from '@dxos/react-ui-theme';
12
+ import { Tooltip, useThemeContext } from '@dxos/react-ui';
13
+ import { 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,
18
19
  decorateMarkdown,
19
20
  comments,
20
- formatting,
21
- image,
21
+ createBasicExtensions,
22
+ createDataExtensions,
23
+ createThemeExtensions,
24
+ formattingKeymap,
22
25
  table,
23
26
  useComments,
24
27
  useFormattingState,
28
+ createMarkdownExtensions,
29
+ image,
25
30
  } from '../../extensions';
26
- import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
31
+ import { useActionHandler, useDocAccessor, useTextEditor } from '../../hooks';
27
32
  import translations from '../../translations';
28
- import { MarkdownEditor } from '../TextEditor';
29
33
 
30
34
  faker.seed(101);
31
35
 
32
- const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) => {
36
+ const Story: FC<{ content: string }> = ({ content }) => {
37
+ const { themeMode } = useThemeContext();
33
38
  const [item] = useState({ text: new TextObject(content) });
34
- const [_comments, setComments] = useState<Comment[]>([]);
35
- const [editorRef, viewInvalidated] = useEditorView(id);
36
- const model = useTextModel({ text: item.text });
39
+ const { id, doc, accessor } = useDocAccessor(item.text);
37
40
  const [formattingState, formattingObserver] = useFormattingState();
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
- [],
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],
54
65
  );
55
66
 
56
- useComments(viewInvalidated ? null : editorRef.current, id, _comments);
57
- const handleAction = useActionHandler(editorRef.current);
67
+ const [_comments, setComments] = useState<Comment[]>([]);
68
+ useComments(view, id, _comments);
58
69
 
59
- if (!model) {
60
- return null;
61
- }
70
+ const handleAction = useActionHandler(view);
62
71
 
63
72
  return (
64
73
  <div role='none' className='fixed inset-0 flex flex-col'>
@@ -67,16 +76,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
67
76
  <Toolbar.Separator />
68
77
  <Toolbar.Extended />
69
78
  </Toolbar.Root>
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
- />
79
+ <div role='textbox' ref={parentRef} className={textBlockWidth} />;
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 } from '@dxos/echo-schema';
12
+ import { createDocAccessor, Filter, DocAccessor } 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 { DocAccessor } from './defs';
21
- import { createBasicExtensions, createThemeExtensions, useTextEditor } from '../../hooks';
20
+ import { useTextEditor } from '../../hooks';
22
21
  import translations from '../../translations';
22
+ import { createBasicExtensions, createThemeExtensions } from '../factories';
23
23
 
24
24
  const initialContent = 'Hello world!';
25
25
 
@@ -34,21 +34,19 @@ type EditorProps = {
34
34
 
35
35
  const Editor = ({ source, autoFocus }: EditorProps) => {
36
36
  const { themeMode } = useThemeContext();
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],
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],
44
48
  );
45
49
 
46
- const { parentRef } = useTextEditor({
47
- doc: DocAccessor.getValue(source),
48
- extensions,
49
- autoFocus,
50
- });
51
-
52
50
  return <div ref={parentRef} />;
53
51
  };
54
52
 
@@ -114,7 +112,8 @@ const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
114
112
 
115
113
  export const Default = {};
116
114
 
117
- // TODO(burdon): Feed already added error.
115
+ // TODO(burdon): Error:
116
+ // chunk-6NX3RPDS.mjs:2021 ControlPipeline#5 Error: invariant violation: Feed already added
118
117
  export const WithEcho = {
119
118
  decorators: [withTheme],
120
119
  render: () => {
@@ -8,9 +8,10 @@ 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';
11
12
 
12
13
  import { cursorConverter } from './cursor';
13
- import { updateHeadsEffect, isReconcile, type State, type DocAccessor } from './defs';
14
+ import { updateHeadsEffect, isReconcile, type State } from './defs';
14
15
  import { Syncer } from './sync';
15
16
  import { Cursor } from '../cursor';
16
17
 
@@ -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';
8
9
  import { log } from '@dxos/log';
9
10
 
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,9 +5,8 @@
5
5
  //
6
6
 
7
7
  import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
8
- import get from 'lodash.get';
9
8
 
10
- import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
9
+ import { type Heads, type Prop } from '@dxos/automerge/automerge';
11
10
 
12
11
  export type State = {
13
12
  path: Prop[];
@@ -31,23 +30,3 @@ export const reconcileAnnotation = Annotation.define<boolean>();
31
30
  export const isReconcile = (tr: Transaction): boolean => {
32
31
  return !!tr.annotation(reconcileAnnotation);
33
32
  };
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,4 +3,3 @@
3
3
  //
4
4
 
5
5
  export { automerge } from './automerge';
6
- export { type IDocHandle } from './defs';
@@ -8,16 +8,9 @@ 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';
11
12
 
12
- import {
13
- getLastHeads,
14
- getPath,
15
- type IDocHandle,
16
- type State,
17
- reconcileAnnotation,
18
- updateHeads,
19
- isReconcile,
20
- } from './defs';
13
+ import { getLastHeads, getPath, type State, reconcileAnnotation, updateHeads, isReconcile } from './defs';
21
14
  import { updateAutomerge } from './update-automerge';
22
15
  import { updateCodeMirror } from './update-codemirror';
23
16
 
@@ -7,8 +7,9 @@
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';
10
11
 
11
- import { type IDocHandle, type State } from './defs';
12
+ import { type State } from './defs';
12
13
 
13
14
  export const updateAutomerge = (
14
15
  field: StateField<State>,
@@ -9,7 +9,7 @@ import { log } from '@dxos/log';
9
9
  import { type Space } from '@dxos/react-client/echo';
10
10
  import { type GossipMessage } from '@dxos/react-client/mesh';
11
11
 
12
- import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from '../extensions';
12
+ import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from './awareness';
13
13
 
14
14
  type ProtocolMessage =
15
15
  | {
@@ -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>;
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './awareness';
6
+ export * from './awareness-provider';
@@ -74,7 +74,7 @@ export const commandState = StateField.define<CommandState>({
74
74
 
75
75
  return state;
76
76
  },
77
- provide: (f) => [showTooltip.from(f, (value) => value.tooltip ?? null)],
77
+ provide: (field) => [showTooltip.from(field, (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 '../hooks';
16
+ import { type Comment, type Range } from './types';
17
17
  import { getToken } from '../styles';
18
18
  import { callbackWrapper } from '../util';
19
19
 
@@ -508,12 +508,16 @@ 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, id: string, comments: Comment[] = []) => {
511
+ export const useComments = (view: EditorView | null | undefined, id: string, comments: Comment[] = []) => {
512
512
  useEffect(() => {
513
513
  if (view) {
514
- view.dispatch({
515
- effects: setComments.of({ id, comments }),
516
- });
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
+ }
517
521
  }
518
522
  }, [id, view, comments]);
519
523
  };