@dxos/react-ui-editor 0.3.9-main.501fe30 → 0.3.9-main.52d137d
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 +88 -183
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts +2 -2
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +2 -2
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-plugin/index.d.ts +2 -2
- package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts +5 -6
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge.stories.d.ts +0 -1
- package/dist/types/src/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/automerge/demo.d.ts +25 -0
- package/dist/types/src/automerge/demo.d.ts.map +1 -0
- package/dist/types/src/components/Editor/Editor.d.ts +1 -0
- package/dist/types/src/components/Editor/Editor.d.ts.map +1 -1
- package/dist/types/src/components/Editor/Editor.stories.d.ts +8 -2
- package/dist/types/src/components/Editor/Editor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Markdown/Markdown.d.ts +1 -2
- package/dist/types/src/components/Markdown/Markdown.d.ts.map +1 -1
- package/dist/types/src/components/Markdown/Markdown.stories.d.ts +1 -0
- package/dist/types/src/components/Markdown/Markdown.stories.d.ts.map +1 -1
- package/dist/types/src/components/Markdown/markdownTheme.d.ts +1 -8
- package/dist/types/src/components/Markdown/markdownTheme.d.ts.map +1 -1
- package/dist/types/src/components/RichText/RichText.stories.d.ts +3 -1
- package/dist/types/src/components/RichText/RichText.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +15 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/{hyperlinkWidget.d.ts → hyperlink.d.ts} +3 -3
- package/dist/types/src/components/TextEditor/extensions/hyperlink.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/hyperlinkTooltip.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +1 -2
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/theme.d.ts +0 -1
- package/dist/types/src/components/TextEditor/theme.d.ts.map +1 -1
- package/dist/types/stories/html/code-mirror.stories.d.ts.map +1 -1
- package/package.json +27 -26
- package/src/automerge/automerge-plugin/PatchSemaphore.ts +5 -5
- package/src/automerge/automerge-plugin/codeMirrorToAm.ts +7 -10
- package/src/automerge/automerge-plugin/index.ts +2 -2
- package/src/automerge/automerge-plugin/plugin.ts +10 -39
- package/src/automerge/automerge.stories.tsx +46 -31
- package/src/automerge/demo.ts +106 -0
- package/src/components/Editor/Editor.stories.tsx +5 -3
- package/src/components/Editor/Editor.tsx +1 -4
- package/src/components/Markdown/Markdown.stories.tsx +25 -36
- package/src/components/Markdown/Markdown.tsx +22 -47
- package/src/components/Markdown/markdownTheme.ts +1 -12
- package/src/components/RichText/RichText.stories.tsx +33 -35
- package/src/components/TextEditor/TextEditor.stories.tsx +64 -39
- package/src/components/TextEditor/TextEditor.tsx +0 -1
- package/src/components/TextEditor/extensions/{hyperlinkWidget.tsx → hyperlink.tsx} +10 -21
- package/src/components/TextEditor/extensions/hyperlinkTooltip.tsx +0 -1
- package/src/components/TextEditor/extensions/index.ts +1 -2
- package/src/components/TextEditor/theme.ts +4 -6
- package/dist/types/src/automerge/automerge-plugin/handle.d.ts +0 -9
- package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlinkDecoration.d.ts +0 -11
- package/dist/types/src/components/TextEditor/extensions/hyperlinkDecoration.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlinkWidget.d.ts.map +0 -1
- package/src/automerge/automerge-plugin/handle.ts +0 -14
- package/src/components/TextEditor/extensions/hyperlinkDecoration.ts +0 -99
|
@@ -2,20 +2,17 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel';
|
|
5
|
+
import { basicSetup } from '@codemirror/basic-setup';
|
|
7
6
|
import { EditorView } from '@codemirror/view';
|
|
8
|
-
import { basicSetup } from 'codemirror';
|
|
9
|
-
import get from 'lodash.get';
|
|
10
7
|
import React, { useEffect, useRef, useState } from 'react';
|
|
11
8
|
|
|
12
|
-
import { type Prop } from '@dxos/automerge/automerge';
|
|
13
|
-
import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
|
|
9
|
+
import { type Prop, next as automerge } from '@dxos/automerge/automerge';
|
|
14
10
|
|
|
15
|
-
import {
|
|
11
|
+
import { plugin as amgPlugin, reconcile } from './automerge-plugin';
|
|
12
|
+
import { Peer } from './demo';
|
|
16
13
|
|
|
17
14
|
type EditorProps = {
|
|
18
|
-
handle:
|
|
15
|
+
handle: Peer;
|
|
19
16
|
path: Prop[];
|
|
20
17
|
};
|
|
21
18
|
|
|
@@ -24,13 +21,27 @@ const Editor = ({ handle, path }: EditorProps) => {
|
|
|
24
21
|
const editorRoot = useRef<EditorView>();
|
|
25
22
|
|
|
26
23
|
useEffect(() => {
|
|
24
|
+
const doc = handle.doc;
|
|
25
|
+
const source = doc.text; // this should use path
|
|
26
|
+
const plugin = amgPlugin(doc, path);
|
|
27
27
|
const view = (editorRoot.current = new EditorView({
|
|
28
|
-
doc:
|
|
29
|
-
extensions: [basicSetup,
|
|
28
|
+
doc: source,
|
|
29
|
+
extensions: [basicSetup, plugin],
|
|
30
|
+
dispatch: (transaction) => {
|
|
31
|
+
view.update([transaction]);
|
|
32
|
+
reconcile(handle, view);
|
|
33
|
+
},
|
|
30
34
|
parent: containerRef.current as any,
|
|
31
35
|
}));
|
|
32
36
|
|
|
37
|
+
const handleChange = () => {
|
|
38
|
+
reconcile(handle, view);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
handle.changeEvent.on(handleChange);
|
|
42
|
+
|
|
33
43
|
return () => {
|
|
44
|
+
handle.changeEvent.off(handleChange);
|
|
34
45
|
view.destroy();
|
|
35
46
|
};
|
|
36
47
|
}, []);
|
|
@@ -39,29 +50,31 @@ const Editor = ({ handle, path }: EditorProps) => {
|
|
|
39
50
|
};
|
|
40
51
|
|
|
41
52
|
const Story = () => {
|
|
42
|
-
const [object1, setObject1] = useState<
|
|
43
|
-
const [object2, setObject2] = useState<
|
|
53
|
+
const [object1, setObject1] = useState<Peer | null>(null);
|
|
54
|
+
const [object2, setObject2] = useState<Peer | null>(null);
|
|
55
|
+
const [stats1, setStats1] = useState<any>({});
|
|
56
|
+
const [stats2, setStats2] = useState<any>({});
|
|
44
57
|
|
|
45
58
|
useEffect(() => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
59
|
+
const object1 = new Peer();
|
|
60
|
+
object1.doc = automerge.from({ text: 'Hello world!' });
|
|
61
|
+
|
|
62
|
+
const object2 = new Peer();
|
|
63
|
+
object2.doc = automerge.init();
|
|
64
|
+
|
|
65
|
+
const r1 = object1.replicate();
|
|
66
|
+
const r2 = object2.replicate();
|
|
67
|
+
|
|
68
|
+
void r1.readable.pipeTo(r2.writable);
|
|
69
|
+
void r2.readable.pipeTo(r1.writable);
|
|
70
|
+
|
|
71
|
+
setObject1(object1);
|
|
72
|
+
setObject2(object2);
|
|
73
|
+
|
|
74
|
+
setInterval(() => {
|
|
75
|
+
setStats1({ ...object1.stats });
|
|
76
|
+
setStats2({ ...object2.stats });
|
|
77
|
+
}, 500);
|
|
65
78
|
}, []);
|
|
66
79
|
|
|
67
80
|
if (!object1 || !object2) {
|
|
@@ -72,9 +85,11 @@ const Story = () => {
|
|
|
72
85
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', width: '100vw' }}>
|
|
73
86
|
<div>
|
|
74
87
|
<Editor handle={object1} path={['text']} />
|
|
88
|
+
<div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats1, null, 2)}</div>
|
|
75
89
|
</div>
|
|
76
90
|
<div>
|
|
77
91
|
<Editor handle={object2} path={['text']} />
|
|
92
|
+
<div style={{ whiteSpace: 'pre' }}>{JSON.stringify(stats2, null, 2)}</div>
|
|
78
93
|
</div>
|
|
79
94
|
</div>
|
|
80
95
|
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Event } from '@dxos/async';
|
|
6
|
+
import {
|
|
7
|
+
type Doc,
|
|
8
|
+
next as automerge,
|
|
9
|
+
type ChangeOptions,
|
|
10
|
+
type ChangeFn,
|
|
11
|
+
type Heads,
|
|
12
|
+
type SyncState,
|
|
13
|
+
} from '@dxos/automerge/automerge';
|
|
14
|
+
|
|
15
|
+
export class Peer {
|
|
16
|
+
changeEvent = new Event();
|
|
17
|
+
|
|
18
|
+
storage = new Map<string, Uint8Array>();
|
|
19
|
+
stats = {
|
|
20
|
+
messagesSent: 0,
|
|
21
|
+
messagesReceived: 0,
|
|
22
|
+
bytesSent: 0,
|
|
23
|
+
bytesReceived: 0,
|
|
24
|
+
snapshotBytes: 0,
|
|
25
|
+
incrementalBytes: 0,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
doc!: Doc<{ text: string }>;
|
|
29
|
+
|
|
30
|
+
change(options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>) {
|
|
31
|
+
this.doc = automerge.change(this.doc, options, callback);
|
|
32
|
+
this._handleChange();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
changeAt(scope: Heads, options: string | ChangeOptions<any> | ChangeFn<any>, callback?: ChangeFn<any>) {
|
|
36
|
+
const { newDoc, newHeads } = automerge.changeAt(this.doc, scope, options, callback);
|
|
37
|
+
this.doc = newDoc;
|
|
38
|
+
this._handleChange();
|
|
39
|
+
return newHeads;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
replicate(): { readable: ReadableStream<Uint8Array>; writable: WritableStream<Uint8Array> } {
|
|
43
|
+
let syncState: SyncState = automerge.initSyncState();
|
|
44
|
+
|
|
45
|
+
let unsub: (() => void) | undefined;
|
|
46
|
+
const readable = new ReadableStream({
|
|
47
|
+
start: (controller) => {
|
|
48
|
+
unsub = this.changeEvent.on(() => {
|
|
49
|
+
const [newState, msg] = automerge.generateSyncMessage(this.doc, syncState);
|
|
50
|
+
syncState = newState;
|
|
51
|
+
|
|
52
|
+
if (msg) {
|
|
53
|
+
this.stats.messagesSent++;
|
|
54
|
+
this.stats.bytesSent += msg.byteLength;
|
|
55
|
+
controller.enqueue(msg);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
pull: (controller) => {
|
|
60
|
+
const [newState, msg] = automerge.generateSyncMessage(this.doc, syncState);
|
|
61
|
+
syncState = newState;
|
|
62
|
+
|
|
63
|
+
if (msg) {
|
|
64
|
+
this.stats.messagesSent++;
|
|
65
|
+
this.stats.bytesSent += msg.byteLength;
|
|
66
|
+
controller.enqueue(msg);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
cancel: () => {
|
|
70
|
+
unsub?.();
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const writable = new WritableStream({
|
|
75
|
+
write: (chunk) => {
|
|
76
|
+
this.stats.messagesReceived++;
|
|
77
|
+
this.stats.bytesReceived += chunk.byteLength;
|
|
78
|
+
|
|
79
|
+
const [newDoc, newState] = automerge.receiveSyncMessage(this.doc, syncState, chunk);
|
|
80
|
+
this.doc = newDoc;
|
|
81
|
+
syncState = newState;
|
|
82
|
+
|
|
83
|
+
this._handleChange();
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return { readable, writable };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private _handleChange() {
|
|
91
|
+
// TODO(dmaretskyi): Save since + debounce.
|
|
92
|
+
|
|
93
|
+
if (this.stats.incrementalBytes > 1024 && this.stats.incrementalBytes > this.stats.snapshotBytes) {
|
|
94
|
+
const snapshot = automerge.save(this.doc);
|
|
95
|
+
this.stats.snapshotBytes = snapshot.byteLength;
|
|
96
|
+
this.stats.incrementalBytes = 0;
|
|
97
|
+
console.log('snapshot save', snapshot.byteLength);
|
|
98
|
+
} else {
|
|
99
|
+
const chunk = automerge.saveIncremental(this.doc);
|
|
100
|
+
this.stats.incrementalBytes += chunk.length;
|
|
101
|
+
console.log('incremental save', chunk.length);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.changeEvent.emit();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -9,7 +9,7 @@ import React, { useState } from 'react';
|
|
|
9
9
|
import { type PublicKey } from '@dxos/react-client';
|
|
10
10
|
import { TextObject, TextKind, useQuery, useSpace } from '@dxos/react-client/echo';
|
|
11
11
|
import { useIdentity } from '@dxos/react-client/halo';
|
|
12
|
-
import { setupPeersInSpace, textGenerator, useDataGenerator
|
|
12
|
+
import { ClientDecorator, setupPeersInSpace, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
|
|
13
13
|
import { useId } from '@dxos/react-ui';
|
|
14
14
|
import { withTheme } from '@dxos/storybook-utils';
|
|
15
15
|
|
|
@@ -56,7 +56,8 @@ const { spaceKey: markdownSpaceKey, clients: markdownClients } = await setupPeer
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
export const Markdown = {
|
|
59
|
-
render: () => <
|
|
59
|
+
render: (args: { id: number }) => <Story {...args} spaceKey={markdownSpaceKey} />,
|
|
60
|
+
decorators: [ClientDecorator({ clients: markdownClients })],
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
const { spaceKey: richSpaceKey, clients: richClients } = await setupPeersInSpace({
|
|
@@ -69,7 +70,7 @@ const { spaceKey: richSpaceKey, clients: richClients } = await setupPeersInSpace
|
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
export const Rich = {
|
|
72
|
-
render: () => <
|
|
73
|
+
render: (args: { id: number }) => <Story {...args} spaceKey={richSpaceKey} />,
|
|
73
74
|
args: {
|
|
74
75
|
slots: {
|
|
75
76
|
editor: {
|
|
@@ -77,4 +78,5 @@ export const Rich = {
|
|
|
77
78
|
},
|
|
78
79
|
},
|
|
79
80
|
},
|
|
81
|
+
decorators: [ClientDecorator({ clients: richClients })],
|
|
80
82
|
};
|
|
@@ -16,6 +16,7 @@ export type EditorProps = UseTextModelOptions & {
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Memoized editor which depends on DXOS platform.
|
|
19
|
+
*
|
|
19
20
|
* Determines which editor to render based on the kind of text.
|
|
20
21
|
*/
|
|
21
22
|
// NOTE: Without `memo`, if parent component uses `observer` the editor re-renders excessively.
|
|
@@ -23,10 +24,6 @@ export type EditorProps = UseTextModelOptions & {
|
|
|
23
24
|
export const Editor = memo(
|
|
24
25
|
forwardRef<TipTapEditor | MarkdownEditorRef, EditorProps>(({ slots, ...params }, forwardedRef) => {
|
|
25
26
|
const model = useTextModel(params);
|
|
26
|
-
if (!model) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
27
|
if (model?.content instanceof YXmlFragment) {
|
|
31
28
|
return <RichTextEditor ref={forwardedRef as Ref<TipTapEditor>} model={model} slots={slots} />;
|
|
32
29
|
} else {
|
|
@@ -8,7 +8,7 @@ import React, { useState } from 'react';
|
|
|
8
8
|
|
|
9
9
|
import { PublicKey } from '@dxos/react-client';
|
|
10
10
|
import { TextKind } from '@dxos/react-client/echo';
|
|
11
|
-
import {
|
|
11
|
+
import { ClientDecorator, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
|
|
12
12
|
import { useId } from '@dxos/react-ui';
|
|
13
13
|
import { withTheme } from '@dxos/storybook-utils';
|
|
14
14
|
|
|
@@ -20,51 +20,40 @@ export default {
|
|
|
20
20
|
decorators: [withTheme],
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const content = [
|
|
24
|
-
'',
|
|
25
|
-
'',
|
|
26
|
-
'This is all about [DXOS](https://dxos.org); read more [here](https://docs.dxos.org/guide/getting-started.html).',
|
|
27
|
-
'',
|
|
28
|
-
'This is a link: https://www.dxos.org',
|
|
29
|
-
'',
|
|
30
|
-
'',
|
|
31
|
-
].join('\n');
|
|
32
|
-
|
|
33
23
|
export const Default = {
|
|
34
24
|
args: {
|
|
35
25
|
model: {
|
|
36
26
|
id: 'editor',
|
|
37
|
-
content,
|
|
27
|
+
content: 'Hello, Storybook!',
|
|
38
28
|
},
|
|
39
29
|
},
|
|
40
30
|
};
|
|
41
31
|
|
|
42
32
|
const replicator = new Replicator(TextKind.PLAIN);
|
|
43
33
|
|
|
44
|
-
const Story = () => {
|
|
45
|
-
const [generate, setGenerate] = useState(false);
|
|
46
|
-
const generateId = useId('generate');
|
|
47
|
-
|
|
48
|
-
const [id] = useState(PublicKey.random().toHex());
|
|
49
|
-
const model = useYjsModel({ id, replicator });
|
|
50
|
-
|
|
51
|
-
useDataGenerator({
|
|
52
|
-
generator: generate ? textGenerator : undefined,
|
|
53
|
-
options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<main className='flex-1 min-w-0 p-4'>
|
|
58
|
-
<div id={generateId} className='flex'>
|
|
59
|
-
<input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
|
|
60
|
-
Generate Data
|
|
61
|
-
</div>
|
|
62
|
-
<MarkdownEditor model={model} />
|
|
63
|
-
</main>
|
|
64
|
-
);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
34
|
export const WithYjs = {
|
|
35
|
+
render: () => {
|
|
36
|
+
const [generate, setGenerate] = useState(false);
|
|
37
|
+
const generateId = useId('generate');
|
|
38
|
+
|
|
39
|
+
const [id] = useState(PublicKey.random().toHex());
|
|
40
|
+
const model = useYjsModel({ id, replicator });
|
|
41
|
+
|
|
42
|
+
useDataGenerator({
|
|
43
|
+
generator: generate ? textGenerator : undefined,
|
|
44
|
+
options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<main className='flex-1 min-w-0 p-4'>
|
|
49
|
+
<div id={generateId} className='flex'>
|
|
50
|
+
<input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
|
|
51
|
+
Generate Data
|
|
52
|
+
</div>
|
|
53
|
+
<MarkdownEditor model={model} />
|
|
54
|
+
</main>
|
|
55
|
+
);
|
|
56
|
+
},
|
|
68
57
|
// TODO(wittjosiah): Decorator for doing this without clients being initialized?
|
|
69
|
-
|
|
58
|
+
decorators: [ClientDecorator({ count: 2 })],
|
|
70
59
|
};
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
EditorView,
|
|
31
31
|
} from '@codemirror/view';
|
|
32
32
|
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
33
|
-
// import { GFM } from '@lezer/markdown';
|
|
34
33
|
import { vim } from '@replit/codemirror-vim';
|
|
35
34
|
import React, {
|
|
36
35
|
type KeyboardEvent,
|
|
@@ -49,17 +48,16 @@ import { getColorForValue } from '@dxos/react-ui-theme';
|
|
|
49
48
|
import { YText } from '@dxos/text-model';
|
|
50
49
|
|
|
51
50
|
import { markdownTagsExtension } from './markdownTags';
|
|
52
|
-
import {
|
|
51
|
+
import { markdownDarkHighlighting, markdownTheme } from './markdownTheme';
|
|
53
52
|
import { type EditorModel, type EditorSlots } from '../../model';
|
|
54
53
|
|
|
55
54
|
export const EditorModes = ['default', 'vim'] as const;
|
|
56
55
|
export type EditorMode = (typeof EditorModes)[number];
|
|
57
56
|
|
|
58
57
|
export type MarkdownEditorProps = {
|
|
59
|
-
model
|
|
58
|
+
model?: EditorModel;
|
|
60
59
|
slots?: EditorSlots;
|
|
61
60
|
editorMode?: EditorMode;
|
|
62
|
-
showWidgets?: boolean;
|
|
63
61
|
extensions?: Extension[];
|
|
64
62
|
onChange?: (content: string | Text) => void;
|
|
65
63
|
};
|
|
@@ -118,73 +116,50 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|
|
118
116
|
const state = EditorState.create({
|
|
119
117
|
doc: content?.toString(),
|
|
120
118
|
extensions: [
|
|
121
|
-
// Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
|
|
119
|
+
// Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799.
|
|
122
120
|
listenChangesExtension,
|
|
123
121
|
|
|
124
|
-
|
|
122
|
+
...(editorMode === 'vim' ? [vim()] : []),
|
|
123
|
+
|
|
125
124
|
// All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
highlightActiveLineGutter(),
|
|
126
|
+
highlightSpecialChars(),
|
|
127
|
+
history(),
|
|
128
|
+
drawSelection(),
|
|
129
|
+
dropCursor(),
|
|
130
|
+
EditorState.allowMultipleSelections.of(true),
|
|
131
|
+
indentOnInput(),
|
|
132
|
+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
128
133
|
bracketMatching(),
|
|
129
134
|
closeBrackets(),
|
|
135
|
+
autocompletion(),
|
|
136
|
+
rectangularSelection(),
|
|
130
137
|
crosshairCursor(),
|
|
131
|
-
dropCursor(),
|
|
132
|
-
drawSelection(),
|
|
133
138
|
highlightActiveLine(),
|
|
134
|
-
highlightActiveLineGutter(),
|
|
135
139
|
highlightSelectionMatches(),
|
|
136
|
-
highlightSpecialChars(),
|
|
137
|
-
history(),
|
|
138
|
-
indentOnInput(),
|
|
139
140
|
placeholder(slots.editor?.placeholder ?? ''), // TODO(burdon): Needs consistent styling.
|
|
140
|
-
rectangularSelection(),
|
|
141
|
-
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
142
|
-
EditorState.allowMultipleSelections.of(true),
|
|
143
|
-
EditorView.lineWrapping,
|
|
144
|
-
|
|
145
141
|
keymap.of([
|
|
146
142
|
...closeBracketsKeymap,
|
|
147
|
-
...completionKeymap,
|
|
148
143
|
...defaultKeymap,
|
|
149
|
-
...
|
|
144
|
+
...searchKeymap,
|
|
150
145
|
...historyKeymap,
|
|
146
|
+
...foldKeymap,
|
|
147
|
+
...completionKeymap,
|
|
151
148
|
...lintKeymap,
|
|
152
|
-
...searchKeymap,
|
|
153
149
|
indentWithTab,
|
|
154
150
|
]),
|
|
151
|
+
EditorView.lineWrapping,
|
|
155
152
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
markdown({
|
|
159
|
-
base: markdownLanguage,
|
|
160
|
-
codeLanguages: languages,
|
|
161
|
-
extensions: [
|
|
162
|
-
// TODO(burdon): This seems to upgrade the parser.
|
|
163
|
-
// GitHub flavored markdown bundle: Table, TaskList, Strikethrough, and Autolink.
|
|
164
|
-
// https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
|
|
165
|
-
// https://github.github.com/gfm
|
|
166
|
-
// GFM,
|
|
167
|
-
|
|
168
|
-
// Custom styling.
|
|
169
|
-
markdownTagsExtension,
|
|
170
|
-
],
|
|
171
|
-
}),
|
|
172
|
-
|
|
173
|
-
// Theme.
|
|
153
|
+
// Themes.
|
|
154
|
+
markdown({ base: markdownLanguage, codeLanguages: languages, extensions: [markdownTagsExtension] }),
|
|
174
155
|
EditorView.theme({ ...markdownTheme, ...slots.editor?.markdownTheme }),
|
|
175
156
|
...(themeMode === 'dark'
|
|
176
157
|
? [syntaxHighlighting(oneDarkHighlightStyle)]
|
|
177
158
|
: [syntaxHighlighting(defaultHighlightStyle)]),
|
|
178
|
-
|
|
179
159
|
// TODO(thure): All but one rule here apply to both themes; rename or refactor.
|
|
180
|
-
syntaxHighlighting(
|
|
181
|
-
|
|
182
|
-
// Settings.
|
|
183
|
-
...(editorMode === 'vim' ? [vim()] : []),
|
|
160
|
+
syntaxHighlighting(markdownDarkHighlighting),
|
|
184
161
|
|
|
185
|
-
// TODO(burdon): Move to extensions.
|
|
186
162
|
// Replication and awareness (incl. remote selection).
|
|
187
|
-
// https://codemirror.net/docs/ref/#collab
|
|
188
163
|
...(content instanceof YText ? [yCollab(content, provider?.awareness)] : []),
|
|
189
164
|
|
|
190
165
|
// Custom.
|
|
@@ -32,10 +32,6 @@ export const cursor = '#ffffff';
|
|
|
32
32
|
|
|
33
33
|
const monospace = get(tokens, 'fontFamily.mono', ['monospace']).join(',');
|
|
34
34
|
|
|
35
|
-
// TODO(burdon): Subtle difference in layout for documents and stacks.
|
|
36
|
-
// TODO(burdon): On first character, height of editor changes by a few pixels.
|
|
37
|
-
// TODO(burdon): Is the '&' prefix required? If so, document.
|
|
38
|
-
|
|
39
35
|
export const markdownTheme = {
|
|
40
36
|
// TODO(thure): consider whether these commented-out rules from one-dark-theme should be integrated.
|
|
41
37
|
// '&': {
|
|
@@ -168,13 +164,6 @@ export const markdownTheme = {
|
|
|
168
164
|
'& .cm-yLineSelection': {
|
|
169
165
|
margin: '0',
|
|
170
166
|
},
|
|
171
|
-
'.cm-link': {
|
|
172
|
-
color: 'rgb(20 89 208)',
|
|
173
|
-
textDecorationLine: 'underline',
|
|
174
|
-
textDecorationThickness: '1px',
|
|
175
|
-
textUnderlineOffset: '2px',
|
|
176
|
-
borderRadius: '.125rem',
|
|
177
|
-
},
|
|
178
167
|
...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
|
|
179
168
|
const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
|
|
180
169
|
// TODO(thure): This appears to be the best or only way to set selection caret heights, but it's far more verbose than it needs to be.
|
|
@@ -185,7 +174,7 @@ export const markdownTheme = {
|
|
|
185
174
|
}, {}),
|
|
186
175
|
};
|
|
187
176
|
|
|
188
|
-
export const
|
|
177
|
+
export const markdownDarkHighlighting = HighlightStyle.define(
|
|
189
178
|
[
|
|
190
179
|
{
|
|
191
180
|
tag: [
|
|
@@ -8,7 +8,7 @@ import React, { useState } from 'react';
|
|
|
8
8
|
|
|
9
9
|
import { PublicKey } from '@dxos/react-client';
|
|
10
10
|
import { TextKind } from '@dxos/react-client/echo';
|
|
11
|
-
import {
|
|
11
|
+
import { ClientDecorator, textGenerator, useDataGenerator } from '@dxos/react-client/testing';
|
|
12
12
|
import { useId } from '@dxos/react-ui';
|
|
13
13
|
import { mx } from '@dxos/react-ui-theme';
|
|
14
14
|
import { withTheme } from '@dxos/storybook-utils';
|
|
@@ -31,42 +31,40 @@ export const Default = {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const replicator = new Replicator(TextKind.RICH);
|
|
34
|
+
export const WithYjs = {
|
|
35
|
+
render: (args: Omit<RichTextEditorProps, 'model'>) => {
|
|
36
|
+
const [generate, setGenerate] = useState(false);
|
|
37
|
+
const generateId = useId('generate');
|
|
34
38
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const generateId = useId('generate');
|
|
38
|
-
|
|
39
|
-
const [id] = useState(PublicKey.random().toHex());
|
|
40
|
-
const model = useYjsModel({ id, replicator });
|
|
41
|
-
|
|
42
|
-
useDataGenerator({
|
|
43
|
-
generator: generate ? textGenerator : undefined,
|
|
44
|
-
options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
|
|
45
|
-
});
|
|
39
|
+
const [id] = useState(PublicKey.random().toHex());
|
|
40
|
+
const model = useYjsModel({ id, replicator });
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Generate Data
|
|
52
|
-
</div>
|
|
53
|
-
<RichTextEditor
|
|
54
|
-
{...args}
|
|
55
|
-
model={model}
|
|
56
|
-
slots={{
|
|
57
|
-
editor: {
|
|
58
|
-
className: mx(
|
|
59
|
-
'z-0 rounded bg-white text-neutral-900 w-full p-4 dark:bg-neutral-850 dark:text-white min-bs-[12em]',
|
|
60
|
-
args.slots?.editor?.className,
|
|
61
|
-
),
|
|
62
|
-
},
|
|
63
|
-
}}
|
|
64
|
-
/>
|
|
65
|
-
</main>
|
|
66
|
-
);
|
|
67
|
-
};
|
|
42
|
+
useDataGenerator({
|
|
43
|
+
generator: generate ? textGenerator : undefined,
|
|
44
|
+
options: { text: typeof model?.content !== 'string' ? model?.content : undefined },
|
|
45
|
+
});
|
|
68
46
|
|
|
69
|
-
|
|
47
|
+
return (
|
|
48
|
+
<main className='flex-1 min-w-0 p-4'>
|
|
49
|
+
<div id={generateId} className='flex'>
|
|
50
|
+
<input type='checkbox' onChange={(event) => setGenerate(event.target.checked)} />
|
|
51
|
+
Generate Data
|
|
52
|
+
</div>
|
|
53
|
+
<RichTextEditor
|
|
54
|
+
{...args}
|
|
55
|
+
model={model}
|
|
56
|
+
slots={{
|
|
57
|
+
editor: {
|
|
58
|
+
className: mx(
|
|
59
|
+
'z-0 rounded bg-white text-neutral-900 w-full p-4 dark:bg-neutral-850 dark:text-white min-bs-[12em]',
|
|
60
|
+
args.slots?.editor?.className,
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
</main>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
70
68
|
// TODO(wittjosiah): Decorator for doing this without clients being initialized?
|
|
71
|
-
|
|
69
|
+
decorators: [ClientDecorator({ count: 2 })],
|
|
72
70
|
};
|