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