@dxos/react-ui-editor 0.3.11-main.55b2292 → 0.3.11-main.56ff0ac

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 (72) hide show
  1. package/dist/lib/browser/index.mjs +108 -283
  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 +2 -2
  5. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  6. package/dist/types/src/extensions/awareness.d.ts +1 -3
  7. package/dist/types/src/extensions/awareness.d.ts.map +1 -1
  8. package/dist/types/src/extensions/basic.d.ts +2 -2
  9. package/dist/types/src/extensions/basic.d.ts.map +1 -1
  10. package/dist/types/src/extensions/comments.d.ts +3 -3
  11. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  12. package/dist/types/src/extensions/index.d.ts +0 -1
  13. package/dist/types/src/extensions/index.d.ts.map +1 -1
  14. package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
  15. package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
  16. package/dist/types/src/hooks/defs.d.ts +1 -4
  17. package/dist/types/src/hooks/defs.d.ts.map +1 -1
  18. package/dist/types/src/hooks/index.d.ts +1 -2
  19. package/dist/types/src/hooks/index.d.ts.map +1 -1
  20. package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
  21. package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
  22. package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
  23. package/package.json +24 -27
  24. package/src/components/TextEditor/MarkdownEditor.stories.tsx +1 -1
  25. package/src/components/TextEditor/TextEditor.tsx +11 -14
  26. package/src/components/TextEditor/comments.stories.tsx +5 -5
  27. package/src/extensions/awareness.ts +14 -31
  28. package/src/extensions/basic.ts +3 -3
  29. package/src/extensions/comments.ts +15 -15
  30. package/src/extensions/index.ts +0 -1
  31. package/src/hooks/awareness-provider.ts +24 -20
  32. package/src/hooks/defs.ts +1 -6
  33. package/src/hooks/index.ts +1 -2
  34. package/src/hooks/useTextEditor.ts +29 -0
  35. package/src/hooks/useTextModel.ts +46 -12
  36. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
  37. package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
  38. package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
  39. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  40. package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
  41. package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
  42. package/dist/types/src/extensions/yjs/index.d.ts +0 -2
  43. package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
  44. package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
  45. package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
  46. package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
  47. package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
  48. package/dist/types/src/hooks/automerge.d.ts +0 -4
  49. package/dist/types/src/hooks/automerge.d.ts.map +0 -1
  50. package/dist/types/src/hooks/yjs.d.ts +0 -28
  51. package/dist/types/src/hooks/yjs.d.ts.map +0 -1
  52. package/dist/types/src/testing/index.d.ts +0 -3
  53. package/dist/types/src/testing/index.d.ts.map +0 -1
  54. package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
  55. package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
  56. package/dist/types/src/testing/proto/index.d.ts +0 -2
  57. package/dist/types/src/testing/proto/index.d.ts.map +0 -1
  58. package/dist/types/src/testing/replicator.d.ts +0 -45
  59. package/dist/types/src/testing/replicator.d.ts.map +0 -1
  60. package/src/components/TextEditor/codemirror.stories.ts +0 -115
  61. package/src/components/TextEditor/yjs.stories.tsx +0 -56
  62. package/src/extensions/yjs/cursor.ts +0 -22
  63. package/src/extensions/yjs/index.ts +0 -5
  64. package/src/extensions/yjs/yjs.test.ts +0 -35
  65. package/src/extensions/yjs/yjs.ts +0 -16
  66. package/src/hooks/automerge.ts +0 -53
  67. package/src/hooks/yjs.ts +0 -157
  68. package/src/testing/index.ts +0 -6
  69. package/src/testing/proto/gen/schema.ts +0 -49
  70. package/src/testing/proto/index.ts +0 -5
  71. package/src/testing/proto/schema.proto +0 -15
  72. package/src/testing/replicator.ts +0 -184
