@dxos/react-ui-editor 0.3.11-main.6fccc32 → 0.3.11-main.73ee990
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 +1259 -1060
- 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/components/Toolbar/Toolbar.d.ts +9 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +14 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- 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/automerge/update-automerge.d.ts.map +1 -1
- 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 +35 -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/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/formatting.d.ts +11 -0
- package/dist/types/src/extensions/markdown/formatting.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 +8 -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.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 +3 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
- 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 +34 -37
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +72 -80
- package/src/components/TextEditor/TextEditor.tsx +96 -70
- package/src/components/TextEditor/comments.stories.tsx +113 -71
- package/src/components/Toolbar/Toolbar.stories.tsx +86 -0
- package/src/components/Toolbar/Toolbar.tsx +115 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.ts +39 -46
- 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 +14 -8
- package/src/extensions/awareness.ts +15 -14
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/comments.ts +269 -162
- 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 +142 -0
- package/src/extensions/markdown/formatting.ts +159 -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 +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +8 -5
- package/src/extensions/markdown/tasklist.ts +110 -0
- 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 +4 -2
- package/src/hooks/useActionHandler.ts +45 -0
- 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 +1 -11
- 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.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 -106
- package/src/extensions/hr.ts +0 -73
- package/src/extensions/tasklist.ts +0 -119
- 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/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.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';
|
|
@@ -19,27 +19,25 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
19
19
|
|
|
20
20
|
import { MarkdownEditor, type TextEditorProps } from './TextEditor';
|
|
21
21
|
import {
|
|
22
|
+
type CommentsOptions,
|
|
23
|
+
type LinkOptions,
|
|
22
24
|
autocomplete,
|
|
23
25
|
blast,
|
|
24
26
|
code,
|
|
25
27
|
comments,
|
|
26
28
|
defaultOptions,
|
|
29
|
+
heading,
|
|
27
30
|
hr,
|
|
28
31
|
image,
|
|
29
|
-
listener,
|
|
30
32
|
link,
|
|
31
33
|
mention,
|
|
32
34
|
table,
|
|
33
35
|
tasklist,
|
|
34
36
|
typewriter,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
useComments,
|
|
38
|
+
formatting,
|
|
37
39
|
} from '../../extensions';
|
|
38
|
-
import { type
|
|
39
|
-
|
|
40
|
-
// Extensions:
|
|
41
|
-
// TODO(burdon): Table of contents.
|
|
42
|
-
// TODO(burdon): Front-matter
|
|
40
|
+
import { type Comment, useTextModel } from '../../hooks';
|
|
43
41
|
|
|
44
42
|
faker.seed(101);
|
|
45
43
|
|
|
@@ -55,8 +53,8 @@ const text = {
|
|
|
55
53
|
'- [x] decorator',
|
|
56
54
|
'- [ ] checkbox',
|
|
57
55
|
' - [ ] state',
|
|
58
|
-
'
|
|
59
|
-
'
|
|
56
|
+
' - [ ] indent',
|
|
57
|
+
' - [x] style',
|
|
60
58
|
'',
|
|
61
59
|
),
|
|
62
60
|
|
|
@@ -82,7 +80,7 @@ const text = {
|
|
|
82
80
|
'## Code',
|
|
83
81
|
'',
|
|
84
82
|
'```',
|
|
85
|
-
'
|
|
83
|
+
'$ ls -las',
|
|
86
84
|
'```',
|
|
87
85
|
'',
|
|
88
86
|
'```tsx',
|
|
@@ -162,6 +160,8 @@ const links = [
|
|
|
162
160
|
{ label: 'StackEdit', apply: '[StackEdit](https://stackedit.io/app)' },
|
|
163
161
|
];
|
|
164
162
|
|
|
163
|
+
const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
|
|
164
|
+
|
|
165
165
|
const hover =
|
|
166
166
|
'rounded-sm text-base text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
|
|
167
167
|
|
|
@@ -210,12 +210,15 @@ const onRenderLink: LinkOptions['onRender'] = (el, url) => {
|
|
|
210
210
|
|
|
211
211
|
type StoryProps = {
|
|
212
212
|
text?: string;
|
|
213
|
+
comments?: Comment[];
|
|
213
214
|
automerge?: boolean;
|
|
214
|
-
} & Pick<TextEditorProps, '
|
|
215
|
+
} & Pick<TextEditorProps, 'readonly' | 'placeholder' | 'slots' | 'extensions'>;
|
|
215
216
|
|
|
216
|
-
const Story = ({ text, automerge, ...props }: StoryProps) => {
|
|
217
|
+
const Story = ({ text, comments, automerge, placeholder = 'New document.', ...props }: StoryProps) => {
|
|
217
218
|
const [item] = useState({ text: new TextObject(text, undefined, undefined, { automerge }) });
|
|
219
|
+
const view = useRef<EditorView>(null);
|
|
218
220
|
const model = useTextModel({ text: item.text });
|
|
221
|
+
useComments(view.current, comments);
|
|
219
222
|
if (!model) {
|
|
220
223
|
return null;
|
|
221
224
|
}
|
|
@@ -224,7 +227,7 @@ const Story = ({ text, automerge, ...props }: StoryProps) => {
|
|
|
224
227
|
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
225
228
|
<div className='flex h-full justify-center'>
|
|
226
229
|
<div className='flex flex-col h-full w-[800px]'>
|
|
227
|
-
<MarkdownEditor model={model} {...props} />
|
|
230
|
+
<MarkdownEditor ref={view} model={model} placeholder={placeholder} {...props} />
|
|
228
231
|
</div>
|
|
229
232
|
</div>
|
|
230
233
|
</div>
|
|
@@ -238,11 +241,13 @@ export default {
|
|
|
238
241
|
render: Story,
|
|
239
242
|
};
|
|
240
243
|
|
|
241
|
-
const
|
|
244
|
+
const defaults = [
|
|
242
245
|
autocomplete({
|
|
243
246
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
244
247
|
}),
|
|
245
248
|
code(),
|
|
249
|
+
formatting(),
|
|
250
|
+
heading(),
|
|
246
251
|
hr(),
|
|
247
252
|
image(),
|
|
248
253
|
link({ onRender: onRenderLink, onHover: onHoverLinkTooltip }),
|
|
@@ -251,35 +256,25 @@ const extensions = [
|
|
|
251
256
|
];
|
|
252
257
|
|
|
253
258
|
export const Default = {
|
|
254
|
-
render: () => <Story text={document} extensions={
|
|
259
|
+
render: () => <Story text={document} extensions={defaults} />,
|
|
255
260
|
};
|
|
256
261
|
|
|
257
262
|
export const Readonly = {
|
|
258
|
-
render: () => <Story text={document} extensions={
|
|
263
|
+
render: () => <Story text={document} extensions={defaults} readonly />,
|
|
259
264
|
};
|
|
260
265
|
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
export const Large = {
|
|
264
|
-
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
266
|
+
export const NoExtensions = {
|
|
267
|
+
render: () => <Story text={document} />,
|
|
265
268
|
};
|
|
266
269
|
|
|
270
|
+
const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
|
|
271
|
+
|
|
267
272
|
export const Empty = {
|
|
268
273
|
render: () => <Story />,
|
|
269
274
|
};
|
|
270
275
|
|
|
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
|
-
),
|
|
276
|
+
export const Scrolling = {
|
|
277
|
+
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
283
278
|
};
|
|
284
279
|
|
|
285
280
|
export const Links = {
|
|
@@ -292,37 +287,36 @@ export const Links = {
|
|
|
292
287
|
};
|
|
293
288
|
|
|
294
289
|
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()]} />,
|
|
290
|
+
render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} />,
|
|
300
291
|
};
|
|
301
292
|
|
|
302
293
|
export const Image = {
|
|
303
|
-
render: () => <Story text={str(text.image, text.footer)}
|
|
294
|
+
render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
|
|
304
295
|
};
|
|
305
296
|
|
|
306
|
-
export const
|
|
297
|
+
export const Lists = {
|
|
307
298
|
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
|
-
/>
|
|
299
|
+
<Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[tasklist()]} />
|
|
319
300
|
),
|
|
320
301
|
};
|
|
321
302
|
|
|
303
|
+
export const Table = {
|
|
304
|
+
render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// export const Outliner = {
|
|
308
|
+
// render: () => (
|
|
309
|
+
// <Story
|
|
310
|
+
// text={str('# Outliner', '', 'Block', ': this is a block', ': with multiple lines', text.footer)}
|
|
311
|
+
// extensions={[outliner()]}
|
|
312
|
+
// />
|
|
313
|
+
// ),
|
|
314
|
+
// };
|
|
315
|
+
|
|
322
316
|
export const Autocomplete = {
|
|
323
317
|
render: () => (
|
|
324
318
|
<Story
|
|
325
|
-
text={str('# Autocomplete', '', 'Press
|
|
319
|
+
text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
|
|
326
320
|
extensions={[
|
|
327
321
|
link({ onRender: onRenderLink }),
|
|
328
322
|
autocomplete({
|
|
@@ -333,8 +327,6 @@ export const Autocomplete = {
|
|
|
333
327
|
),
|
|
334
328
|
};
|
|
335
329
|
|
|
336
|
-
const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
|
|
337
|
-
|
|
338
330
|
export const Mention = {
|
|
339
331
|
render: () => (
|
|
340
332
|
<Story
|
|
@@ -350,18 +342,18 @@ export const Mention = {
|
|
|
350
342
|
|
|
351
343
|
export const Comments = {
|
|
352
344
|
render: () => {
|
|
353
|
-
const [
|
|
345
|
+
const [_comments, setComments] = useState<Comment[]>([]);
|
|
354
346
|
|
|
355
347
|
return (
|
|
356
348
|
<Story
|
|
357
349
|
text={str('# Comments', '', text.paragraphs, text.footer)}
|
|
358
|
-
comments={
|
|
350
|
+
comments={_comments}
|
|
359
351
|
extensions={[
|
|
360
352
|
comments({
|
|
361
353
|
onHover: onCommentsHover,
|
|
362
354
|
onCreate: (range) => {
|
|
363
355
|
const id = PublicKey.random().toHex();
|
|
364
|
-
|
|
356
|
+
setComments((commentRanges) => [...commentRanges, { id, cursor: range }]);
|
|
365
357
|
return id;
|
|
366
358
|
},
|
|
367
359
|
onSelect: (state) => {
|
|
@@ -370,9 +362,9 @@ export const Comments = {
|
|
|
370
362
|
console.log(
|
|
371
363
|
'update',
|
|
372
364
|
JSON.stringify({
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
365
|
+
comments: state.comments.length,
|
|
366
|
+
active: state.selection.current?.slice(0, 8),
|
|
367
|
+
closest: state.selection.closest?.slice(0, 8),
|
|
376
368
|
}),
|
|
377
369
|
);
|
|
378
370
|
}
|
|
@@ -384,8 +376,24 @@ export const Comments = {
|
|
|
384
376
|
},
|
|
385
377
|
};
|
|
386
378
|
|
|
379
|
+
export const HorizontalRule = {
|
|
380
|
+
render: () => (
|
|
381
|
+
<Story
|
|
382
|
+
text={str('# Horizontal Rule', '', text.paragraphs, '---', text.paragraphs, '---', text.paragraphs)}
|
|
383
|
+
extensions={[hr()]}
|
|
384
|
+
/>
|
|
385
|
+
),
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
|
|
389
|
+
|
|
387
390
|
export const Typewriter = {
|
|
388
|
-
render: () =>
|
|
391
|
+
render: () => (
|
|
392
|
+
<Story
|
|
393
|
+
text={str('# Typewriter', '', text.paragraphs, text.footer)}
|
|
394
|
+
extensions={[typewriter({ items: typewriterItems })]}
|
|
395
|
+
/>
|
|
396
|
+
),
|
|
389
397
|
};
|
|
390
398
|
|
|
391
399
|
export const Blast = {
|
|
@@ -393,9 +401,7 @@ export const Blast = {
|
|
|
393
401
|
<Story
|
|
394
402
|
text={str('# Blast', '', text.paragraphs, text.code, text.paragraphs)}
|
|
395
403
|
extensions={[
|
|
396
|
-
typewriter({
|
|
397
|
-
items: localStorage.getItem('dxos.composer.extension.demo')?.split(','),
|
|
398
|
-
}),
|
|
404
|
+
typewriter({ items: typewriterItems }),
|
|
399
405
|
blast(
|
|
400
406
|
defaultsDeep(
|
|
401
407
|
{
|
|
@@ -412,17 +418,3 @@ export const Blast = {
|
|
|
412
418
|
/>
|
|
413
419
|
),
|
|
414
420
|
};
|
|
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,123 @@ 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
|
+
EditorView.editable.of(!readonly),
|
|
136
|
+
EditorState.readOnly.of(!!readonly),
|
|
128
137
|
|
|
129
138
|
// Storage and replication.
|
|
139
|
+
// NOTE: This must come before user extensions.
|
|
130
140
|
model.extension,
|
|
131
141
|
|
|
142
|
+
// TODO(burdon): Factor out (requires special handling for Escape/focus).
|
|
143
|
+
editorMode === 'vim' && vim(),
|
|
144
|
+
|
|
132
145
|
// Custom.
|
|
133
146
|
...extensions,
|
|
134
147
|
].filter(Boolean) as Extension[],
|
|
135
148
|
});
|
|
136
149
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
150
|
+
//
|
|
151
|
+
// EditorView
|
|
152
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
153
|
+
//
|
|
141
154
|
const newView = new EditorView({
|
|
142
|
-
parent: rootRef.current,
|
|
143
155
|
state,
|
|
144
|
-
|
|
156
|
+
parent: rootRef.current,
|
|
157
|
+
scrollTo,
|
|
158
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
145
159
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
dispatchTransactions: (trs, view) => {
|
|
161
|
+
if (debug) {
|
|
162
|
+
logChanges(trs);
|
|
163
|
+
}
|
|
164
|
+
view.update(trs);
|
|
165
|
+
},
|
|
149
166
|
});
|
|
150
167
|
|
|
168
|
+
view?.destroy();
|
|
151
169
|
setView(newView);
|
|
170
|
+
|
|
152
171
|
return () => {
|
|
153
172
|
newView?.destroy();
|
|
154
173
|
setView(null);
|
|
155
174
|
};
|
|
156
175
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
157
176
|
|
|
177
|
+
// Handles tab/focus.
|
|
178
|
+
// Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
|
|
158
179
|
const handleKeyUp = useCallback(
|
|
159
180
|
(event: KeyboardEvent) => {
|
|
160
181
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
161
182
|
switch (key) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// }
|
|
183
|
+
case 'Enter': {
|
|
184
|
+
view?.focus();
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
167
187
|
|
|
168
188
|
case 'Escape': {
|
|
169
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
189
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
190
|
+
rootRef.current?.focus();
|
|
191
|
+
}
|
|
170
192
|
break;
|
|
171
193
|
}
|
|
172
194
|
}
|
|
@@ -178,25 +200,26 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
178
200
|
<div
|
|
179
201
|
key={model.id}
|
|
180
202
|
role='none'
|
|
181
|
-
ref={rootRef}
|
|
182
203
|
tabIndex={0}
|
|
183
204
|
onKeyUp={handleKeyUp}
|
|
184
205
|
{...slots.root}
|
|
185
206
|
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
207
|
+
ref={rootRef}
|
|
186
208
|
/>
|
|
187
209
|
);
|
|
188
210
|
},
|
|
189
211
|
);
|
|
190
212
|
|
|
213
|
+
// TODO(burdon): Single-line/scroll.
|
|
191
214
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
192
|
-
({ readonly, placeholder,
|
|
215
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
193
216
|
const { themeMode } = useThemeContext();
|
|
194
217
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
195
218
|
return (
|
|
196
219
|
<BaseTextEditor
|
|
197
220
|
ref={forwardedRef}
|
|
198
221
|
readonly={readonly}
|
|
199
|
-
extensions={[basicBundle({
|
|
222
|
+
extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
200
223
|
theme={theme}
|
|
201
224
|
slots={updatedSlots}
|
|
202
225
|
{...props}
|
|
@@ -205,16 +228,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
205
228
|
},
|
|
206
229
|
);
|
|
207
230
|
|
|
208
|
-
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
209
231
|
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
210
|
-
({ readonly, placeholder,
|
|
232
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
211
233
|
const { themeMode } = useThemeContext();
|
|
212
234
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
213
235
|
return (
|
|
214
236
|
<BaseTextEditor
|
|
215
237
|
ref={forwardedRef}
|
|
216
238
|
readonly={readonly}
|
|
217
|
-
extensions={[markdownBundle({
|
|
239
|
+
extensions={[markdownBundle({ themeMode, placeholder }), ...extensions]}
|
|
218
240
|
theme={theme}
|
|
219
241
|
slots={updatedSlots}
|
|
220
242
|
{...props}
|
|
@@ -225,7 +247,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
225
247
|
|
|
226
248
|
export const defaultSlots: TextEditorSlots = {
|
|
227
249
|
root: {
|
|
228
|
-
|
|
250
|
+
// TODO(burdon): Add focusRing by default/as property?
|
|
251
|
+
className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
|
|
252
|
+
},
|
|
253
|
+
editor: {
|
|
254
|
+
className: 'h-full p-2',
|
|
229
255
|
},
|
|
230
256
|
};
|
|
231
257
|
|