@dxos/react-ui-editor 0.3.11-main.d56f337 → 0.3.11-main.d8b8a39
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 +595 -665
- 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 +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/code.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +27 -18
- 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 -1
- 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/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/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 +64 -69
- package/src/components/TextEditor/TextEditor.tsx +95 -70
- package/src/components/TextEditor/comments.stories.tsx +104 -76
- 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 +15 -14
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/code.ts +15 -17
- package/src/extensions/comments.ts +199 -161
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/hr.ts +49 -50
- package/src/extensions/index.ts +1 -1
- 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/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +47 -56
- 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/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/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
|
@@ -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 { 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
|
-
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,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);
|
|
77
|
-
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
78
94
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
view.focus();
|
|
83
|
-
}
|
|
84
|
-
}, [view, focus]);
|
|
85
|
-
|
|
86
|
-
// TODO(burdon): Factor out as extension.
|
|
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]);
|
|
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]);
|
|
97
98
|
|
|
98
|
-
//
|
|
99
|
+
// Set focus.
|
|
99
100
|
useEffect(() => {
|
|
100
|
-
if (
|
|
101
|
-
view
|
|
102
|
-
effects: setCommentRange.of({ model, comments }),
|
|
103
|
-
});
|
|
101
|
+
if (autoFocus) {
|
|
102
|
+
view?.focus();
|
|
104
103
|
}
|
|
105
|
-
}, [view,
|
|
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
|
}
|
|
@@ -178,25 +199,26 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
178
199
|
<div
|
|
179
200
|
key={model.id}
|
|
180
201
|
role='none'
|
|
181
|
-
ref={rootRef}
|
|
182
202
|
tabIndex={0}
|
|
183
203
|
onKeyUp={handleKeyUp}
|
|
184
204
|
{...slots.root}
|
|
185
205
|
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
206
|
+
ref={rootRef}
|
|
186
207
|
/>
|
|
187
208
|
);
|
|
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, readonly, 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
|
|