@@ -20,9 +20,9 @@ import React, {
20
20
 
21
21
  import { log } from '@dxos/log';
22
22
  import { useThemeContext } from '@dxos/react-ui';
23
- import { inputSurface, mx } from '@dxos/react-ui-theme';
23
+ import { attentionSurface, mx } from '@dxos/react-ui-theme';
24
24
 
25
- import { basicBundle, markdownBundle, useAwareness } from '../../extensions';
25
+ import { basicBundle, markdownBundle } from '../../extensions';
26
26
  import { type EditorModel } from '../../hooks';
27
27
  import { type ThemeStyles } from '../../styles';
28
28
  import { defaultTheme, markdownTheme, textTheme } from '../../themes';
@@ -53,10 +53,10 @@ export type TextEditorSlots = {
53
53
 
54
54
  // TODO(burdon): Spellcheck?
55
55
  export type TextEditorProps = {
56
- model: EditorModel;
56
+ model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
57
57
  readonly?: boolean; // TODO(burdon): Move into model.
58
- autofocus?: boolean;
59
- multiline?: boolean;
58
+ autoFocus?: boolean;
59
+ lineWrapping?: boolean;
60
60
  scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
61
61
  selection?: { anchor: number; head?: number };
62
62
  editorMode?: EditorMode; // TODO(burdon): Factor out.
@@ -75,7 +75,7 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
75
75
  {
76
76
  model,
77
77
  readonly,
78
- autofocus,
78
+ autoFocus,
79
79
  scrollTo,
80
80
  selection,
81
81
  editorMode,
@@ -98,13 +98,10 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
98
98
 
99
99
  // Set focus.
100
100
  useEffect(() => {
101
- if (autofocus) {
101
+ if (autoFocus) {
102
102
  view?.focus();
103
103
  }
104
- }, [view, autofocus]);
105
-
106
- // Monitor awareness.
107
- useAwareness(model);
104
+ }, [view, autoFocus]);
108
105
 
109
106
  // Create editor state and view.
110
107
  // The view is recreated if the model or extensions are changed.
@@ -214,14 +211,14 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
214
211
 
215
212
  // TODO(burdon): Single-line/scroll.
216
213
  export const TextEditor = forwardRef<EditorView, TextEditorProps>(
217
- ({ readonly, placeholder, multiline, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
214
+ ({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
218
215
  const { themeMode } = useThemeContext();
219
216
  const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
220
217
  return (
221
218
  <BaseTextEditor
222
219
  ref={forwardedRef}
223
220
  readonly={readonly}
224
- extensions={[basicBundle({ themeMode, placeholder, multiline }), ...extensions]}
221
+ extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
225
222
  theme={theme}
226
223
  slots={updatedSlots}
227
224
  {...props}
@@ -250,7 +247,7 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
250
247
  export const defaultSlots: TextEditorSlots = {
251
248
  root: {
252
249
  // TODO(burdon): Add focusRing by default/as property?
253
- className: mx('flex flex-col grow overflow-y-auto', inputSurface),
250
+ className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
254
251
  },
255
252
  editor: {
256
253
  className: 'h-full p-2',
@@ -153,7 +153,7 @@ const Thread: FC<{
153
153
  <div ref={containerRef} onClick={() => onSelect()} className='flex'>
154
154
  <TextEditor
155
155
  ref={editorRef}
156
- autofocus={focus}
156
+ autoFocus={focus}
157
157
  model={model}
158
158
  placeholder={'Enter comment...'}
159
159
  slots={{ root: { className: 'grow rounded-b' } }}
@@ -241,7 +241,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
241
241
  : [],
242
242
  },
243
243
  ]);
244
- setSelected(id);
244
+ setSelected(id); // TODO(burdon): Not required.
245
245
  return id;
246
246
  };
247
247
 
@@ -273,8 +273,8 @@ const Story = ({ text, autoCreate }: StoryProps) => {
273
273
  );
274
274
  };
275
275
 
276
- const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { active, closest } }) => {
277
- log.info('select', { active: active?.slice(0, 4), closest: closest?.slice(0, 4) });
276
+ const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: { current, closest } }) => {
277
+ log.info('select', { active: current?.slice(0, 4), closest: closest?.slice(0, 4) });
278
278
  setThreads((threads) =>
279
279
  threads.map((thread) => {
280
280
  const comment = comments.find(({ comment }) => comment.id === thread.id);
@@ -287,7 +287,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
287
287
  }),
288
288
  );
289
289
 
290
- setSelected(active ?? closest);
290
+ setSelected(current ?? closest);
291
291
  };
292
292
 
293
293
  const handleSelectThread = (id: string) => {
@@ -12,16 +12,11 @@ import {
12
12
  type ViewUpdate,
13
13
  WidgetType,
14
14
  } from '@codemirror/view';
15
- import { useEffect } from 'react';
16
15
 
17
16
  import { Event } from '@dxos/async';
18
17
  import { Context } from '@dxos/context';
19
- import { generateName } from '@dxos/display-name';
20
- import { useThemeContext } from '@dxos/react-ui';
21
- import { getColorForValue } from '@dxos/react-ui-theme';
22
18
 
23
19
  import { Cursor, type CursorConverter } from './cursor';
24
- import { type EditorModel } from '../hooks';
25
20
 
26
21
  export interface AwarenessProvider {
27
22
  remoteStateChange: Event<void>;
@@ -29,8 +24,8 @@ export interface AwarenessProvider {
29
24
  open(): void;
30
25
  close(): void;
31
26
 
32
- update(position: AwarenessPosition | undefined): void;
33
27
  getRemoteStates(): AwarenessState[];
28
+ update(position: AwarenessPosition | undefined): void;
34
29
  }
35
30
 
36
31
  const dummyProvider: AwarenessProvider = {
@@ -39,8 +34,8 @@ const dummyProvider: AwarenessProvider = {
39
34
  open: () => {},
40
35
  close: () => {},
41
36
 
42
- update: () => {},
43
37
  getRemoteStates: () => [],
38
+ update: () => {},
44
39
  };
45
40
 
46
41
  export const AwarenessProvider = Facet.define<AwarenessProvider, AwarenessProvider>({
@@ -81,19 +76,6 @@ export const awareness = (provider = dummyProvider): Extension => {
81
76
  ];
82
77
  };
83
78
 
84
- export const useAwareness = ({ awareness, peer }: EditorModel) => {
85
- const { themeMode } = useThemeContext();
86
- useEffect(() => {
87
- if (awareness && peer) {
88
- awareness.setLocalStateField('user', {
89
- name: peer.name ?? generateName(peer.id),
90
- color: getColorForValue({ value: peer.id, type: 'color' }),
91
- colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
92
- });
93
- }
94
- }, [awareness, peer, themeMode]);
95
- };
96
-
97
79
  /**
98
80
  * Generates selection decorations from remote peers.
99
81
  */
@@ -268,7 +250,6 @@ class RemoteCaretWidget extends WidgetType {
268
250
  }
269
251
  }
270
252
 
271
- // TODO(burdon): Mixed case?
272
253
  const styles = EditorView.baseTheme({
273
254
  '.cm-collab-selection': {},
274
255
  '.cm-collab-selectionLine': {
@@ -283,38 +264,40 @@ const styles = EditorView.baseTheme({
283
264
  marginRight: '-1px',
284
265
  boxSizing: 'border-box',
285
266
  display: 'inline',
267
+ cursor: 'pointer',
286
268
  },
287
269
  '.cm-collab-selectionCaretDot': {
288
270
  borderRadius: '50%',
289
271
  position: 'absolute',
290
- width: '.4em',
291
- height: '.4em',
292
- top: '-.2em',
293
- left: '-.2em',
272
+ width: '.5em',
273
+ height: '.5em',
274
+ top: '-.25em',
275
+ left: '-.25em',
294
276
  backgroundColor: 'inherit',
295
277
  transition: 'transform .3s ease-in-out',
296
278
  boxSizing: 'border-box',
297
279
  },
298
280
  '.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
299
- transformOrigin: 'bottom center',
300
281
  transform: 'scale(0)',
282
+ transformOrigin: 'center',
301
283
  },
302
284
  '.cm-collab-selectionInfo': {
303
285
  position: 'absolute',
304
- top: '-1.05em',
305
- left: '-1px',
286
+ transform: 'translate(-50%, 0)',
287
+ top: '-20px',
288
+ left: 0,
306
289
  fontSize: '.75em',
307
- fontFamily: 'serif',
290
+ fontFamily: 'sans-serif',
308
291
  fontStyle: 'normal',
309
292
  fontWeight: 'normal',
310
293
  lineHeight: 'normal',
311
294
  userSelect: 'none',
312
295
  color: 'white',
313
- paddingLeft: '2px',
314
- paddingRight: '2px',
296
+ padding: '2px',
315
297
  zIndex: 101,
316
298
  transition: 'opacity .3s ease-in-out',
317
299
  backgroundColor: 'inherit',
300
+ borderRadius: '2px',
318
301
  // These should be separate.
319
302
  opacity: 0,
320
303
  transitionDelay: '0s',
@@ -15,14 +15,14 @@ import { type TextEditorProps } from '../components';
15
15
 
16
16
  export type BasicBundleOptions = {
17
17
  themeMode?: ThemeMode;
18
- } & Pick<TextEditorProps, 'placeholder' | 'multiline'>;
18
+ } & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
19
19
 
20
20
  export const basicBundle = ({
21
21
  themeMode,
22
22
  placeholder: _placeholder,
23
- multiline = true,
23
+ lineWrapping = true,
24
24
  }: BasicBundleOptions): Extension[] => [
25
- multiline ? EditorView.lineWrapping : [],
25
+ lineWrapping ? EditorView.lineWrapping : [],
26
26
 
27
27
  // https://codemirror.net/docs/ref/#codemirror.minimalSetup
28
28
  bracketMatching(),
@@ -21,14 +21,14 @@ const styles = EditorView.baseTheme({
21
21
  '& .cm-comment': {
22
22
  backgroundColor: getToken('extend.colors.yellow.50'),
23
23
  },
24
- '& .cm-comment-active': {
24
+ '& .cm-comment-current': {
25
25
  backgroundColor: getToken('extend.colors.yellow.100'),
26
26
  },
27
27
  });
28
28
 
29
29
  const marks = {
30
30
  highlight: Decoration.mark({ class: 'cm-comment' }),
31
- highlightActive: Decoration.mark({ class: 'cm-comment-active' }),
31
+ highlightActive: Decoration.mark({ class: 'cm-comment-current' }),
32
32
  };
33
33
 
34
34
  type CommentState = {
@@ -38,7 +38,7 @@ type CommentState = {
38
38
  };
39
39
 
40
40
  type SelectionState = {
41
- active?: string;
41
+ current?: string;
42
42
  closest?: string;
43
43
  };
44
44
 
@@ -60,7 +60,7 @@ export const setFocus = (view: EditorView, id: string, center = true) => {
60
60
  effects: [
61
61
  //
62
62
  EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
63
- setSelection.of({ active: id }),
63
+ setSelection.of({ current: id }),
64
64
  ],
65
65
  });
66
66
  }
@@ -117,7 +117,7 @@ const commentsState = StateField.define<CommentsState>({
117
117
  */
118
118
  const highlightDecorations = EditorView.decorations.compute([commentsState], (state) => {
119
119
  const {
120
- selection: { active },
120
+ selection: { current },
121
121
  comments,
122
122
  } = state.field(commentsState);
123
123
 
@@ -129,7 +129,7 @@ const highlightDecorations = EditorView.decorations.compute([commentsState], (st
129
129
  return undefined;
130
130
  }
131
131
 
132
- if (comment.comment.id === active) {
132
+ if (comment.comment.id === current) {
133
133
  return marks.highlightActive.range(range.from, range.to);
134
134
  } else {
135
135
  return marks.highlight.range(range.from, range.to);
@@ -279,7 +279,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
279
279
  if (id) {
280
280
  // Update range.
281
281
  view.dispatch({
282
- effects: setSelection.of({ active: id }),
282
+ effects: setSelection.of({ current: id }),
283
283
  selection: { anchor: from },
284
284
  });
285
285
 
@@ -371,10 +371,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
371
371
  //
372
372
  // Track selection/proximity.
373
373
  //
374
- EditorView.updateListener.of(({ view, state, changes }) => {
374
+ EditorView.updateListener.of(({ view, state }) => {
375
375
  let min = Infinity;
376
376
  const {
377
- selection: { active, closest },
377
+ selection: { current, closest },
378
378
  comments,
379
379
  } = state.field(commentsState);
380
380
 
@@ -382,11 +382,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
382
382
  const selection: SelectionState = {};
383
383
  comments.forEach(({ comment, range }) => {
384
384
  if (head >= range.from && head <= range.to) {
385
- selection.active = comment.id;
385
+ selection.current = comment.id;
386
386
  selection.closest = undefined;
387
387
  }
388
388
 
389
- if (!selection.active) {
389
+ if (!selection.current) {
390
390
  const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
391
391
  if (d < min) {
392
392
  selection.closest = comment.id;
@@ -395,7 +395,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
395
395
  }
396
396
  });
397
397
 
398
- if (selection.active !== active || selection.closest !== closest) {
398
+ if (selection.current !== current || selection.closest !== closest) {
399
399
  view.dispatch({ effects: setSelection.of(selection) });
400
400
 
401
401
  // Update callback.
@@ -415,11 +415,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
415
415
  };
416
416
 
417
417
  /**
418
- * Update state field.
418
+ * Update comments state field.
419
419
  */
420
- export const useComments = (view?: EditorView | null, comments?: Comment[]) => {
420
+ export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
421
421
  useEffect(() => {
422
- if (view && comments) {
422
+ if (view) {
423
423
  view.dispatch({
424
424
  effects: setComments.of(comments),
425
425
  });
@@ -20,4 +20,3 @@ export * from './outliner';
20
20
  export * from './table';
21
21
  export * from './tasklist';
22
22
  export * from './typewriter';
23
- export * from './yjs';
@@ -29,9 +29,10 @@ export type AwarenessProviderParams = {
29
29
  info: AwarenessInfo;
30
30
  };
31
31
 
32
+ /**
33
+ * Receives and broadcasts profile and cursor position.
34
+ */
32
35
  export class SpaceAwarenessProvider implements AwarenessProvider {
33
- public readonly remoteStateChange = new Event<void>();
34
-
35
36
  private readonly _remoteStates = new Map<string, AwarenessState>();
36
37
 
37
38
  private readonly _space: Space;
@@ -39,9 +40,11 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
39
40
  private readonly _peerId: string;
40
41
  private readonly _info: AwarenessInfo;
41
42
 
42
- private _localState?: AwarenessState;
43
- private _postTask?: DeferredTask;
44
43
  private _ctx?: Context;
44
+ private _postTask?: DeferredTask;
45
+ private _localState?: AwarenessState;
46
+
47
+ public readonly remoteStateChange = new Event<void>();
45
48
 
46
49
  constructor(params: AwarenessProviderParams) {
47
50
  this._space = params.space;
@@ -63,19 +66,20 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
63
66
  }
64
67
  });
65
68
 
66
- const unsubscribe = this._space.listen(this._channel, (message: GossipMessage) => {
67
- switch (message.payload.kind) {
68
- case 'query': {
69
- this._handleQueryMessage();
70
- break;
69
+ this._ctx.onDispose(
70
+ this._space.listen(this._channel, (message: GossipMessage) => {
71
+ switch (message.payload.kind) {
72
+ case 'query': {
73
+ this._handleQueryMessage();
74
+ break;
75
+ }
76
+ case 'post': {
77
+ this._handlePostMessage(message.payload);
78
+ break;
79
+ }
71
80
  }
72
- case 'post': {
73
- this._handlePostMessage(message.payload);
74
- break;
75
- }
76
- }
77
- });
78
- this._ctx.onDispose(unsubscribe);
81
+ }),
82
+ );
79
83
 
80
84
  void this._space
81
85
  .postMessage(this._channel, {
@@ -92,6 +96,10 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
92
96
  this._postTask = undefined;
93
97
  }
94
98
 
99
+ getRemoteStates(): AwarenessState[] {
100
+ return Array.from(this._remoteStates.values());
101
+ }
102
+
95
103
  update(position: AwarenessPosition | undefined): void {
96
104
  invariant(this._postTask);
97
105
  this._localState = {
@@ -103,10 +111,6 @@ export class SpaceAwarenessProvider implements AwarenessProvider {
103
111
  this._postTask.schedule();
104
112
  }
105
113
 
106
- getRemoteStates(): AwarenessState[] {
107
- return Array.from(this._remoteStates.values());
108
- }
109
-
110
114
  private _handleQueryMessage() {
111
115
  invariant(this._postTask);
112
116
  this._postTask.schedule();
package/src/hooks/defs.ts CHANGED
@@ -3,10 +3,8 @@
3
3
  //
4
4
 
5
5
  import { type Extension, StateField } from '@codemirror/state';
6
- import type * as YP from 'y-protocols/awareness';
7
6
 
8
7
  import { type DocAccessor } from '@dxos/echo-schema';
9
- import type { YText, YXmlFragment } from '@dxos/text-model';
10
8
 
11
9
  // Runtime data structure.
12
10
  export type Range = {
@@ -26,10 +24,7 @@ export type EditorModel = {
26
24
  extension?: Extension;
27
25
 
28
26
  // TODO(burdon): Remove.
29
- content: string | YText | YXmlFragment | DocAccessor;
30
-
31
- // TODO(burdon): Move into extension.
32
- awareness?: YP.Awareness;
27
+ content: string | DocAccessor;
33
28
 
34
29
  // TODO(burdon): Remove.
35
30
  peer?: {
@@ -2,9 +2,8 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './automerge';
6
5
  export * from './awareness-provider';
7
6
  export * from './defs';
8
- export * from './yjs';
9
7
 
8
+ export * from './useTextEditor';
10
9
  export * from './useTextModel';
@@ -0,0 +1,29 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type EditorView } from '@codemirror/view';
6
+ import { type RefCallback, useState } from 'react';
7
+
8
+ /**
9
+ * Hook for accessing the editor view.
10
+ *
11
+ * ```tsx
12
+ * const Test = () => {
13
+ * const [editorRef, editorView] = useTextEditor();
14
+ * useEffect(() => {
15
+ * editorView?.focus();
16
+ * }, [editorView]);
17
+ * return <TextEditor ref={editorRef} />;
18
+ * };
19
+ * ```
20
+ */
21
+ export const useTextEditor = (): [RefCallback<EditorView | null>, EditorView | null] => {
22
+ const [view, setView] = useState<EditorView | null>(null);
23
+ return [
24
+ (ref: EditorView | null) => {
25
+ setView(ref);
26
+ },
27
+ view,
28
+ ];
29
+ };
@@ -2,14 +2,20 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import get from 'lodash.get';
5
6
  import { useEffect, useState } from 'react';
6
7
 
8
+ import { generateName } from '@dxos/display-name';
9
+ import { type AutomergeTextCompat, getRawDoc } from '@dxos/echo-schema';
10
+ import { invariant } from '@dxos/invariant';
7
11
  import { isAutomergeObject, type Space, type TextObject } from '@dxos/react-client/echo';
8
12
  import { type Identity } from '@dxos/react-client/halo';
13
+ import { isNotNullOrUndefined } from '@dxos/util';
9
14
 
10
- import { createAutomergeModel } from './automerge';
11
- import { type EditorModel } from './defs';
12
- import { createYjsModel } from './yjs';
15
+ import { SpaceAwarenessProvider } from './awareness-provider';
16
+ import { type EditorModel, modelState } from './defs';
17
+ import { automerge, awareness, AwarenessProvider } from '../extensions';
18
+ import { cursorColor } from '../styles';
13
19
 
14
20
  // TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
15
21
  export type UseTextModelProps = {
@@ -25,13 +31,41 @@ export const useTextModel = (props: UseTextModelProps): EditorModel | undefined
25
31
  return model;
26
32
  };
27
33
 
28
- const createModel = (props: UseTextModelProps) => {
29
- const { text } = props;
30
- if (isAutomergeObject(text)) {
31
- return createAutomergeModel(props);
32
- } else if (text?.doc) {
33
- return createYjsModel(props);
34
- } else {
35
- return undefined;
36
- }
34
+ const createModel = ({ space, identity, text }: UseTextModelProps) => {
35
+ invariant(isAutomergeObject(text));
36
+ const obj = text as any as AutomergeTextCompat;
37
+ const doc = getRawDoc(obj, [obj.field]);
38
+
39
+ const awarenessProvider =
40
+ space &&
41
+ new SpaceAwarenessProvider({
42
+ space,
43
+ channel: `automerge.awareness.${obj.id}`,
44
+ info: {
45
+ displayName: identity ? identity.profile?.displayName ?? generateName(identity.identityKey.toHex()) : undefined,
46
+ color: cursorColor.color,
47
+ lightColor: cursorColor.light,
48
+ },
49
+ peerId: identity?.identityKey.toHex() ?? 'Anonymous',
50
+ });
51
+
52
+ const model: EditorModel = {
53
+ id: obj.id,
54
+ content: doc,
55
+ text: () => get(doc.handle.docSync(), doc.path),
56
+ extension: [
57
+ modelState.init(() => model),
58
+ automerge({ handle: doc.handle, path: doc.path }),
59
+ awarenessProvider && AwarenessProvider.of(awarenessProvider),
60
+ awareness(),
61
+ ].filter(isNotNullOrUndefined),
62
+ peer: identity
63
+ ? {
64
+ id: identity.identityKey.toHex(),
65
+ name: identity.profile?.displayName,
66
+ }
67
+ : undefined,
68
+ };
69
+
70
+ return model;
37
71
  };
@@ -1,12 +0,0 @@
1
- import { type StoryObj } from '@storybook/html';
2
- import { Client, type PublicKey } from '@dxos/react-client';
3
- declare const _default: {
4
- title: string;
5
- };
6
- export default _default;
7
- export declare const Default: StoryObj<{
8
- id: number;
9
- client: Client;
10
- spaceKey: PublicKey;
11
- }>;
12
- //# sourceMappingURL=codemirror.stories.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"codemirror.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/codemirror.stories.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAMhD,OAAO,EAAE,MAAM,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;;;;AAU5D,wBAEE;AAqEF,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,CAoBjF,CAAC"}
@@ -1,12 +0,0 @@
1
- import '@dxosTheme';
2
- import React from 'react';
3
- declare const _default: {
4
- title: string;
5
- component: React.ForwardRefExoticComponent<import("./TextEditor").TextEditorProps & React.RefAttributes<import("@codemirror/view").EditorView>>;
6
- decorators: import("@storybook/react").Decorator[];
7
- };
8
- export default _default;
9
- export declare const Default: {
10
- render: () => JSX.Element;
11
- };
12
- //# sourceMappingURL=yjs.stories.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"yjs.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/TextEditor/yjs.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;AAEpB,OAAO,KAAmB,MAAM,OAAO,CAAC;;;;;;AAWxC,wBAIE;AA+BF,eAAO,MAAM,OAAO;;CAGnB,CAAC"}
@@ -1,4 +0,0 @@
1
- import * as Y from 'yjs';
2
- import { type CursorConverter } from '../cursor';
3
- export declare const cursorConverter: (text: Y.Text) => CursorConverter;
4
- //# sourceMappingURL=cursor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/yjs/cursor.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAIzB,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,eAAO,MAAM,eAAe,SAAU,MAAM,KAAG,eAW7C,CAAC"}
@@ -1,2 +0,0 @@
1
- export * from './yjs';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/yjs/index.ts"],"names":[],"mappings":"AAIA,cAAc,OAAO,CAAC"}
@@ -1,4 +0,0 @@
1
- import type * as YP from 'y-protocols/awareness';
2
- import type { YText } from '@dxos/text-model';
3
- export declare const yjs: (content: YText, awareness?: YP.Awareness) => import("@codemirror/state").Extension[];
4
- //# sourceMappingURL=yjs.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"yjs.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/yjs/yjs.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAEjD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAK9C,eAAO,MAAM,GAAG,YAAa,KAAK,cAAc,GAAG,SAAS,4CAG3D,CAAC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=yjs.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"yjs.test.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/yjs/yjs.test.ts"],"names":[],"mappings":""}
@@ -1,4 +0,0 @@
1
- import { type EditorModel } from './defs';
2
- import { type UseTextModelProps } from './useTextModel';
3
- export declare const createAutomergeModel: ({ space, identity, text }: UseTextModelProps) => EditorModel;
4
- //# sourceMappingURL=automerge.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"automerge.d.ts","sourceRoot":"","sources":["../../../../src/hooks/automerge.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,WAAW,EAAc,MAAM,QAAQ,CAAC;AACtD,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAIxD,eAAO,MAAM,oBAAoB,8BAA+B,iBAAiB,KAAG,WAqCnF,CAAC"}