@dxos/react-ui-editor 0.3.11-main.08776d7 → 0.3.11-main.0999ea0
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 +1211 -1005
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +12 -17
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +13 -7
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +14 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +6 -6
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +6 -6
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts +2 -2
- package/dist/types/src/extensions/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/basic.d.ts +4 -5
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +11 -3
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +0 -8
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts +11 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
- package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +8 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts +3 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.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 +2 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
- 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/dist/types/src/styles/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +2 -0
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +45 -36
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +61 -72
- package/src/components/TextEditor/TextEditor.tsx +101 -77
- package/src/components/TextEditor/comments.stories.tsx +16 -21
- package/src/components/Toolbar/Toolbar.stories.tsx +86 -0
- package/src/components/Toolbar/Toolbar.tsx +115 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +53 -13
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +9 -29
- package/src/extensions/automerge/semaphore.ts +24 -28
- package/src/extensions/automerge/update-automerge.ts +14 -8
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +17 -17
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/comments.ts +149 -92
- package/src/extensions/index.ts +0 -8
- package/src/extensions/markdown/bundle.ts +20 -30
- package/src/extensions/markdown/code.ts +142 -0
- package/src/extensions/markdown/formatting.ts +159 -0
- package/src/extensions/markdown/heading.ts +59 -0
- package/src/extensions/markdown/highlight.ts +9 -15
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +5 -2
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
- package/src/extensions/mention.ts +9 -2
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +1 -6
- package/src/hooks/index.ts +2 -2
- package/src/hooks/useActionHandler.ts +45 -0
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +48 -11
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +1 -3
- package/src/util.ts +26 -3
- 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/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/sync.d.ts +0 -7
- package/dist/types/src/extensions/sync.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts +0 -7
- package/dist/types/src/extensions/tasklist.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/code.ts +0 -104
- package/src/extensions/hr.ts +0 -72
- package/src/extensions/sync.ts +0 -15
- 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
- /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
- /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
- /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
- /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
- /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
- /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
- /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
|
@@ -17,7 +17,7 @@ import { fixedInsetFlexLayout, mx } from '@dxos/react-ui-theme';
|
|
|
17
17
|
import { withTheme } from '@dxos/storybook-utils';
|
|
18
18
|
|
|
19
19
|
import { MarkdownEditor, TextEditor } from './TextEditor';
|
|
20
|
-
import { comments, type CommentsOptions,
|
|
20
|
+
import { comments, type CommentsOptions, setFocus, useComments } from '../../extensions';
|
|
21
21
|
import { type Comment, type Range, useTextModel } from '../../hooks';
|
|
22
22
|
|
|
23
23
|
faker.seed(101);
|
|
@@ -44,24 +44,19 @@ const Editor: FC<{
|
|
|
44
44
|
onSelectComment,
|
|
45
45
|
}) => {
|
|
46
46
|
const model = useTextModel({ text: item.text });
|
|
47
|
-
const
|
|
47
|
+
const view = useRef<EditorView>(null);
|
|
48
48
|
const [selected, setSelected] = useState<string>();
|
|
49
49
|
useEffect(() => {
|
|
50
|
-
if (!
|
|
51
|
-
const thread = commentRanges.find((range) => range.id === selectedValue);
|
|
52
|
-
if (thread) {
|
|
53
|
-
const { cursor } = thread;
|
|
54
|
-
const range = cursor && Cursor.getRangeFromCursor(editorRef.current!.state, cursor);
|
|
55
|
-
if (range) {
|
|
56
|
-
// TODO(burdon): Scroll selection to center of screen?
|
|
57
|
-
editorRef.current?.dispatch({ selection: { anchor: range.from }, scrollIntoView: true });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
50
|
+
if (!view.current?.hasFocus && selectedValue !== selected) {
|
|
61
51
|
setSelected(selectedValue);
|
|
52
|
+
if (selectedValue) {
|
|
53
|
+
setFocus(view.current!, selectedValue);
|
|
54
|
+
}
|
|
62
55
|
}
|
|
63
56
|
}, [selected, commentRanges, selectedValue]);
|
|
64
57
|
|
|
58
|
+
useComments(view.current, commentRanges);
|
|
59
|
+
|
|
65
60
|
const extensions = useMemo(() => {
|
|
66
61
|
return [
|
|
67
62
|
comments({
|
|
@@ -80,7 +75,7 @@ const Editor: FC<{
|
|
|
80
75
|
// TODO(burdon): Highlight currently selected comment.
|
|
81
76
|
return (
|
|
82
77
|
<div className='flex grow overflow-y-scroll'>
|
|
83
|
-
<MarkdownEditor ref={
|
|
78
|
+
<MarkdownEditor ref={view} model={model} extensions={extensions} />
|
|
84
79
|
</div>
|
|
85
80
|
);
|
|
86
81
|
};
|
|
@@ -155,13 +150,13 @@ const Thread: FC<{
|
|
|
155
150
|
</div>
|
|
156
151
|
))}
|
|
157
152
|
|
|
158
|
-
<div ref={containerRef} onClick={() => onSelect()}
|
|
153
|
+
<div ref={containerRef} className='flex' onClick={() => onSelect()}>
|
|
159
154
|
<TextEditor
|
|
160
155
|
ref={editorRef}
|
|
161
|
-
|
|
156
|
+
autoFocus={focus}
|
|
162
157
|
model={model}
|
|
163
158
|
placeholder={'Enter comment...'}
|
|
164
|
-
slots={{ root: { className: 'grow
|
|
159
|
+
slots={{ root: { className: 'grow rounded-b' } }}
|
|
165
160
|
extensions={[
|
|
166
161
|
keymap.of([
|
|
167
162
|
{
|
|
@@ -246,7 +241,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
246
241
|
: [],
|
|
247
242
|
},
|
|
248
243
|
]);
|
|
249
|
-
setSelected(id);
|
|
244
|
+
setSelected(id); // TODO(burdon): Not required.
|
|
250
245
|
return id;
|
|
251
246
|
};
|
|
252
247
|
|
|
@@ -278,8 +273,8 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
278
273
|
);
|
|
279
274
|
};
|
|
280
275
|
|
|
281
|
-
const handleSelectComment: CommentsOptions['onSelect'] = ({ comments, selection: {
|
|
282
|
-
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) });
|
|
283
278
|
setThreads((threads) =>
|
|
284
279
|
threads.map((thread) => {
|
|
285
280
|
const comment = comments.find(({ comment }) => comment.id === thread.id);
|
|
@@ -292,7 +287,7 @@ const Story = ({ text, autoCreate }: StoryProps) => {
|
|
|
292
287
|
}),
|
|
293
288
|
);
|
|
294
289
|
|
|
295
|
-
setSelected(
|
|
290
|
+
setSelected(current ?? closest);
|
|
296
291
|
};
|
|
297
292
|
|
|
298
293
|
const handleSelectThread = (id: string) => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import { faker } from '@faker-js/faker';
|
|
8
|
+
import React, { type FC, useMemo, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { TextObject } from '@dxos/echo-schema';
|
|
11
|
+
import { PublicKey } from '@dxos/keys';
|
|
12
|
+
import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
13
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
14
|
+
|
|
15
|
+
import { Toolbar } from './Toolbar';
|
|
16
|
+
import { code, comments, formatting, heading, image, table, tasklist, useComments } from '../../extensions';
|
|
17
|
+
import { type Comment, useActionHandler, useTextEditor, useTextModel } from '../../hooks';
|
|
18
|
+
import { MarkdownEditor } from '../TextEditor';
|
|
19
|
+
|
|
20
|
+
faker.seed(101);
|
|
21
|
+
|
|
22
|
+
const Story: FC<{ content: string }> = ({ content }) => {
|
|
23
|
+
const [item] = useState({ text: new TextObject(content) });
|
|
24
|
+
const [_comments, setComments] = useState<Comment[]>([]);
|
|
25
|
+
const [editorRef, editorView] = useTextEditor();
|
|
26
|
+
const model = useTextModel({ text: item.text });
|
|
27
|
+
const extensions = useMemo(
|
|
28
|
+
() => [
|
|
29
|
+
code(),
|
|
30
|
+
comments({
|
|
31
|
+
onCreate: (cursor) => {
|
|
32
|
+
const id = PublicKey.random().toHex();
|
|
33
|
+
setComments((comments) => [...comments, { id, cursor }]);
|
|
34
|
+
return id;
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
formatting(),
|
|
38
|
+
heading(),
|
|
39
|
+
image(),
|
|
40
|
+
table(),
|
|
41
|
+
tasklist(),
|
|
42
|
+
],
|
|
43
|
+
[],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
useComments(editorView, _comments);
|
|
47
|
+
const handleAction = useActionHandler(editorView);
|
|
48
|
+
|
|
49
|
+
if (!model) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
55
|
+
<div className='flex h-full justify-center'>
|
|
56
|
+
<div className='flex flex-col h-full w-[800px]'>
|
|
57
|
+
<Toolbar onAction={handleAction} />
|
|
58
|
+
<MarkdownEditor ref={editorRef} model={model} extensions={extensions} />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export default {
|
|
66
|
+
title: 'react-ui-editor/Toolbar',
|
|
67
|
+
component: Toolbar,
|
|
68
|
+
render: (args: any) => <Story {...args} />,
|
|
69
|
+
decorators: [withTheme],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const content = [
|
|
73
|
+
'Demo',
|
|
74
|
+
'',
|
|
75
|
+
'The editor supports **Markdown** styles.',
|
|
76
|
+
'',
|
|
77
|
+
faker.lorem.paragraph({ min: 5, max: 8 }),
|
|
78
|
+
'',
|
|
79
|
+
'',
|
|
80
|
+
].join('\n');
|
|
81
|
+
|
|
82
|
+
export const Default = {
|
|
83
|
+
args: {
|
|
84
|
+
content,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type Icon,
|
|
7
|
+
At,
|
|
8
|
+
Brain,
|
|
9
|
+
CaretRight,
|
|
10
|
+
ChatText,
|
|
11
|
+
Code,
|
|
12
|
+
Image,
|
|
13
|
+
Link,
|
|
14
|
+
ListBullets,
|
|
15
|
+
ListChecks,
|
|
16
|
+
ListNumbers,
|
|
17
|
+
TextStrikethrough,
|
|
18
|
+
Table,
|
|
19
|
+
TextB,
|
|
20
|
+
TextItalic,
|
|
21
|
+
} from '@phosphor-icons/react';
|
|
22
|
+
import React, { type FC } from 'react';
|
|
23
|
+
|
|
24
|
+
import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
|
|
25
|
+
import { getSize } from '@dxos/react-ui-theme';
|
|
26
|
+
|
|
27
|
+
export type Action =
|
|
28
|
+
| 'blockquote'
|
|
29
|
+
| 'bold'
|
|
30
|
+
| 'code'
|
|
31
|
+
| 'comment'
|
|
32
|
+
| 'heading'
|
|
33
|
+
| 'image'
|
|
34
|
+
| 'italic'
|
|
35
|
+
| 'link'
|
|
36
|
+
| 'list'
|
|
37
|
+
| 'list-ordered'
|
|
38
|
+
| 'list-tasks'
|
|
39
|
+
| 'mention'
|
|
40
|
+
| 'prompt'
|
|
41
|
+
| 'strikethrough'
|
|
42
|
+
| 'table';
|
|
43
|
+
|
|
44
|
+
export type ToolbarProps = {
|
|
45
|
+
onAction?: (action: { type: Action; data?: any }) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const IconButton: FC<{ Icon: Icon; title: string } & NonNullable<Pick<ButtonProps, 'onClick'>>> = ({
|
|
49
|
+
Icon,
|
|
50
|
+
title,
|
|
51
|
+
onClick,
|
|
52
|
+
}) => (
|
|
53
|
+
<Button variant='ghost' classNames='p-2' onClick={onClick} title={title}>
|
|
54
|
+
<Icon className={getSize(5)} />
|
|
55
|
+
</Button>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// TODO(burdon): Update state based on current selection?
|
|
59
|
+
export const Toolbar = ({ onAction }: ToolbarProps) => {
|
|
60
|
+
return (
|
|
61
|
+
<DensityProvider density='fine'>
|
|
62
|
+
<div role='toolbar' className='flex w-full p-2 items-center gap-4'>
|
|
63
|
+
<div>
|
|
64
|
+
<Select.Root
|
|
65
|
+
defaultValue='0'
|
|
66
|
+
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
67
|
+
>
|
|
68
|
+
<Select.TriggerButton classNames='w-[8rem]' />
|
|
69
|
+
<Select.Portal>
|
|
70
|
+
<Select.Content>
|
|
71
|
+
<Select.Viewport>
|
|
72
|
+
<Select.Option value='0'>Paragraph</Select.Option>
|
|
73
|
+
{[1, 2, 3, 4, 5, 6].map((level) => (
|
|
74
|
+
<Select.Option key={level} value={String(level)}>
|
|
75
|
+
Heading {level}
|
|
76
|
+
</Select.Option>
|
|
77
|
+
))}
|
|
78
|
+
</Select.Viewport>
|
|
79
|
+
</Select.Content>
|
|
80
|
+
</Select.Portal>
|
|
81
|
+
</Select.Root>
|
|
82
|
+
</div>
|
|
83
|
+
<div>
|
|
84
|
+
<IconButton Icon={TextB} title='Bold' onClick={() => onAction?.({ type: 'bold' })} />
|
|
85
|
+
<IconButton Icon={TextItalic} title='Italic' onClick={() => onAction?.({ type: 'italic' })} />
|
|
86
|
+
<IconButton
|
|
87
|
+
Icon={TextStrikethrough}
|
|
88
|
+
title='Strike-through'
|
|
89
|
+
onClick={() => onAction?.({ type: 'strikethrough' })}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
<div>
|
|
93
|
+
<IconButton Icon={ListBullets} title='Bullet list' onClick={() => onAction?.({ type: 'list' })} />
|
|
94
|
+
<IconButton Icon={ListNumbers} title='Numbered list' onClick={() => onAction?.({ type: 'list-ordered' })} />
|
|
95
|
+
<IconButton Icon={ListChecks} title='Task list' onClick={() => onAction?.({ type: 'list-tasks' })} />
|
|
96
|
+
</div>
|
|
97
|
+
<div>
|
|
98
|
+
<IconButton Icon={CaretRight} title='Block quite' onClick={() => onAction?.({ type: 'blockquote' })} />
|
|
99
|
+
<IconButton Icon={Code} title='Code block' onClick={() => onAction?.({ type: 'code' })} />
|
|
100
|
+
<IconButton Icon={Table} title='Table' onClick={() => onAction?.({ type: 'table' })} />
|
|
101
|
+
</div>
|
|
102
|
+
<div>
|
|
103
|
+
<IconButton Icon={Link} title='Link' onClick={() => onAction?.({ type: 'link' })} />
|
|
104
|
+
<IconButton Icon={At} title='Mention' onClick={() => onAction?.({ type: 'mention' })} />
|
|
105
|
+
<IconButton Icon={Image} title='Image' onClick={() => onAction?.({ type: 'image' })} />
|
|
106
|
+
</div>
|
|
107
|
+
<div className='grow' />
|
|
108
|
+
<div>
|
|
109
|
+
<IconButton Icon={Brain} title='AI prompt' onClick={() => onAction?.({ type: 'prompt' })} />
|
|
110
|
+
<IconButton Icon={ChatText} title='Create comment' onClick={() => onAction?.({ type: 'comment' })} />
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</DensityProvider>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
@@ -35,10 +35,13 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
|
35
35
|
// https://codemirror.net/examples/autocompletion
|
|
36
36
|
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
37
37
|
autocompletion({
|
|
38
|
-
|
|
38
|
+
activateOnTyping: true,
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
|
|
40
|
+
// closeOnBlur: false,
|
|
41
|
+
// defaultKeymap: false,
|
|
42
|
+
|
|
43
|
+
// TODO(burdon): Styles/fragments.
|
|
44
|
+
tooltipClass: () => 'shadow rounded',
|
|
42
45
|
}),
|
|
43
46
|
|
|
44
47
|
// TODO(burdon): Option to create new page?
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { EditorState } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
import { render, screen } from '@testing-library/react';
|
|
8
|
+
import chai, { expect } from 'chai';
|
|
9
|
+
import chaiDom from 'chai-dom';
|
|
10
|
+
import get from 'lodash.get';
|
|
11
|
+
import React, { type FC, useEffect, useRef, useState } from 'react';
|
|
12
|
+
import { describe, test } from 'vitest';
|
|
13
|
+
|
|
14
|
+
import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
|
|
15
|
+
|
|
16
|
+
import { automerge } from './automerge';
|
|
17
|
+
|
|
18
|
+
type TestObject = {
|
|
19
|
+
text: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const path = ['text'];
|
|
23
|
+
|
|
24
|
+
class Generator {
|
|
25
|
+
constructor(private readonly _handle: DocHandle<TestObject>) {}
|
|
26
|
+
update(text: string) {
|
|
27
|
+
this._handle.change((doc: TestObject) => {
|
|
28
|
+
doc.text = text;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const Test: FC<{ handle: DocHandle<TestObject>; generator: Generator }> = ({ handle, generator }) => {
|
|
34
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
35
|
+
const [view, setView] = useState<EditorView>();
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const extensions = [
|
|
38
|
+
automerge({ handle, path }),
|
|
39
|
+
EditorView.updateListener.of((update) => {
|
|
40
|
+
if (view.state.doc.toString() === 'hello!') {
|
|
41
|
+
// Update editor.
|
|
42
|
+
update.view.dispatch({ changes: { from: view.state.doc.length - 1, insert: ' world' } });
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const view = new EditorView({
|
|
48
|
+
state: EditorState.create({ doc: get(handle.docSync()!, path), extensions }),
|
|
49
|
+
parent: ref.current!,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
setView(view);
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {}, [view]);
|
|
56
|
+
|
|
57
|
+
return <div ref={ref} />;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
chai.use(chaiDom);
|
|
61
|
+
|
|
62
|
+
describe('Automerge', () => {
|
|
63
|
+
test('basic sync', () => {
|
|
64
|
+
const repo = new Repo({ network: [] });
|
|
65
|
+
const handle = repo.create<TestObject>();
|
|
66
|
+
const generator = new Generator(handle);
|
|
67
|
+
render(<Test handle={handle} generator={generator} />);
|
|
68
|
+
expect(screen.getByRole('textbox')).to.have.text('');
|
|
69
|
+
|
|
70
|
+
generator.update('hello!');
|
|
71
|
+
expect(screen.getByRole('textbox')).to.have.text('hello world!');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// TODO(burdon): Test history/undo.
|
|
75
|
+
// TODO(burdon): https://testing-library.com/docs/react-testing-library/example-intro/
|
|
76
|
+
});
|
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { invertedEffects } from '@codemirror/commands';
|
|
8
|
+
import { StateField, type Extension, type StateEffect } from '@codemirror/state';
|
|
9
|
+
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
8
10
|
|
|
9
|
-
import {
|
|
11
|
+
import { next as A, type Prop } from '@dxos/automerge/automerge';
|
|
10
12
|
|
|
11
13
|
import { cursorConverter } from './cursor';
|
|
12
|
-
import {
|
|
14
|
+
import { updateHeadsEffect, type IDocHandle, isReconcile, type State } from './defs';
|
|
13
15
|
import { PatchSemaphore } from './semaphore';
|
|
14
16
|
import { Cursor } from '../cursor';
|
|
15
|
-
import { syncFacet } from '../sync';
|
|
16
17
|
|
|
17
18
|
export type AutomergeOptions = {
|
|
18
19
|
handle: IDocHandle;
|
|
@@ -20,8 +21,7 @@ export type AutomergeOptions = {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
23
|
-
|
|
24
|
-
const stateField = StateField.define<Value>({
|
|
24
|
+
const syncState = StateField.define<State>({
|
|
25
25
|
create: () => ({
|
|
26
26
|
path: path.slice(),
|
|
27
27
|
lastHeads: A.getHeads(handle.docSync()!),
|
|
@@ -29,7 +29,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
29
29
|
}),
|
|
30
30
|
|
|
31
31
|
update: (value, tr) => {
|
|
32
|
-
const result:
|
|
32
|
+
const result: State = {
|
|
33
33
|
path: path.slice(),
|
|
34
34
|
lastHeads: value.lastHeads,
|
|
35
35
|
unreconciledTransactions: value.unreconciledTransactions.slice(),
|
|
@@ -37,7 +37,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
37
37
|
|
|
38
38
|
let clearUnreconciled = false;
|
|
39
39
|
for (const effect of tr.effects) {
|
|
40
|
-
if (effect.is(
|
|
40
|
+
if (effect.is(updateHeadsEffect)) {
|
|
41
41
|
result.lastHeads = effect.value.newHeads;
|
|
42
42
|
clearUnreconciled = true;
|
|
43
43
|
}
|
|
@@ -46,7 +46,7 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
46
46
|
if (clearUnreconciled) {
|
|
47
47
|
result.unreconciledTransactions = [];
|
|
48
48
|
} else {
|
|
49
|
-
if (!
|
|
49
|
+
if (!isReconcile(tr)) {
|
|
50
50
|
result.unreconciledTransactions.push(tr);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -55,9 +55,49 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
55
55
|
},
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
const semaphore = new PatchSemaphore(handle,
|
|
58
|
+
const semaphore = new PatchSemaphore(handle, syncState);
|
|
59
59
|
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
return [
|
|
61
|
+
Cursor.converter.of(cursorConverter(handle, path)),
|
|
62
|
+
syncState,
|
|
63
|
+
|
|
64
|
+
// Reconcile external updates.
|
|
65
|
+
ViewPlugin.fromClass(
|
|
66
|
+
class {
|
|
67
|
+
constructor(private readonly _view: EditorView) {
|
|
68
|
+
handle.addListener('change', this._handleChange.bind(this));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
destroy() {
|
|
72
|
+
handle.removeListener('change', this._handleChange.bind(this));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
_handleChange() {
|
|
76
|
+
semaphore.reconcile(this._view);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
),
|
|
80
|
+
|
|
81
|
+
// Reconcile local updates.
|
|
82
|
+
EditorView.updateListener.of(({ view, changes }) => {
|
|
83
|
+
if (!changes.empty) {
|
|
84
|
+
semaphore.reconcile(view);
|
|
85
|
+
}
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
// TODO(burdon): Record undo transactions.
|
|
89
|
+
// See https://github.com/yjs/y-codemirror.next/tree/main
|
|
90
|
+
// https://codemirror.net/examples/inverted-effect
|
|
91
|
+
invertedEffects.of((tr) => {
|
|
92
|
+
// Each change results it three transactions: insert, delete, insert.
|
|
93
|
+
const effects: StateEffect<any>[] = [];
|
|
94
|
+
if (!tr.changes.empty) {
|
|
95
|
+
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
|
96
|
+
// effects.push(effectType.of({});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return effects;
|
|
101
|
+
}),
|
|
102
|
+
];
|
|
63
103
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import get from 'lodash.get';
|
|
6
6
|
|
|
7
7
|
import type { Prop } from '@dxos/automerge/automerge';
|
|
8
|
-
import { next as
|
|
8
|
+
import { next as A } from '@dxos/automerge/automerge';
|
|
9
9
|
|
|
10
10
|
import { type IDocHandle } from './defs';
|
|
11
11
|
import { type CursorConverter } from '../cursor';
|
|
@@ -24,7 +24,7 @@ export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConvert
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// NOTE: Slice is needed because getCursor mutates the array.
|
|
27
|
-
return
|
|
27
|
+
return A.getCursor(doc, path.slice(), pos);
|
|
28
28
|
},
|
|
29
29
|
|
|
30
30
|
fromCursor: (cursor) => {
|
|
@@ -47,6 +47,6 @@ export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConvert
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// NOTE: Slice is needed because getCursor mutates the array.
|
|
50
|
-
return
|
|
50
|
+
return A.getCursorPosition(doc, path.slice(), cursor);
|
|
51
51
|
},
|
|
52
52
|
});
|
|
@@ -8,48 +8,28 @@ import { Annotation, StateEffect, type StateField, type EditorState, type Transa
|
|
|
8
8
|
|
|
9
9
|
import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
export type Value = {
|
|
11
|
+
export type State = {
|
|
13
12
|
path: Prop[];
|
|
14
13
|
lastHeads: Heads;
|
|
15
14
|
unreconciledTransactions: Transaction[];
|
|
16
15
|
};
|
|
17
16
|
|
|
17
|
+
export const getPath = (state: EditorState, field: StateField<State>): Prop[] => state.field(field).path;
|
|
18
|
+
export const getLastHeads = (state: EditorState, field: StateField<State>): Heads => state.field(field).lastHeads;
|
|
19
|
+
|
|
18
20
|
export type UpdateHeads = {
|
|
19
21
|
newHeads: Heads;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
|
-
export const
|
|
23
|
-
|
|
24
|
-
export const reconcileAnnotationType = Annotation.define<unknown>();
|
|
25
|
-
|
|
26
|
-
export const getPath = (state: EditorState, field: StateField<Value>): Prop[] => state.field(field).path;
|
|
27
|
-
|
|
28
|
-
export const getLastHeads = (state: EditorState, field: StateField<Value>): Heads => state.field(field).lastHeads;
|
|
29
|
-
|
|
30
|
-
export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => effectType.of({ newHeads });
|
|
24
|
+
export const updateHeadsEffect = StateEffect.define<UpdateHeads>({});
|
|
31
25
|
|
|
32
|
-
export const
|
|
26
|
+
export const updateHeads = (newHeads: Heads): StateEffect<UpdateHeads> => updateHeadsEffect.of({ newHeads });
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
/*
|
|
36
|
-
export const makeReconcile = (tr: TransactionSpec) => {
|
|
37
|
-
if (tr.annotations != null) {
|
|
38
|
-
if (tr.annotations instanceof Array) {
|
|
39
|
-
tr.annotations = [...tr.annotations, reconcileAnnotationType.of({})];
|
|
40
|
-
} else {
|
|
41
|
-
tr.annotations = [tr.annotations, reconcileAnnotationType.of({})];
|
|
42
|
-
}
|
|
43
|
-
} else {
|
|
44
|
-
tr.annotations = [reconcileAnnotationType.of({})];
|
|
45
|
-
}
|
|
28
|
+
export const reconcileAnnotation = Annotation.define<boolean>();
|
|
46
29
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// annotations: reconcileAnnotationType.of({})
|
|
50
|
-
// }
|
|
30
|
+
export const isReconcile = (tr: Transaction): boolean => {
|
|
31
|
+
return !!tr.annotation(reconcileAnnotation);
|
|
51
32
|
};
|
|
52
|
-
*/
|
|
53
33
|
|
|
54
34
|
export type IDocHandle<T = any> = {
|
|
55
35
|
docSync(): Doc<T> | undefined;
|