@dxos/react-ui-editor 0.3.11-main.008b7c6 → 0.3.11-main.08776d7
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 +745 -810
- 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 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +16 -19
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -1
- 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/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +6 -8
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +7 -11
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/code.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +26 -17
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/hr.d.ts.map +1 -1
- package/dist/types/src/extensions/image.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +3 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- package/dist/types/src/extensions/sync.d.ts +7 -0
- package/dist/types/src/extensions/sync.d.ts.map +1 -0
- package/dist/types/src/extensions/tasklist.d.ts +5 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge.d.ts +2 -1
- package/dist/types/src/hooks/automerge.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +28 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +2 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +2 -32
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs.d.ts +2 -1
- package/dist/types/src/hooks/yjs.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +25 -24
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +45 -41
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +101 -89
- package/src/components/TextEditor/comments.stories.tsx +362 -0
- package/src/extensions/autocomplete.ts +18 -17
- package/src/extensions/automerge/automerge.ts +15 -46
- package/src/extensions/automerge/cursor.ts +2 -1
- package/src/extensions/automerge/defs.ts +13 -14
- package/src/extensions/automerge/semaphore.ts +21 -16
- package/src/extensions/awareness.ts +27 -33
- package/src/extensions/code.ts +28 -23
- package/src/extensions/comments.ts +234 -236
- package/src/extensions/cursor.ts +50 -0
- package/src/extensions/hr.ts +10 -12
- package/src/extensions/image.ts +40 -22
- package/src/extensions/index.ts +3 -1
- package/src/extensions/link.ts +86 -88
- package/src/extensions/markdown/highlight.ts +3 -12
- package/src/extensions/outliner.ts +12 -0
- package/src/extensions/sync.ts +15 -0
- package/src/extensions/table.ts +10 -4
- package/src/extensions/tasklist.ts +54 -79
- package/src/extensions/yjs/cursor.ts +2 -1
- package/src/extensions/yjs/yjs.test.ts +1 -1
- package/src/extensions/yjs/yjs.ts +2 -2
- package/src/hooks/automerge.ts +2 -1
- package/src/hooks/defs.ts +47 -0
- package/src/hooks/index.ts +3 -1
- package/src/hooks/useTextModel.ts +3 -49
- package/src/hooks/yjs.ts +2 -23
- package/src/index.ts +3 -0
- package/src/styles/markdown.ts +0 -2
- package/src/styles/tokens.ts +3 -0
- package/src/themes/default.ts +51 -78
- package/dist/types/src/extensions/mermaid.d.ts +0 -3
- package/dist/types/src/extensions/mermaid.d.ts.map +0 -1
- package/dist/types/src/util/cursor.d.ts.map +0 -1
- package/dist/types/src/util/index.d.ts +0 -3
- package/dist/types/src/util/index.d.ts.map +0 -1
- package/dist/types/src/util/util.d.ts.map +0 -1
- package/src/extensions/mermaid.ts +0 -11
- package/src/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- /package/dist/types/src/{util/util.d.ts → util.d.ts} +0 -0
- /package/src/{util/util.ts → util.ts} +0 -0
|
@@ -8,15 +8,16 @@ import { EditorView } from '@codemirror/view';
|
|
|
8
8
|
import { faker } from '@faker-js/faker';
|
|
9
9
|
import { ArrowSquareOut } from '@phosphor-icons/react';
|
|
10
10
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
11
|
-
import React, { type FC, StrictMode,
|
|
11
|
+
import React, { type FC, StrictMode, useState } from 'react';
|
|
12
12
|
import { createRoot } from 'react-dom/client';
|
|
13
13
|
|
|
14
14
|
import { TextObject } from '@dxos/echo-schema';
|
|
15
|
+
import { keySymbols, parseShortcut } from '@dxos/keyboard';
|
|
15
16
|
import { PublicKey } from '@dxos/keys';
|
|
16
17
|
import { fixedInsetFlexLayout, getSize, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
17
18
|
import { withTheme } from '@dxos/storybook-utils';
|
|
18
19
|
|
|
19
|
-
import { MarkdownEditor, type TextEditorProps
|
|
20
|
+
import { MarkdownEditor, type TextEditorProps } from './TextEditor';
|
|
20
21
|
import {
|
|
21
22
|
autocomplete,
|
|
22
23
|
blast,
|
|
@@ -25,7 +26,6 @@ import {
|
|
|
25
26
|
defaultOptions,
|
|
26
27
|
hr,
|
|
27
28
|
image,
|
|
28
|
-
listener,
|
|
29
29
|
link,
|
|
30
30
|
mention,
|
|
31
31
|
table,
|
|
@@ -33,9 +33,9 @@ import {
|
|
|
33
33
|
typewriter,
|
|
34
34
|
type CommentsOptions,
|
|
35
35
|
type LinkOptions,
|
|
36
|
-
|
|
36
|
+
outliner,
|
|
37
37
|
} from '../../extensions';
|
|
38
|
-
import { type
|
|
38
|
+
import { type Comment, useTextModel } from '../../hooks';
|
|
39
39
|
|
|
40
40
|
// Extensions:
|
|
41
41
|
// TODO(burdon): Table of contents.
|
|
@@ -123,8 +123,6 @@ const text = {
|
|
|
123
123
|
|
|
124
124
|
paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
|
|
125
125
|
|
|
126
|
-
mermaid: str('```mermaid', 'graph TD;', 'A-->B;', 'A-->C;', 'B-->D;', 'C-->D;', '```'),
|
|
127
|
-
|
|
128
126
|
footer: str('', '', '', '', '')
|
|
129
127
|
};
|
|
130
128
|
|
|
@@ -185,15 +183,15 @@ const Key: FC<{ char: string }> = ({ char }) => (
|
|
|
185
183
|
</span>
|
|
186
184
|
);
|
|
187
185
|
|
|
188
|
-
const onCommentsHover: CommentsOptions['onHover'] = (el) => {
|
|
186
|
+
const onCommentsHover: CommentsOptions['onHover'] = (el, shortcut) => {
|
|
189
187
|
createRoot(el).render(
|
|
190
188
|
<StrictMode>
|
|
191
189
|
<div className='flex items-center gap-2 px-2 py-2 bg-neutral-700 text-white text-xs rounded'>
|
|
192
190
|
<div>Create comment</div>
|
|
193
|
-
{/* TODO(burdon): Unify shortcuts. */}
|
|
194
191
|
<div className='flex gap-1'>
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
{keySymbols(parseShortcut(shortcut)).map((char) => (
|
|
193
|
+
<Key key={char} char={char} />
|
|
194
|
+
))}
|
|
197
195
|
</div>
|
|
198
196
|
</div>
|
|
199
197
|
</StrictMode>,
|
|
@@ -204,7 +202,7 @@ const onRenderLink: LinkOptions['onRender'] = (el, url) => {
|
|
|
204
202
|
createRoot(el).render(
|
|
205
203
|
<StrictMode>
|
|
206
204
|
<a href={url} target='_blank' rel='noreferrer' className={hover}>
|
|
207
|
-
<ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1 mb-
|
|
205
|
+
<ArrowSquareOut weight='bold' className={mx(getSize(4), 'inline-block leading-none mis-1 mb-[2px]')} />
|
|
208
206
|
</a>
|
|
209
207
|
</StrictMode>,
|
|
210
208
|
);
|
|
@@ -216,7 +214,6 @@ type StoryProps = {
|
|
|
216
214
|
} & Pick<TextEditorProps, 'comments' | 'readonly' | 'extensions' | 'slots'>;
|
|
217
215
|
|
|
218
216
|
const Story = ({ text, automerge, ...props }: StoryProps) => {
|
|
219
|
-
const ref = useRef<TextEditorRef>(null);
|
|
220
217
|
const [item] = useState({ text: new TextObject(text, undefined, undefined, { automerge }) });
|
|
221
218
|
const model = useTextModel({ text: item.text });
|
|
222
219
|
if (!model) {
|
|
@@ -225,10 +222,9 @@ const Story = ({ text, automerge, ...props }: StoryProps) => {
|
|
|
225
222
|
|
|
226
223
|
return (
|
|
227
224
|
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
228
|
-
<div className='flex justify-center
|
|
229
|
-
<div className='flex flex-col w-[800px]
|
|
230
|
-
<MarkdownEditor
|
|
231
|
-
<div className='flex shrink-0 h-[300px]'></div>
|
|
225
|
+
<div className='flex h-full justify-center'>
|
|
226
|
+
<div className='flex flex-col h-full w-[800px]'>
|
|
227
|
+
<MarkdownEditor model={model} {...props} />
|
|
232
228
|
</div>
|
|
233
229
|
</div>
|
|
234
230
|
</div>
|
|
@@ -262,6 +258,16 @@ export const Readonly = {
|
|
|
262
258
|
render: () => <Story text={document} extensions={extensions} readonly />,
|
|
263
259
|
};
|
|
264
260
|
|
|
261
|
+
const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
|
|
262
|
+
|
|
263
|
+
export const Large = {
|
|
264
|
+
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export const Empty = {
|
|
268
|
+
render: () => <Story />,
|
|
269
|
+
};
|
|
270
|
+
|
|
265
271
|
export const NoExtensions = {
|
|
266
272
|
render: () => <Story text={document} />,
|
|
267
273
|
};
|
|
@@ -298,17 +304,14 @@ export const Image = {
|
|
|
298
304
|
};
|
|
299
305
|
|
|
300
306
|
export const Tasklist = {
|
|
307
|
+
render: () => <Story text={str(text.tasks, '', text.list, text.footer)} extensions={[tasklist()]} />,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export const Outliner = {
|
|
301
311
|
render: () => (
|
|
302
312
|
<Story
|
|
303
|
-
text={str(
|
|
304
|
-
extensions={[
|
|
305
|
-
tasklist(),
|
|
306
|
-
listener({
|
|
307
|
-
onChange: (text) => {
|
|
308
|
-
console.log(text);
|
|
309
|
-
},
|
|
310
|
-
}),
|
|
311
|
-
]}
|
|
313
|
+
text={str('# Outliner', '', 'Block', ': this is a block', ': with multiple lines', text.footer)}
|
|
314
|
+
extensions={[outliner()]}
|
|
312
315
|
/>
|
|
313
316
|
),
|
|
314
317
|
};
|
|
@@ -342,24 +345,20 @@ export const Mention = {
|
|
|
342
345
|
),
|
|
343
346
|
};
|
|
344
347
|
|
|
345
|
-
export const Mermaid = {
|
|
346
|
-
render: () => <Story text={str('# Mermaid', '', text.mermaid, text.footer)} extensions={[code(), mermaid()]} />,
|
|
347
|
-
};
|
|
348
|
-
|
|
349
348
|
export const Comments = {
|
|
350
349
|
render: () => {
|
|
351
|
-
const [
|
|
350
|
+
const [commentRanges, setCommentRanges] = useState<Comment[]>([]);
|
|
352
351
|
|
|
353
352
|
return (
|
|
354
353
|
<Story
|
|
355
354
|
text={str('# Comments', '', text.paragraphs, text.footer)}
|
|
356
|
-
comments={
|
|
355
|
+
comments={commentRanges}
|
|
357
356
|
extensions={[
|
|
358
357
|
comments({
|
|
359
358
|
onHover: onCommentsHover,
|
|
360
|
-
onCreate: (
|
|
359
|
+
onCreate: (range) => {
|
|
361
360
|
const id = PublicKey.random().toHex();
|
|
362
|
-
|
|
361
|
+
setCommentRanges((commentRanges) => [...commentRanges, { id, cursor: range }]);
|
|
363
362
|
return id;
|
|
364
363
|
},
|
|
365
364
|
onSelect: (state) => {
|
|
@@ -368,9 +367,9 @@ export const Comments = {
|
|
|
368
367
|
console.log(
|
|
369
368
|
'update',
|
|
370
369
|
JSON.stringify({
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
370
|
+
comments: state.comments.length,
|
|
371
|
+
active: state.selection.active?.slice(0, 8),
|
|
372
|
+
closest: state.selection.closest?.slice(0, 8),
|
|
374
373
|
}),
|
|
375
374
|
);
|
|
376
375
|
}
|
|
@@ -382,8 +381,15 @@ export const Comments = {
|
|
|
382
381
|
},
|
|
383
382
|
};
|
|
384
383
|
|
|
384
|
+
const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
|
|
385
|
+
|
|
385
386
|
export const Typewriter = {
|
|
386
|
-
render: () =>
|
|
387
|
+
render: () => (
|
|
388
|
+
<Story
|
|
389
|
+
text={str('# Typewriter', '', text.paragraphs, text.footer)}
|
|
390
|
+
extensions={[typewriter({ items: typewriterItems })]}
|
|
391
|
+
/>
|
|
392
|
+
),
|
|
387
393
|
};
|
|
388
394
|
|
|
389
395
|
export const Blast = {
|
|
@@ -391,9 +397,7 @@ export const Blast = {
|
|
|
391
397
|
<Story
|
|
392
398
|
text={str('# Blast', '', text.paragraphs, text.code, text.paragraphs)}
|
|
393
399
|
extensions={[
|
|
394
|
-
typewriter({
|
|
395
|
-
items: localStorage.getItem('dxos.composer.extension.demo')?.split(','),
|
|
396
|
-
}),
|
|
400
|
+
typewriter({ items: typewriterItems }),
|
|
397
401
|
blast(
|
|
398
402
|
defaultsDeep(
|
|
399
403
|
{
|
|
@@ -4,17 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
|
-
import
|
|
8
|
-
import React, { useRef, useState } from 'react';
|
|
7
|
+
import React, { useState } from 'react';
|
|
9
8
|
|
|
10
9
|
import { TextObject } from '@dxos/echo-schema';
|
|
11
10
|
import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
12
11
|
import { withTheme } from '@dxos/storybook-utils';
|
|
13
12
|
|
|
14
|
-
import {
|
|
13
|
+
import { TextEditor, type TextEditorProps } from './TextEditor';
|
|
15
14
|
import { listener } from '../../extensions';
|
|
16
15
|
import { useTextModel } from '../../hooks';
|
|
17
|
-
import { textTheme } from '../../themes';
|
|
18
16
|
|
|
19
17
|
const initialText = [
|
|
20
18
|
'# TextEditor',
|
|
@@ -30,8 +28,7 @@ const Story = ({
|
|
|
30
28
|
text,
|
|
31
29
|
automerge,
|
|
32
30
|
...props
|
|
33
|
-
}: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'slots'>) => {
|
|
34
|
-
const ref = useRef<TextEditorRef>(null);
|
|
31
|
+
}: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'placeholder' | 'slots'>) => {
|
|
35
32
|
const [item] = useState({ text: new TextObject(text, undefined, undefined, { automerge }) });
|
|
36
33
|
const model = useTextModel({ text: item.text });
|
|
37
34
|
if (!model) {
|
|
@@ -40,10 +37,9 @@ const Story = ({
|
|
|
40
37
|
|
|
41
38
|
return (
|
|
42
39
|
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
43
|
-
<div className='flex justify-center
|
|
40
|
+
<div className='flex justify-center'>
|
|
44
41
|
<div className='flex flex-col w-[800px] py-16'>
|
|
45
|
-
<TextEditor
|
|
46
|
-
<div className='flex shrink-0 h-[300px]'></div>
|
|
42
|
+
<TextEditor model={model} {...props} />
|
|
47
43
|
</div>
|
|
48
44
|
</div>
|
|
49
45
|
</div>
|
|
@@ -58,49 +54,19 @@ export default {
|
|
|
58
54
|
};
|
|
59
55
|
|
|
60
56
|
export const Default = {
|
|
61
|
-
render: () =>
|
|
62
|
-
<Story
|
|
63
|
-
slots={defaultsDeep(
|
|
64
|
-
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
65
|
-
defaultSlots,
|
|
66
|
-
)}
|
|
67
|
-
/>
|
|
68
|
-
),
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export const Listener = {
|
|
72
|
-
render: () => (
|
|
73
|
-
<Story
|
|
74
|
-
slots={defaultsDeep(
|
|
75
|
-
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
76
|
-
defaultSlots,
|
|
77
|
-
)}
|
|
78
|
-
extensions={[listener({ onChange: (text) => console.log(text) })]}
|
|
79
|
-
/>
|
|
80
|
-
),
|
|
57
|
+
render: () => <Story placeholder='Enter text...' />,
|
|
81
58
|
};
|
|
82
59
|
|
|
83
60
|
export const Text = {
|
|
84
|
-
render: () =>
|
|
85
|
-
<Story
|
|
86
|
-
text={initialText}
|
|
87
|
-
slots={defaultsDeep(
|
|
88
|
-
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
89
|
-
defaultSlots,
|
|
90
|
-
)}
|
|
91
|
-
/>
|
|
92
|
-
),
|
|
61
|
+
render: () => <Story text={initialText} placeholder='Enter text...' />,
|
|
93
62
|
};
|
|
94
63
|
|
|
95
64
|
export const Automerge = {
|
|
65
|
+
render: () => <Story text={initialText} placeholder='Enter text...' automerge />,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const Listener = {
|
|
96
69
|
render: () => (
|
|
97
|
-
<Story
|
|
98
|
-
automerge
|
|
99
|
-
text={initialText}
|
|
100
|
-
slots={defaultsDeep(
|
|
101
|
-
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
102
|
-
defaultSlots,
|
|
103
|
-
)}
|
|
104
|
-
/>
|
|
70
|
+
<Story placeholder='Enter text...' extensions={[listener({ onChange: (text) => console.log(text) })]} />
|
|
105
71
|
),
|
|
106
72
|
};
|
|
@@ -15,14 +15,15 @@ import React, {
|
|
|
15
15
|
useEffect,
|
|
16
16
|
useImperativeHandle,
|
|
17
17
|
useState,
|
|
18
|
+
useRef,
|
|
18
19
|
} from 'react';
|
|
19
20
|
|
|
20
21
|
import { generateName } from '@dxos/display-name';
|
|
21
22
|
import { useThemeContext } from '@dxos/react-ui';
|
|
22
23
|
import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
|
|
23
24
|
|
|
24
|
-
import { basicBundle, markdownBundle,
|
|
25
|
-
import { type
|
|
25
|
+
import { basicBundle, markdownBundle, setComments, syncFacet } from '../../extensions';
|
|
26
|
+
import { type Comment, type EditorModel } from '../../hooks';
|
|
26
27
|
import { type ThemeStyles } from '../../styles';
|
|
27
28
|
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
28
29
|
|
|
@@ -34,92 +35,106 @@ export type CursorInfo = {
|
|
|
34
35
|
to: number;
|
|
35
36
|
line: number;
|
|
36
37
|
lines: number;
|
|
38
|
+
length: number;
|
|
37
39
|
after?: string;
|
|
38
40
|
};
|
|
39
41
|
|
|
40
|
-
export type TextEditorRef = {
|
|
41
|
-
root: HTMLDivElement | null;
|
|
42
|
-
state?: EditorState;
|
|
43
|
-
view?: EditorView;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
42
|
export type TextEditorSlots = {
|
|
47
43
|
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
48
|
-
editor?: {
|
|
49
|
-
className?: string;
|
|
50
|
-
placeholder?: string;
|
|
51
|
-
spellCheck?: boolean;
|
|
52
|
-
tabIndex?: number;
|
|
53
|
-
theme?: ThemeStyles;
|
|
54
|
-
};
|
|
55
44
|
};
|
|
56
45
|
|
|
46
|
+
// TODO(burdon): Spellcheck?
|
|
57
47
|
export type TextEditorProps = {
|
|
58
48
|
model: EditorModel;
|
|
49
|
+
autofocus?: boolean;
|
|
59
50
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
60
|
-
|
|
51
|
+
selection?: { anchor: number; head?: number };
|
|
52
|
+
comments?: Comment[]; // TODO(burdon): Move into extension?
|
|
61
53
|
extensions?: Extension[];
|
|
62
54
|
editorMode?: EditorMode;
|
|
55
|
+
placeholder?: string;
|
|
56
|
+
theme?: ThemeStyles;
|
|
63
57
|
slots?: TextEditorSlots;
|
|
64
58
|
};
|
|
65
59
|
|
|
60
|
+
// TODO(burdon): Factor out?
|
|
61
|
+
export const useComments = (view?: EditorView | null, comments?: Comment[]) => {
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (view && comments !== undefined) {
|
|
64
|
+
view.dispatch({
|
|
65
|
+
effects: setComments.of(comments),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}, [view, comments]);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// TODO(burdon): Factor out?
|
|
72
|
+
export const useAwareness = ({ awareness, peer }: EditorModel) => {
|
|
73
|
+
const { themeMode } = useThemeContext();
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (awareness && peer) {
|
|
76
|
+
awareness.setLocalStateField('user', {
|
|
77
|
+
name: peer.name ?? generateName(peer.id),
|
|
78
|
+
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
79
|
+
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}, [awareness, peer, themeMode]);
|
|
83
|
+
};
|
|
84
|
+
|
|
66
85
|
/**
|
|
67
86
|
* Base text editor.
|
|
68
|
-
* NOTE: Rather than adding properties, try to create extensions that can be reused.
|
|
69
87
|
*/
|
|
70
|
-
export const BaseTextEditor = forwardRef<
|
|
71
|
-
(
|
|
72
|
-
|
|
88
|
+
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
89
|
+
(
|
|
90
|
+
{ model, autofocus, readonly, selection, comments, extensions = [], editorMode, theme, slots = defaultSlots },
|
|
91
|
+
forwardedRef,
|
|
92
|
+
) => {
|
|
73
93
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
>();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const { awareness, peer } = model;
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (awareness && peer) {
|
|
84
|
-
awareness.setLocalStateField('user', {
|
|
85
|
-
name: peer.name ?? generateName(peer.id),
|
|
86
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
87
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}, [awareness, peer, themeMode]);
|
|
94
|
+
const { themeMode } = useThemeContext();
|
|
95
|
+
|
|
96
|
+
// The editor view ref should only be used as an escape hatch.
|
|
97
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
98
|
+
const [view, setView] = useState<EditorView | null>(null);
|
|
99
|
+
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
100
|
+
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
91
101
|
|
|
92
|
-
//
|
|
102
|
+
// Focus.
|
|
93
103
|
useEffect(() => {
|
|
94
|
-
if (
|
|
95
|
-
view
|
|
96
|
-
effects: setCommentRange.of({ model, comments }),
|
|
97
|
-
});
|
|
104
|
+
if (autofocus) {
|
|
105
|
+
view?.focus();
|
|
98
106
|
}
|
|
99
|
-
}, [view,
|
|
107
|
+
}, [view, autofocus]);
|
|
108
|
+
|
|
109
|
+
// TODO(burdon): Factor out as extension/hook.
|
|
110
|
+
useAwareness(model);
|
|
111
|
+
|
|
112
|
+
// TODO(burdon): Factor out as extension/hook.
|
|
113
|
+
useComments(view, comments);
|
|
100
114
|
|
|
101
115
|
useEffect(() => {
|
|
102
|
-
if (!
|
|
116
|
+
if (!model || !rootRef.current) {
|
|
103
117
|
return;
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
// TODO(burdon): Remember cursor position.
|
|
107
120
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
108
121
|
const state = EditorState.create({
|
|
109
122
|
doc: model.text(),
|
|
123
|
+
// TODO(burdon): Composer should store and set selection when switching documents.
|
|
124
|
+
selection,
|
|
110
125
|
extensions: [
|
|
111
126
|
readonly && EditorState.readOnly.of(readonly),
|
|
112
127
|
|
|
113
|
-
// TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
|
|
114
|
-
editorMode === 'vim' && vim(),
|
|
115
|
-
|
|
116
128
|
// Theme.
|
|
117
129
|
// TODO(burdon): Make theme configurable.
|
|
118
130
|
EditorView.baseTheme(defaultTheme),
|
|
119
|
-
EditorView.theme(
|
|
131
|
+
EditorView.theme(theme ?? {}),
|
|
120
132
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
121
133
|
|
|
122
|
-
//
|
|
134
|
+
// TODO(burdon): Factor out VIM mode? (manage via MarkdownPlugin).
|
|
135
|
+
editorMode === 'vim' && vim(),
|
|
136
|
+
|
|
137
|
+
// Storage and replication (NOTE: must come before other extensions).
|
|
123
138
|
model.extension,
|
|
124
139
|
|
|
125
140
|
// Custom.
|
|
@@ -131,36 +146,35 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
131
146
|
// If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
|
|
132
147
|
// Ideally this should not happen except when changing between text objects.
|
|
133
148
|
view?.destroy();
|
|
134
|
-
|
|
149
|
+
const newView = new EditorView({
|
|
150
|
+
parent: rootRef.current,
|
|
135
151
|
state,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}),
|
|
152
|
+
// NOTE: Uncomment to spy on all transactions.
|
|
153
|
+
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
154
|
+
dispatch: (transaction, view) => {
|
|
155
|
+
view.update([transaction]);
|
|
156
|
+
|
|
157
|
+
// TODO(burdon): Is there a place to register this?
|
|
158
|
+
view.state.facet(syncFacet)?.reconcile(view);
|
|
159
|
+
},
|
|
145
160
|
});
|
|
146
161
|
|
|
162
|
+
setView(newView);
|
|
147
163
|
return () => {
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
newView?.destroy();
|
|
165
|
+
setView(null);
|
|
150
166
|
};
|
|
151
|
-
}, [
|
|
167
|
+
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
152
168
|
|
|
169
|
+
// TODO(burdon): Replace with keymap?
|
|
153
170
|
const handleKeyUp = useCallback(
|
|
154
171
|
(event: KeyboardEvent) => {
|
|
155
172
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
156
173
|
switch (key) {
|
|
157
|
-
case 'Enter': {
|
|
158
|
-
view?.contentDOM.focus();
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
174
|
case 'Escape': {
|
|
163
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
175
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
176
|
+
rootRef.current?.focus();
|
|
177
|
+
}
|
|
164
178
|
break;
|
|
165
179
|
}
|
|
166
180
|
}
|
|
@@ -171,42 +185,46 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
171
185
|
return (
|
|
172
186
|
<div
|
|
173
187
|
key={model.id}
|
|
174
|
-
|
|
188
|
+
role='none'
|
|
175
189
|
tabIndex={0}
|
|
176
|
-
{...slots?.root}
|
|
177
|
-
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
178
190
|
onKeyUp={handleKeyUp}
|
|
191
|
+
{...slots.root}
|
|
192
|
+
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
193
|
+
ref={rootRef}
|
|
179
194
|
/>
|
|
180
195
|
);
|
|
181
196
|
},
|
|
182
197
|
);
|
|
183
198
|
|
|
184
|
-
export const TextEditor = forwardRef<
|
|
185
|
-
({ readonly, extensions = [],
|
|
199
|
+
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
200
|
+
({ readonly, placeholder, extensions = [], theme = textTheme, slots, ...props }, forwardedRef) => {
|
|
186
201
|
const { themeMode } = useThemeContext();
|
|
187
|
-
const
|
|
202
|
+
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
188
203
|
return (
|
|
189
204
|
<BaseTextEditor
|
|
190
205
|
ref={forwardedRef}
|
|
191
206
|
readonly={readonly}
|
|
192
|
-
extensions={[basicBundle({ readonly, themeMode, placeholder
|
|
193
|
-
|
|
207
|
+
extensions={[basicBundle({ readonly, themeMode, placeholder }), ...extensions]}
|
|
208
|
+
theme={theme}
|
|
209
|
+
slots={updatedSlots}
|
|
194
210
|
{...props}
|
|
195
211
|
/>
|
|
196
212
|
);
|
|
197
213
|
},
|
|
198
214
|
);
|
|
199
215
|
|
|
200
|
-
|
|
201
|
-
|
|
216
|
+
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
217
|
+
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
218
|
+
({ readonly, placeholder, extensions = [], theme = markdownTheme, slots, ...props }, forwardedRef) => {
|
|
202
219
|
const { themeMode } = useThemeContext();
|
|
203
|
-
const
|
|
220
|
+
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
204
221
|
return (
|
|
205
222
|
<BaseTextEditor
|
|
206
223
|
ref={forwardedRef}
|
|
207
224
|
readonly={readonly}
|
|
208
|
-
extensions={[markdownBundle({ readonly, themeMode, placeholder
|
|
209
|
-
|
|
225
|
+
extensions={[markdownBundle({ readonly, themeMode, placeholder }), ...extensions]}
|
|
226
|
+
theme={theme}
|
|
227
|
+
slots={updatedSlots}
|
|
210
228
|
{...props}
|
|
211
229
|
/>
|
|
212
230
|
);
|
|
@@ -215,20 +233,14 @@ export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
215
233
|
|
|
216
234
|
export const defaultSlots: TextEditorSlots = {
|
|
217
235
|
root: {
|
|
218
|
-
className: mx('p-2', inputSurface),
|
|
236
|
+
className: mx('w-full p-2 overflow-y-auto', inputSurface),
|
|
219
237
|
},
|
|
220
238
|
};
|
|
221
239
|
|
|
222
240
|
export const defaultTextSlots: TextEditorSlots = {
|
|
223
241
|
...defaultSlots,
|
|
224
|
-
editor: {
|
|
225
|
-
theme: textTheme,
|
|
226
|
-
},
|
|
227
242
|
};
|
|
228
243
|
|
|
229
244
|
export const defaultMarkdownSlots: TextEditorSlots = {
|
|
230
245
|
...defaultSlots,
|
|
231
|
-
editor: {
|
|
232
|
-
theme: markdownTheme,
|
|
233
|
-
},
|
|
234
246
|
};
|