@dxos/react-ui-editor 0.3.11-main.c8e9429 → 0.3.11-main.cb8120f
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 +1306 -1084
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +13 -14
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +13 -5
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
- package/dist/types/src/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/automerge.spec.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +8 -10
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +7 -6
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts +2 -2
- 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 +23 -15
- 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 +3 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +25 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +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 +44 -35
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +72 -80
- package/src/components/TextEditor/TextEditor.tsx +94 -67
- package/src/components/TextEditor/comments.stories.tsx +45 -43
- 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.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +50 -39
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +11 -36
- package/src/extensions/automerge/semaphore.ts +31 -28
- package/src/extensions/automerge/update-automerge.ts +14 -8
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +17 -17
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/comments.ts +247 -179
- 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} +5 -2
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -34
- package/src/extensions/mention.ts +9 -2
- package/src/extensions/outliner.ts +12 -0
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useActionHandler.ts +45 -0
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +49 -55
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +1 -3
- package/src/util.ts +26 -3
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts +0 -7
- package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -3
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -27
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.d.ts.map +0 -1
- package/src/components/TextEditor/codemirror.stories.ts +0 -115
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/code.ts +0 -104
- package/src/extensions/hr.ts +0 -72
- package/src/extensions/yjs/cursor.ts +0 -22
- package/src/extensions/yjs/index.ts +0 -5
- package/src/extensions/yjs/yjs.test.ts +0 -35
- package/src/extensions/yjs/yjs.ts +0 -16
- package/src/hooks/automerge.ts +0 -52
- package/src/hooks/yjs.ts +0 -156
- package/src/testing/index.ts +0 -6
- package/src/testing/proto/gen/schema.ts +0 -49
- package/src/testing/proto/index.ts +0 -5
- package/src/testing/proto/schema.proto +0 -15
- package/src/testing/replicator.ts +0 -184
- /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
- /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
- /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
- /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
- /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
- /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
- /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
|
-
import { EditorView } from '@codemirror/view';
|
|
7
|
+
import { type EditorView } from '@codemirror/view';
|
|
8
8
|
import { faker } from '@faker-js/faker';
|
|
9
9
|
import { ArrowSquareOut } from '@phosphor-icons/react';
|
|
10
10
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
11
|
-
import React, { type FC, StrictMode, useState } from 'react';
|
|
11
|
+
import React, { type FC, StrictMode, useRef, useState } from 'react';
|
|
12
12
|
import { createRoot } from 'react-dom/client';
|
|
13
13
|
|
|
14
14
|
import { TextObject } from '@dxos/echo-schema';
|
|
@@ -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,22 @@ 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
|
+
import { nonNullable } from '@dxos/util';
|
|
24
25
|
|
|
25
|
-
import { basicBundle, markdownBundle
|
|
26
|
-
import { type
|
|
26
|
+
import { basicBundle, markdownBundle } from '../../extensions';
|
|
27
|
+
import { type EditorModel } from '../../hooks';
|
|
27
28
|
import { type ThemeStyles } from '../../styles';
|
|
28
29
|
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
30
|
+
import { logChanges } from '../../util';
|
|
29
31
|
|
|
32
|
+
// TODO(burdon): Change to enum?
|
|
30
33
|
export const EditorModes = ['default', 'vim'] as const;
|
|
31
34
|
export type EditorMode = (typeof EditorModes)[number];
|
|
32
35
|
|
|
@@ -41,23 +44,28 @@ export type CursorInfo = {
|
|
|
41
44
|
|
|
42
45
|
export type TextEditorSlots = {
|
|
43
46
|
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
editor?: {
|
|
48
|
+
className?: string;
|
|
49
|
+
};
|
|
50
|
+
content?: {
|
|
51
|
+
className?: string;
|
|
52
|
+
};
|
|
47
53
|
};
|
|
48
54
|
|
|
49
55
|
// TODO(burdon): Spellcheck?
|
|
50
56
|
export type TextEditorProps = {
|
|
51
|
-
model: EditorModel;
|
|
52
|
-
autofocus?: boolean;
|
|
57
|
+
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
53
58
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
59
|
+
autoFocus?: boolean;
|
|
60
|
+
lineWrapping?: boolean;
|
|
61
|
+
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
54
62
|
selection?: { anchor: number; head?: number };
|
|
55
|
-
|
|
56
|
-
extensions?: Extension[];
|
|
57
|
-
editorMode?: EditorMode;
|
|
63
|
+
editorMode?: EditorMode; // TODO(burdon): Factor out.
|
|
58
64
|
placeholder?: string;
|
|
59
65
|
theme?: ThemeStyles;
|
|
60
66
|
slots?: TextEditorSlots;
|
|
67
|
+
extensions?: Extension[];
|
|
68
|
+
debug?: boolean;
|
|
61
69
|
};
|
|
62
70
|
|
|
63
71
|
/**
|
|
@@ -65,108 +73,123 @@ export type TextEditorProps = {
|
|
|
65
73
|
*/
|
|
66
74
|
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
67
75
|
(
|
|
68
|
-
{
|
|
76
|
+
{
|
|
77
|
+
model,
|
|
78
|
+
readonly,
|
|
79
|
+
autoFocus,
|
|
80
|
+
scrollTo,
|
|
81
|
+
selection,
|
|
82
|
+
editorMode,
|
|
83
|
+
theme,
|
|
84
|
+
slots = defaultSlots,
|
|
85
|
+
extensions = [],
|
|
86
|
+
debug,
|
|
87
|
+
},
|
|
69
88
|
forwardedRef,
|
|
70
89
|
) => {
|
|
71
90
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
72
91
|
const { themeMode } = useThemeContext();
|
|
73
92
|
|
|
74
|
-
// The editor view ref should only be used as an escape hatch.
|
|
75
93
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
76
94
|
const [view, setView] = useState<EditorView | null>(null);
|
|
95
|
+
|
|
96
|
+
// The view ref can be used to focus the editor.
|
|
77
97
|
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
78
98
|
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
79
99
|
|
|
100
|
+
// Set focus.
|
|
80
101
|
useEffect(() => {
|
|
81
|
-
if (
|
|
102
|
+
if (autoFocus) {
|
|
82
103
|
view?.focus();
|
|
83
104
|
}
|
|
84
|
-
}, [
|
|
85
|
-
|
|
86
|
-
// TODO(burdon): Factor out as extension/hook.
|
|
87
|
-
const { awareness, peer } = model;
|
|
88
|
-
useEffect(() => {
|
|
89
|
-
if (awareness && peer) {
|
|
90
|
-
awareness.setLocalStateField('user', {
|
|
91
|
-
name: peer.name ?? generateName(peer.id),
|
|
92
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
93
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}, [awareness, peer, themeMode]);
|
|
97
|
-
|
|
98
|
-
// TODO(burdon): Factor out as extension/hook.
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
if (view && comments !== undefined) {
|
|
101
|
-
view.dispatch({
|
|
102
|
-
effects: setCommentRange.of({ model, comments }),
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}, [view, comments]);
|
|
105
|
+
}, [view, autoFocus]);
|
|
106
106
|
|
|
107
|
+
// Create editor state and view.
|
|
108
|
+
// The view is recreated if the model or extensions are changed.
|
|
107
109
|
useEffect(() => {
|
|
108
110
|
if (!model || !rootRef.current) {
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
//
|
|
115
|
+
// EditorState
|
|
112
116
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
117
|
+
//
|
|
113
118
|
const state = EditorState.create({
|
|
114
119
|
doc: model.text(),
|
|
115
|
-
// TODO(burdon): Composer should store and set selection when switching documents.
|
|
116
120
|
selection,
|
|
117
121
|
extensions: [
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
123
|
+
EditorView.exceptionSink.of((err) => {
|
|
124
|
+
log.catch(err);
|
|
125
|
+
}),
|
|
122
126
|
|
|
123
127
|
// Theme.
|
|
124
|
-
// TODO(burdon): Make
|
|
128
|
+
// TODO(burdon): Make configurable.
|
|
125
129
|
EditorView.baseTheme(defaultTheme),
|
|
126
130
|
EditorView.theme(theme ?? {}),
|
|
127
131
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
132
|
+
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
133
|
+
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
134
|
+
|
|
135
|
+
// State.
|
|
136
|
+
EditorView.editable.of(!readonly),
|
|
137
|
+
EditorState.readOnly.of(!!readonly),
|
|
128
138
|
|
|
129
139
|
// Storage and replication.
|
|
140
|
+
// NOTE: This must come before user extensions.
|
|
130
141
|
model.extension,
|
|
131
142
|
|
|
143
|
+
// TODO(burdon): Factor out (requires special handling for Escape/focus).
|
|
144
|
+
editorMode === 'vim' && vim(),
|
|
145
|
+
|
|
132
146
|
// Custom.
|
|
133
147
|
...extensions,
|
|
134
|
-
].filter(
|
|
148
|
+
].filter(nonNullable) as Extension[], // TODO(burdon): ???
|
|
135
149
|
});
|
|
136
150
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
151
|
+
//
|
|
152
|
+
// EditorView
|
|
153
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
154
|
+
//
|
|
141
155
|
const newView = new EditorView({
|
|
142
|
-
parent: rootRef.current,
|
|
143
156
|
state,
|
|
144
|
-
|
|
157
|
+
parent: rootRef.current,
|
|
158
|
+
scrollTo,
|
|
159
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
145
160
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
dispatchTransactions: (trs, view) => {
|
|
162
|
+
if (debug) {
|
|
163
|
+
logChanges(trs);
|
|
164
|
+
}
|
|
165
|
+
view.update(trs);
|
|
166
|
+
},
|
|
149
167
|
});
|
|
150
168
|
|
|
169
|
+
view?.destroy();
|
|
151
170
|
setView(newView);
|
|
171
|
+
|
|
152
172
|
return () => {
|
|
153
173
|
newView?.destroy();
|
|
154
174
|
setView(null);
|
|
155
175
|
};
|
|
156
176
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
157
177
|
|
|
178
|
+
// Handles tab/focus.
|
|
179
|
+
// Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
|
|
158
180
|
const handleKeyUp = useCallback(
|
|
159
181
|
(event: KeyboardEvent) => {
|
|
160
182
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
161
183
|
switch (key) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// }
|
|
184
|
+
case 'Enter': {
|
|
185
|
+
view?.focus();
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
167
188
|
|
|
168
189
|
case 'Escape': {
|
|
169
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
190
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
191
|
+
rootRef.current?.focus();
|
|
192
|
+
}
|
|
170
193
|
break;
|
|
171
194
|
}
|
|
172
195
|
}
|
|
@@ -188,15 +211,16 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
188
211
|
},
|
|
189
212
|
);
|
|
190
213
|
|
|
214
|
+
// TODO(burdon): Single-line/scroll.
|
|
191
215
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
192
|
-
({ readonly, placeholder,
|
|
216
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
193
217
|
const { themeMode } = useThemeContext();
|
|
194
218
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
195
219
|
return (
|
|
196
220
|
<BaseTextEditor
|
|
197
221
|
ref={forwardedRef}
|
|
198
222
|
readonly={readonly}
|
|
199
|
-
extensions={[basicBundle({
|
|
223
|
+
extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
200
224
|
theme={theme}
|
|
201
225
|
slots={updatedSlots}
|
|
202
226
|
{...props}
|
|
@@ -205,16 +229,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
205
229
|
},
|
|
206
230
|
);
|
|
207
231
|
|
|
208
|
-
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
209
232
|
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
210
|
-
({ readonly, placeholder,
|
|
233
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
211
234
|
const { themeMode } = useThemeContext();
|
|
212
235
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
213
236
|
return (
|
|
214
237
|
<BaseTextEditor
|
|
215
238
|
ref={forwardedRef}
|
|
216
239
|
readonly={readonly}
|
|
217
|
-
extensions={[markdownBundle({
|
|
240
|
+
extensions={[markdownBundle({ themeMode, placeholder }), ...extensions]}
|
|
218
241
|
theme={theme}
|
|
219
242
|
slots={updatedSlots}
|
|
220
243
|
{...props}
|
|
@@ -225,7 +248,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
225
248
|
|
|
226
249
|
export const defaultSlots: TextEditorSlots = {
|
|
227
250
|
root: {
|
|
228
|
-
|
|
251
|
+
// TODO(burdon): Add focusRing by default/as property?
|
|
252
|
+
className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
|
|
253
|
+
},
|
|
254
|
+
editor: {
|
|
255
|
+
className: 'h-full p-2',
|
|
229
256
|
},
|
|
230
257
|
};
|
|
231
258
|
|