@dxos/react-ui-editor 0.3.11-main.c8e9429 → 0.3.11-main.cc2fe2c
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 +1020 -1022
- 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 +13 -14
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +13 -5
- 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/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/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 +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/awareness.d.ts +1 -1
- 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 +18 -14
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +3 -3
- package/dist/types/src/extensions/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -7
- 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/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 +7 -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/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- 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 +25 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- 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/useTextEditor.d.ts +17 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +2 -27
- 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 +31 -34
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +69 -72
- package/src/components/TextEditor/TextEditor.tsx +91 -66
- package/src/components/TextEditor/comments.stories.tsx +45 -43
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.ts +35 -39
- package/src/extensions/automerge/defs.ts +8 -33
- package/src/extensions/automerge/semaphore.ts +17 -12
- package/src/extensions/automerge/update-automerge.ts +2 -2
- package/src/extensions/awareness.ts +14 -14
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/comments.ts +161 -141
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/index.ts +1 -7
- package/src/extensions/markdown/bundle.ts +20 -30
- package/src/extensions/markdown/code.ts +127 -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 +7 -0
- package/src/extensions/{link.ts → markdown/link.ts} +1 -2
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
- package/src/extensions/mention.ts +9 -2
- package/src/extensions/outliner.ts +12 -0
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +2 -2
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +47 -56
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +0 -2
- 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/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 -3
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -27
- 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/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 -52
- package/src/hooks/yjs.ts +0 -156
- 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
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
|
-
import { EditorView } from '@codemirror/view';
|
|
7
|
+
import { type 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, useState } from 'react';
|
|
11
|
+
import React, { type FC, StrictMode, useRef, useState } from 'react';
|
|
12
12
|
import { createRoot } from 'react-dom/client';
|
|
13
13
|
|
|
14
14
|
import { TextObject } from '@dxos/echo-schema';
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
defaultOptions,
|
|
27
27
|
hr,
|
|
28
28
|
image,
|
|
29
|
-
listener,
|
|
30
29
|
link,
|
|
31
30
|
mention,
|
|
32
31
|
table,
|
|
@@ -34,8 +33,10 @@ import {
|
|
|
34
33
|
typewriter,
|
|
35
34
|
type CommentsOptions,
|
|
36
35
|
type LinkOptions,
|
|
36
|
+
useComments,
|
|
37
|
+
heading,
|
|
37
38
|
} from '../../extensions';
|
|
38
|
-
import { type
|
|
39
|
+
import { type Comment, useTextModel } from '../../hooks';
|
|
39
40
|
|
|
40
41
|
// Extensions:
|
|
41
42
|
// TODO(burdon): Table of contents.
|
|
@@ -55,8 +56,8 @@ const text = {
|
|
|
55
56
|
'- [x] decorator',
|
|
56
57
|
'- [ ] checkbox',
|
|
57
58
|
' - [ ] state',
|
|
58
|
-
'
|
|
59
|
-
'
|
|
59
|
+
' - [ ] indent',
|
|
60
|
+
' - [x] style',
|
|
60
61
|
'',
|
|
61
62
|
),
|
|
62
63
|
|
|
@@ -82,7 +83,7 @@ const text = {
|
|
|
82
83
|
'## Code',
|
|
83
84
|
'',
|
|
84
85
|
'```',
|
|
85
|
-
'
|
|
86
|
+
'$ ls -las',
|
|
86
87
|
'```',
|
|
87
88
|
'',
|
|
88
89
|
'```tsx',
|
|
@@ -162,6 +163,8 @@ const links = [
|
|
|
162
163
|
{ label: 'StackEdit', apply: '[StackEdit](https://stackedit.io/app)' },
|
|
163
164
|
];
|
|
164
165
|
|
|
166
|
+
const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
|
|
167
|
+
|
|
165
168
|
const hover =
|
|
166
169
|
'rounded-sm text-base text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
|
|
167
170
|
|
|
@@ -210,12 +213,15 @@ const onRenderLink: LinkOptions['onRender'] = (el, url) => {
|
|
|
210
213
|
|
|
211
214
|
type StoryProps = {
|
|
212
215
|
text?: string;
|
|
216
|
+
comments?: Comment[];
|
|
213
217
|
automerge?: boolean;
|
|
214
|
-
} & Pick<TextEditorProps, '
|
|
218
|
+
} & Pick<TextEditorProps, 'readonly' | 'placeholder' | 'slots' | 'extensions'>;
|
|
215
219
|
|
|
216
|
-
const Story = ({ text, automerge, ...props }: StoryProps) => {
|
|
220
|
+
const Story = ({ text, comments, automerge, placeholder = 'New document.', ...props }: StoryProps) => {
|
|
217
221
|
const [item] = useState({ text: new TextObject(text, undefined, undefined, { automerge }) });
|
|
222
|
+
const view = useRef<EditorView>(null);
|
|
218
223
|
const model = useTextModel({ text: item.text });
|
|
224
|
+
useComments(view.current, comments);
|
|
219
225
|
if (!model) {
|
|
220
226
|
return null;
|
|
221
227
|
}
|
|
@@ -224,7 +230,7 @@ const Story = ({ text, automerge, ...props }: StoryProps) => {
|
|
|
224
230
|
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
225
231
|
<div className='flex h-full justify-center'>
|
|
226
232
|
<div className='flex flex-col h-full w-[800px]'>
|
|
227
|
-
<MarkdownEditor model={model} {...props} />
|
|
233
|
+
<MarkdownEditor ref={view} model={model} placeholder={placeholder} {...props} />
|
|
228
234
|
</div>
|
|
229
235
|
</div>
|
|
230
236
|
</div>
|
|
@@ -238,48 +244,42 @@ export default {
|
|
|
238
244
|
render: Story,
|
|
239
245
|
};
|
|
240
246
|
|
|
241
|
-
const
|
|
247
|
+
const defaults = [
|
|
242
248
|
autocomplete({
|
|
243
249
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
244
250
|
}),
|
|
245
251
|
code(),
|
|
252
|
+
heading(),
|
|
246
253
|
hr(),
|
|
247
254
|
image(),
|
|
248
255
|
link({ onRender: onRenderLink, onHover: onHoverLinkTooltip }),
|
|
256
|
+
// mention({
|
|
257
|
+
// onSearch: (text) => names.filter((name) => name.toLowerCase().startsWith(text.toLowerCase())),
|
|
258
|
+
// }),
|
|
249
259
|
table(),
|
|
250
260
|
tasklist(),
|
|
251
261
|
];
|
|
252
262
|
|
|
253
263
|
export const Default = {
|
|
254
|
-
render: () => <Story text={document} extensions={
|
|
264
|
+
render: () => <Story text={document} extensions={defaults} />,
|
|
255
265
|
};
|
|
256
266
|
|
|
257
267
|
export const Readonly = {
|
|
258
|
-
render: () => <Story text={document} extensions={
|
|
268
|
+
render: () => <Story text={document} extensions={defaults} readonly />,
|
|
259
269
|
};
|
|
260
270
|
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
export const Large = {
|
|
264
|
-
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
271
|
+
export const NoExtensions = {
|
|
272
|
+
render: () => <Story text={document} />,
|
|
265
273
|
};
|
|
266
274
|
|
|
275
|
+
const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
|
|
276
|
+
|
|
267
277
|
export const Empty = {
|
|
268
278
|
render: () => <Story />,
|
|
269
279
|
};
|
|
270
280
|
|
|
271
|
-
export const
|
|
272
|
-
render: () => <Story text={
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
// TODO(burdon): Cursor bug if no newline after BR (cursor down just loops back to start of each paragraph).
|
|
276
|
-
export const HorizontalRule = {
|
|
277
|
-
render: () => (
|
|
278
|
-
<Story
|
|
279
|
-
text={str('# Horizontal Rule', '', text.paragraphs, '---', '', text.paragraphs, '---', '', text.paragraphs)}
|
|
280
|
-
extensions={[hr()]}
|
|
281
|
-
/>
|
|
282
|
-
),
|
|
281
|
+
export const Scrolling = {
|
|
282
|
+
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
283
283
|
};
|
|
284
284
|
|
|
285
285
|
export const Links = {
|
|
@@ -292,37 +292,36 @@ export const Links = {
|
|
|
292
292
|
};
|
|
293
293
|
|
|
294
294
|
export const Code = {
|
|
295
|
-
render: () => <Story text={str(text.code, text.footer)} extensions={[code()]}
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
export const Table = {
|
|
299
|
-
render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
|
|
295
|
+
render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} />,
|
|
300
296
|
};
|
|
301
297
|
|
|
302
298
|
export const Image = {
|
|
303
|
-
render: () => <Story text={str(text.image, text.footer)}
|
|
299
|
+
render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
|
|
304
300
|
};
|
|
305
301
|
|
|
306
|
-
export const
|
|
302
|
+
export const Lists = {
|
|
307
303
|
render: () => (
|
|
308
|
-
<Story
|
|
309
|
-
text={str(text.tasks, '', text.list, text.footer)}
|
|
310
|
-
extensions={[
|
|
311
|
-
tasklist(),
|
|
312
|
-
listener({
|
|
313
|
-
onChange: (text) => {
|
|
314
|
-
console.log(text);
|
|
315
|
-
},
|
|
316
|
-
}),
|
|
317
|
-
]}
|
|
318
|
-
/>
|
|
304
|
+
<Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[tasklist()]} />
|
|
319
305
|
),
|
|
320
306
|
};
|
|
321
307
|
|
|
308
|
+
export const Table = {
|
|
309
|
+
render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// export const Outliner = {
|
|
313
|
+
// render: () => (
|
|
314
|
+
// <Story
|
|
315
|
+
// text={str('# Outliner', '', 'Block', ': this is a block', ': with multiple lines', text.footer)}
|
|
316
|
+
// extensions={[outliner()]}
|
|
317
|
+
// />
|
|
318
|
+
// ),
|
|
319
|
+
// };
|
|
320
|
+
|
|
322
321
|
export const Autocomplete = {
|
|
323
322
|
render: () => (
|
|
324
323
|
<Story
|
|
325
|
-
text={str('# Autocomplete', '', 'Press
|
|
324
|
+
text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
|
|
326
325
|
extensions={[
|
|
327
326
|
link({ onRender: onRenderLink }),
|
|
328
327
|
autocomplete({
|
|
@@ -333,8 +332,6 @@ export const Autocomplete = {
|
|
|
333
332
|
),
|
|
334
333
|
};
|
|
335
334
|
|
|
336
|
-
const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
|
|
337
|
-
|
|
338
335
|
export const Mention = {
|
|
339
336
|
render: () => (
|
|
340
337
|
<Story
|
|
@@ -350,7 +347,7 @@ export const Mention = {
|
|
|
350
347
|
|
|
351
348
|
export const Comments = {
|
|
352
349
|
render: () => {
|
|
353
|
-
const [commentRanges, setCommentRanges] = useState<
|
|
350
|
+
const [commentRanges, setCommentRanges] = useState<Comment[]>([]);
|
|
354
351
|
|
|
355
352
|
return (
|
|
356
353
|
<Story
|
|
@@ -370,9 +367,9 @@ export const Comments = {
|
|
|
370
367
|
console.log(
|
|
371
368
|
'update',
|
|
372
369
|
JSON.stringify({
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
370
|
+
comments: state.comments.length,
|
|
371
|
+
active: state.selection.current?.slice(0, 8),
|
|
372
|
+
closest: state.selection.closest?.slice(0, 8),
|
|
376
373
|
}),
|
|
377
374
|
);
|
|
378
375
|
}
|
|
@@ -384,8 +381,24 @@ export const Comments = {
|
|
|
384
381
|
},
|
|
385
382
|
};
|
|
386
383
|
|
|
384
|
+
export const HorizontalRule = {
|
|
385
|
+
render: () => (
|
|
386
|
+
<Story
|
|
387
|
+
text={str('# Horizontal Rule', '', text.paragraphs, '---', text.paragraphs, '---', text.paragraphs)}
|
|
388
|
+
extensions={[hr()]}
|
|
389
|
+
/>
|
|
390
|
+
),
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
|
|
394
|
+
|
|
387
395
|
export const Typewriter = {
|
|
388
|
-
render: () =>
|
|
396
|
+
render: () => (
|
|
397
|
+
<Story
|
|
398
|
+
text={str('# Typewriter', '', text.paragraphs, text.footer)}
|
|
399
|
+
extensions={[typewriter({ items: typewriterItems })]}
|
|
400
|
+
/>
|
|
401
|
+
),
|
|
389
402
|
};
|
|
390
403
|
|
|
391
404
|
export const Blast = {
|
|
@@ -393,9 +406,7 @@ export const Blast = {
|
|
|
393
406
|
<Story
|
|
394
407
|
text={str('# Blast', '', text.paragraphs, text.code, text.paragraphs)}
|
|
395
408
|
extensions={[
|
|
396
|
-
typewriter({
|
|
397
|
-
items: localStorage.getItem('dxos.composer.extension.demo')?.split(','),
|
|
398
|
-
}),
|
|
409
|
+
typewriter({ items: typewriterItems }),
|
|
399
410
|
blast(
|
|
400
411
|
defaultsDeep(
|
|
401
412
|
{
|
|
@@ -412,17 +423,3 @@ export const Blast = {
|
|
|
412
423
|
/>
|
|
413
424
|
),
|
|
414
425
|
};
|
|
415
|
-
|
|
416
|
-
export const Diagnostics = {
|
|
417
|
-
render: () => (
|
|
418
|
-
<Story
|
|
419
|
-
text={document}
|
|
420
|
-
extensions={[
|
|
421
|
-
// Cursor moved.
|
|
422
|
-
EditorView.updateListener.of((update) => {
|
|
423
|
-
console.log('update', update.view.state.selection.main.head);
|
|
424
|
-
}),
|
|
425
|
-
]}
|
|
426
|
-
/>
|
|
427
|
-
),
|
|
428
|
-
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { EditorState, type Extension } from '@codemirror/state';
|
|
5
|
+
import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
|
|
6
6
|
import { EditorView } from '@codemirror/view';
|
|
7
7
|
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
8
|
import { vim } from '@replit/codemirror-vim';
|
|
@@ -14,19 +14,21 @@ import React, {
|
|
|
14
14
|
useCallback,
|
|
15
15
|
useEffect,
|
|
16
16
|
useImperativeHandle,
|
|
17
|
-
useState,
|
|
18
17
|
useRef,
|
|
18
|
+
useState,
|
|
19
19
|
} from 'react';
|
|
20
20
|
|
|
21
|
-
import {
|
|
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
|
|
26
|
-
import { type
|
|
25
|
+
import { basicBundle, markdownBundle } from '../../extensions';
|
|
26
|
+
import { type EditorModel } from '../../hooks';
|
|
27
27
|
import { type ThemeStyles } from '../../styles';
|
|
28
28
|
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
29
|
+
import { logChanges } from '../../util';
|
|
29
30
|
|
|
31
|
+
// TODO(burdon): Change to enum?
|
|
30
32
|
export const EditorModes = ['default', 'vim'] as const;
|
|
31
33
|
export type EditorMode = (typeof EditorModes)[number];
|
|
32
34
|
|
|
@@ -41,23 +43,28 @@ export type CursorInfo = {
|
|
|
41
43
|
|
|
42
44
|
export type TextEditorSlots = {
|
|
43
45
|
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
editor?: {
|
|
47
|
+
className?: string;
|
|
48
|
+
};
|
|
49
|
+
content?: {
|
|
50
|
+
className?: string;
|
|
51
|
+
};
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
// TODO(burdon): Spellcheck?
|
|
50
55
|
export type TextEditorProps = {
|
|
51
|
-
model: EditorModel;
|
|
52
|
-
autofocus?: boolean;
|
|
56
|
+
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
53
57
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
58
|
+
autoFocus?: boolean;
|
|
59
|
+
lineWrapping?: boolean;
|
|
60
|
+
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
54
61
|
selection?: { anchor: number; head?: number };
|
|
55
|
-
|
|
56
|
-
extensions?: Extension[];
|
|
57
|
-
editorMode?: EditorMode;
|
|
62
|
+
editorMode?: EditorMode; // TODO(burdon): Factor out.
|
|
58
63
|
placeholder?: string;
|
|
59
64
|
theme?: ThemeStyles;
|
|
60
65
|
slots?: TextEditorSlots;
|
|
66
|
+
extensions?: Extension[];
|
|
67
|
+
debug?: boolean;
|
|
61
68
|
};
|
|
62
69
|
|
|
63
70
|
/**
|
|
@@ -65,108 +72,122 @@ export type TextEditorProps = {
|
|
|
65
72
|
*/
|
|
66
73
|
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
67
74
|
(
|
|
68
|
-
{
|
|
75
|
+
{
|
|
76
|
+
model,
|
|
77
|
+
readonly,
|
|
78
|
+
autoFocus,
|
|
79
|
+
scrollTo,
|
|
80
|
+
selection,
|
|
81
|
+
editorMode,
|
|
82
|
+
theme,
|
|
83
|
+
slots = defaultSlots,
|
|
84
|
+
extensions = [],
|
|
85
|
+
debug,
|
|
86
|
+
},
|
|
69
87
|
forwardedRef,
|
|
70
88
|
) => {
|
|
71
89
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
72
90
|
const { themeMode } = useThemeContext();
|
|
73
91
|
|
|
74
|
-
// The editor view ref should only be used as an escape hatch.
|
|
75
92
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
76
93
|
const [view, setView] = useState<EditorView | null>(null);
|
|
94
|
+
|
|
95
|
+
// The view ref can be used to focus the editor.
|
|
77
96
|
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
78
97
|
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
79
98
|
|
|
99
|
+
// Set focus.
|
|
80
100
|
useEffect(() => {
|
|
81
|
-
if (
|
|
101
|
+
if (autoFocus) {
|
|
82
102
|
view?.focus();
|
|
83
103
|
}
|
|
84
|
-
}, [
|
|
85
|
-
|
|
86
|
-
// TODO(burdon): Factor out as extension/hook.
|
|
87
|
-
const { awareness, peer } = model;
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (awareness && peer) {
|
|
90
|
-
awareness.setLocalStateField('user', {
|
|
91
|
-
name: peer.name ?? generateName(peer.id),
|
|
92
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
93
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}, [awareness, peer, themeMode]);
|
|
97
|
-
|
|
98
|
-
// TODO(burdon): Factor out as extension/hook.
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
if (view && comments !== undefined) {
|
|
101
|
-
view.dispatch({
|
|
102
|
-
effects: setCommentRange.of({ model, comments }),
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}, [view, comments]);
|
|
104
|
+
}, [view, autoFocus]);
|
|
106
105
|
|
|
106
|
+
// Create editor state and view.
|
|
107
|
+
// The view is recreated if the model or extensions are changed.
|
|
107
108
|
useEffect(() => {
|
|
108
109
|
if (!model || !rootRef.current) {
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
//
|
|
114
|
+
// EditorState
|
|
112
115
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
116
|
+
//
|
|
113
117
|
const state = EditorState.create({
|
|
114
118
|
doc: model.text(),
|
|
115
|
-
// TODO(burdon): Composer should store and set selection when switching documents.
|
|
116
119
|
selection,
|
|
117
120
|
extensions: [
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
122
|
+
EditorView.exceptionSink.of((err) => {
|
|
123
|
+
log.catch(err);
|
|
124
|
+
}),
|
|
122
125
|
|
|
123
126
|
// Theme.
|
|
124
|
-
// TODO(burdon): Make
|
|
127
|
+
// TODO(burdon): Make configurable.
|
|
125
128
|
EditorView.baseTheme(defaultTheme),
|
|
126
129
|
EditorView.theme(theme ?? {}),
|
|
127
130
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
131
|
+
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
132
|
+
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
133
|
+
|
|
134
|
+
// State.
|
|
135
|
+
EditorState.readOnly.of(!!readonly),
|
|
128
136
|
|
|
129
137
|
// Storage and replication.
|
|
138
|
+
// NOTE: This must come before user extensions.
|
|
130
139
|
model.extension,
|
|
131
140
|
|
|
141
|
+
// TODO(burdon): Factor out (requires special handling for Escape/focus).
|
|
142
|
+
editorMode === 'vim' && vim(),
|
|
143
|
+
|
|
132
144
|
// Custom.
|
|
133
145
|
...extensions,
|
|
134
146
|
].filter(Boolean) as Extension[],
|
|
135
147
|
});
|
|
136
148
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
149
|
+
//
|
|
150
|
+
// EditorView
|
|
151
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
152
|
+
//
|
|
141
153
|
const newView = new EditorView({
|
|
142
|
-
parent: rootRef.current,
|
|
143
154
|
state,
|
|
144
|
-
|
|
155
|
+
parent: rootRef.current,
|
|
156
|
+
scrollTo,
|
|
157
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
145
158
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
159
|
+
dispatchTransactions: (trs, view) => {
|
|
160
|
+
if (debug) {
|
|
161
|
+
logChanges(trs);
|
|
162
|
+
}
|
|
163
|
+
view.update(trs);
|
|
164
|
+
},
|
|
149
165
|
});
|
|
150
166
|
|
|
167
|
+
view?.destroy();
|
|
151
168
|
setView(newView);
|
|
169
|
+
|
|
152
170
|
return () => {
|
|
153
171
|
newView?.destroy();
|
|
154
172
|
setView(null);
|
|
155
173
|
};
|
|
156
174
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
157
175
|
|
|
176
|
+
// Handles tab/focus.
|
|
177
|
+
// Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
|
|
158
178
|
const handleKeyUp = useCallback(
|
|
159
179
|
(event: KeyboardEvent) => {
|
|
160
180
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
161
181
|
switch (key) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// }
|
|
182
|
+
case 'Enter': {
|
|
183
|
+
view?.focus();
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
167
186
|
|
|
168
187
|
case 'Escape': {
|
|
169
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
188
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
189
|
+
rootRef.current?.focus();
|
|
190
|
+
}
|
|
170
191
|
break;
|
|
171
192
|
}
|
|
172
193
|
}
|
|
@@ -188,15 +209,16 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
188
209
|
},
|
|
189
210
|
);
|
|
190
211
|
|
|
212
|
+
// TODO(burdon): Single-line/scroll.
|
|
191
213
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
192
|
-
({ readonly, placeholder,
|
|
214
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
193
215
|
const { themeMode } = useThemeContext();
|
|
194
216
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
195
217
|
return (
|
|
196
218
|
<BaseTextEditor
|
|
197
219
|
ref={forwardedRef}
|
|
198
220
|
readonly={readonly}
|
|
199
|
-
extensions={[basicBundle({
|
|
221
|
+
extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
200
222
|
theme={theme}
|
|
201
223
|
slots={updatedSlots}
|
|
202
224
|
{...props}
|
|
@@ -205,16 +227,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
205
227
|
},
|
|
206
228
|
);
|
|
207
229
|
|
|
208
|
-
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
209
230
|
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
210
|
-
({ readonly, placeholder,
|
|
231
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
211
232
|
const { themeMode } = useThemeContext();
|
|
212
233
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
213
234
|
return (
|
|
214
235
|
<BaseTextEditor
|
|
215
236
|
ref={forwardedRef}
|
|
216
237
|
readonly={readonly}
|
|
217
|
-
extensions={[markdownBundle({
|
|
238
|
+
extensions={[markdownBundle({ themeMode, placeholder }), ...extensions]}
|
|
218
239
|
theme={theme}
|
|
219
240
|
slots={updatedSlots}
|
|
220
241
|
{...props}
|
|
@@ -225,7 +246,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
225
246
|
|
|
226
247
|
export const defaultSlots: TextEditorSlots = {
|
|
227
248
|
root: {
|
|
228
|
-
|
|
249
|
+
// TODO(burdon): Add focusRing by default/as property?
|
|
250
|
+
className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
|
|
251
|
+
},
|
|
252
|
+
editor: {
|
|
253
|
+
className: 'h-full p-2',
|
|
229
254
|
},
|
|
230
255
|
};
|
|
231
256
|
|