@dxos/react-ui-editor 0.3.11-main.24b414e → 0.3.11-main.264ec26
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 +607 -471
- 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 -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 +29 -16
- 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 +1 -5
- 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 +2 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- 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 +31 -31
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +64 -69
- package/src/components/TextEditor/TextEditor.tsx +85 -68
- package/src/components/TextEditor/comments.stories.tsx +111 -69
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.ts +23 -48
- 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 +235 -132
- 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 +49 -33
- 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 +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.active?.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,27 @@ 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
56
|
model: EditorModel;
|
|
52
|
-
focus?: boolean;
|
|
53
|
-
selection?: { anchor: number; head?: number };
|
|
54
57
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
autofocus?: boolean;
|
|
59
|
+
multiline?: 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[];
|
|
61
67
|
};
|
|
62
68
|
|
|
63
69
|
/**
|
|
@@ -65,108 +71,115 @@ export type TextEditorProps = {
|
|
|
65
71
|
*/
|
|
66
72
|
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
67
73
|
(
|
|
68
|
-
{ model,
|
|
74
|
+
{ model, readonly, autofocus, scrollTo, selection, editorMode, theme, slots = defaultSlots, extensions = [] },
|
|
69
75
|
forwardedRef,
|
|
70
76
|
) => {
|
|
71
77
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
72
78
|
const { themeMode } = useThemeContext();
|
|
73
79
|
|
|
74
|
-
// The editor view ref should only be used as an escape hatch.
|
|
75
80
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
76
81
|
const [view, setView] = useState<EditorView | null>(null);
|
|
77
|
-
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
78
82
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
view.focus();
|
|
83
|
-
}
|
|
84
|
-
}, [view, focus]);
|
|
83
|
+
// The view ref can be used to focus the editor.
|
|
84
|
+
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
85
|
+
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
85
86
|
|
|
86
|
-
//
|
|
87
|
-
const { awareness, peer } = model;
|
|
87
|
+
// Set focus.
|
|
88
88
|
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
|
-
});
|
|
89
|
+
if (autofocus) {
|
|
90
|
+
view?.focus();
|
|
95
91
|
}
|
|
96
|
-
}, [
|
|
92
|
+
}, [view, autofocus]);
|
|
97
93
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
if (view && comments !== undefined) {
|
|
101
|
-
view.dispatch({
|
|
102
|
-
effects: setCommentRange.of({ model, comments }),
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}, [view, comments]);
|
|
94
|
+
// Monitor awareness.
|
|
95
|
+
useAwareness(model);
|
|
106
96
|
|
|
97
|
+
// Create editor state and view.
|
|
98
|
+
// The view is recreated if the model or extensions are changed.
|
|
107
99
|
useEffect(() => {
|
|
108
100
|
if (!model || !rootRef.current) {
|
|
109
101
|
return;
|
|
110
102
|
}
|
|
111
103
|
|
|
104
|
+
//
|
|
105
|
+
// EditorState
|
|
112
106
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
107
|
+
//
|
|
113
108
|
const state = EditorState.create({
|
|
114
109
|
doc: model.text(),
|
|
115
|
-
// TODO(burdon): Composer should store and set selection when switching documents.
|
|
116
110
|
selection,
|
|
117
111
|
extensions: [
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
112
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
113
|
+
EditorView.exceptionSink.of((err) => {
|
|
114
|
+
log.catch(err);
|
|
115
|
+
}),
|
|
122
116
|
|
|
123
117
|
// Theme.
|
|
124
|
-
// TODO(burdon): Make
|
|
118
|
+
// TODO(burdon): Make configurable.
|
|
125
119
|
EditorView.baseTheme(defaultTheme),
|
|
126
120
|
EditorView.theme(theme ?? {}),
|
|
127
121
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
122
|
+
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
123
|
+
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
124
|
+
|
|
125
|
+
// State.
|
|
126
|
+
EditorState.readOnly.of(!!readonly),
|
|
128
127
|
|
|
129
128
|
// Storage and replication.
|
|
129
|
+
// NOTE: This must come before user extensions.
|
|
130
130
|
model.extension,
|
|
131
131
|
|
|
132
|
+
// TODO(burdon): Factor out (requires special handling for Escape/focus).
|
|
133
|
+
editorMode === 'vim' && vim(),
|
|
134
|
+
|
|
132
135
|
// Custom.
|
|
133
136
|
...extensions,
|
|
134
137
|
].filter(Boolean) as Extension[],
|
|
135
138
|
});
|
|
136
139
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
140
|
+
//
|
|
141
|
+
// EditorView
|
|
142
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
143
|
+
//
|
|
141
144
|
const newView = new EditorView({
|
|
142
|
-
parent: rootRef.current,
|
|
143
145
|
state,
|
|
144
|
-
|
|
146
|
+
parent: rootRef.current,
|
|
147
|
+
scrollTo,
|
|
148
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
145
149
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
dispatchTransactions: (trs, view) => {
|
|
151
|
+
view.update(trs);
|
|
152
|
+
const debug = false;
|
|
153
|
+
if (debug) {
|
|
154
|
+
logChanges(trs);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
149
157
|
});
|
|
150
158
|
|
|
159
|
+
view?.destroy();
|
|
151
160
|
setView(newView);
|
|
161
|
+
|
|
152
162
|
return () => {
|
|
153
163
|
newView?.destroy();
|
|
154
164
|
setView(null);
|
|
155
165
|
};
|
|
156
166
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
157
167
|
|
|
168
|
+
// Handles tab/focus.
|
|
169
|
+
// Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
|
|
158
170
|
const handleKeyUp = useCallback(
|
|
159
171
|
(event: KeyboardEvent) => {
|
|
160
172
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
161
173
|
switch (key) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// }
|
|
174
|
+
case 'Enter': {
|
|
175
|
+
view?.focus();
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
167
178
|
|
|
168
179
|
case 'Escape': {
|
|
169
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
180
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
181
|
+
rootRef.current?.focus();
|
|
182
|
+
}
|
|
170
183
|
break;
|
|
171
184
|
}
|
|
172
185
|
}
|
|
@@ -178,25 +191,26 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
178
191
|
<div
|
|
179
192
|
key={model.id}
|
|
180
193
|
role='none'
|
|
181
|
-
ref={rootRef}
|
|
182
194
|
tabIndex={0}
|
|
183
195
|
onKeyUp={handleKeyUp}
|
|
184
196
|
{...slots.root}
|
|
185
197
|
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
198
|
+
ref={rootRef}
|
|
186
199
|
/>
|
|
187
200
|
);
|
|
188
201
|
},
|
|
189
202
|
);
|
|
190
203
|
|
|
204
|
+
// TODO(burdon): Single-line/scroll.
|
|
191
205
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
192
|
-
({ readonly, placeholder,
|
|
206
|
+
({ readonly, placeholder, multiline, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
193
207
|
const { themeMode } = useThemeContext();
|
|
194
208
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
195
209
|
return (
|
|
196
210
|
<BaseTextEditor
|
|
197
211
|
ref={forwardedRef}
|
|
198
212
|
readonly={readonly}
|
|
199
|
-
extensions={[basicBundle({
|
|
213
|
+
extensions={[basicBundle({ themeMode, placeholder, multiline }), ...extensions]}
|
|
200
214
|
theme={theme}
|
|
201
215
|
slots={updatedSlots}
|
|
202
216
|
{...props}
|
|
@@ -205,16 +219,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
205
219
|
},
|
|
206
220
|
);
|
|
207
221
|
|
|
208
|
-
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
209
222
|
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
210
|
-
({ readonly, placeholder,
|
|
223
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
211
224
|
const { themeMode } = useThemeContext();
|
|
212
225
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
213
226
|
return (
|
|
214
227
|
<BaseTextEditor
|
|
215
228
|
ref={forwardedRef}
|
|
216
229
|
readonly={readonly}
|
|
217
|
-
extensions={[markdownBundle({
|
|
230
|
+
extensions={[markdownBundle({ themeMode, readonly, placeholder }), ...extensions]}
|
|
218
231
|
theme={theme}
|
|
219
232
|
slots={updatedSlots}
|
|
220
233
|
{...props}
|
|
@@ -225,7 +238,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
225
238
|
|
|
226
239
|
export const defaultSlots: TextEditorSlots = {
|
|
227
240
|
root: {
|
|
228
|
-
|
|
241
|
+
// TODO(burdon): Add focusRing by default/as property?
|
|
242
|
+
className: mx('flex flex-col grow overflow-y-auto', inputSurface),
|
|
243
|
+
},
|
|
244
|
+
editor: {
|
|
245
|
+
className: 'h-full p-2',
|
|
229
246
|
},
|
|
230
247
|
};
|
|
231
248
|
|