@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.
- package/dist/lib/browser/index.mjs +108 -283
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts +1 -3
- package/dist/types/src/extensions/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/basic.d.ts +2 -2
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +3 -3
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +0 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +1 -4
- package/dist/types/src/hooks/defs.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts +17 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/package.json +24 -27
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +1 -1
- package/src/components/TextEditor/TextEditor.tsx +11 -14
- package/src/components/TextEditor/comments.stories.tsx +5 -5
- package/src/extensions/awareness.ts +14 -31
- package/src/extensions/basic.ts +3 -3
- package/src/extensions/comments.ts +15 -15
- package/src/extensions/index.ts +0 -1
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +1 -6
- package/src/hooks/index.ts +1 -2
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +46 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -4
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -28
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.d.ts.map +0 -1
- package/src/components/TextEditor/codemirror.stories.ts +0 -115
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/yjs/cursor.ts +0 -22
- package/src/extensions/yjs/index.ts +0 -5
- package/src/extensions/yjs/yjs.test.ts +0 -35
- package/src/extensions/yjs/yjs.ts +0 -16
- package/src/hooks/automerge.ts +0 -53
- package/src/hooks/yjs.ts +0 -157
- package/src/testing/index.ts +0 -6
- package/src/testing/proto/gen/schema.ts +0 -49
- package/src/testing/proto/index.ts +0 -5
- package/src/testing/proto/schema.proto +0 -15
- 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 {
|
|
23
|
+
import { attentionSurface, mx } from '@dxos/react-ui-theme';
|
|
24
24
|
|
|
25
|
-
import { basicBundle, markdownBundle
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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 (
|
|
101
|
+
if (autoFocus) {
|
|
102
102
|
view?.focus();
|
|
103
103
|
}
|
|
104
|
-
}, [view,
|
|
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,
|
|
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,
|
|
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',
|
|
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
|
-
|
|
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: {
|
|
277
|
-
log.info('select', { active:
|
|
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(
|
|
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: '.
|
|
291
|
-
height: '.
|
|
292
|
-
top: '-.
|
|
293
|
-
left: '-.
|
|
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
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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',
|
package/src/extensions/basic.ts
CHANGED
|
@@ -15,14 +15,14 @@ import { type TextEditorProps } from '../components';
|
|
|
15
15
|
|
|
16
16
|
export type BasicBundleOptions = {
|
|
17
17
|
themeMode?: ThemeMode;
|
|
18
|
-
} & Pick<TextEditorProps, 'placeholder' | '
|
|
18
|
+
} & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
|
|
19
19
|
|
|
20
20
|
export const basicBundle = ({
|
|
21
21
|
themeMode,
|
|
22
22
|
placeholder: _placeholder,
|
|
23
|
-
|
|
23
|
+
lineWrapping = true,
|
|
24
24
|
}: BasicBundleOptions): Extension[] => [
|
|
25
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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({
|
|
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: {
|
|
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 ===
|
|
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({
|
|
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
|
|
374
|
+
EditorView.updateListener.of(({ view, state }) => {
|
|
375
375
|
let min = Infinity;
|
|
376
376
|
const {
|
|
377
|
-
selection: {
|
|
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.
|
|
385
|
+
selection.current = comment.id;
|
|
386
386
|
selection.closest = undefined;
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
if (!selection.
|
|
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.
|
|
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
|
|
420
|
+
export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
|
|
421
421
|
useEffect(() => {
|
|
422
|
-
if (view
|
|
422
|
+
if (view) {
|
|
423
423
|
view.dispatch({
|
|
424
424
|
effects: setComments.of(comments),
|
|
425
425
|
});
|
package/src/extensions/index.ts
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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 |
|
|
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?: {
|
package/src/hooks/index.ts
CHANGED
|
@@ -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 {
|
|
11
|
-
import { type EditorModel } from './defs';
|
|
12
|
-
import {
|
|
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 = (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 +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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/yjs/index.ts"],"names":[],"mappings":"AAIA,cAAc,OAAO,CAAC"}
|
|
@@ -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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"yjs.test.d.ts","sourceRoot":"","sources":["../../../../../src/extensions/yjs/yjs.test.ts"],"names":[],"mappings":""}
|
|
@@ -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"}
|