@dxos/react-ui-editor 0.3.11-main.00a28cb → 0.3.11-main.0999ea0
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 +1304 -1092
- 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 +12 -17
- 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 +0 -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/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 +45 -36
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +64 -75
- package/src/components/TextEditor/TextEditor.tsx +103 -71
- package/src/components/TextEditor/comments.stories.tsx +46 -44
- 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 +0 -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/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,11 +19,14 @@ 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
32
|
link,
|
|
@@ -31,15 +34,10 @@ import {
|
|
|
31
34
|
table,
|
|
32
35
|
tasklist,
|
|
33
36
|
typewriter,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
outliner,
|
|
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,14 @@ const onRenderLink: LinkOptions['onRender'] = (el, url) => {
|
|
|
210
210
|
|
|
211
211
|
type StoryProps = {
|
|
212
212
|
text?: string;
|
|
213
|
-
|
|
214
|
-
} & Pick<TextEditorProps, '
|
|
213
|
+
comments?: Comment[];
|
|
214
|
+
} & Pick<TextEditorProps, 'readonly' | 'placeholder' | 'slots' | 'extensions'>;
|
|
215
215
|
|
|
216
|
-
const Story = ({ text,
|
|
217
|
-
const [item] = useState({ text: new TextObject(text
|
|
216
|
+
const Story = ({ text, comments, placeholder = 'New document.', ...props }: StoryProps) => {
|
|
217
|
+
const [item] = useState({ text: new TextObject(text) });
|
|
218
|
+
const view = useRef<EditorView>(null);
|
|
218
219
|
const model = useTextModel({ text: item.text });
|
|
220
|
+
useComments(view.current, comments);
|
|
219
221
|
if (!model) {
|
|
220
222
|
return null;
|
|
221
223
|
}
|
|
@@ -224,7 +226,7 @@ const Story = ({ text, automerge, ...props }: StoryProps) => {
|
|
|
224
226
|
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
225
227
|
<div className='flex h-full justify-center'>
|
|
226
228
|
<div className='flex flex-col h-full w-[800px]'>
|
|
227
|
-
<MarkdownEditor model={model} {...props} />
|
|
229
|
+
<MarkdownEditor ref={view} model={model} placeholder={placeholder} {...props} />
|
|
228
230
|
</div>
|
|
229
231
|
</div>
|
|
230
232
|
</div>
|
|
@@ -238,11 +240,13 @@ export default {
|
|
|
238
240
|
render: Story,
|
|
239
241
|
};
|
|
240
242
|
|
|
241
|
-
const
|
|
243
|
+
const defaults = [
|
|
242
244
|
autocomplete({
|
|
243
245
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
244
246
|
}),
|
|
245
247
|
code(),
|
|
248
|
+
formatting(),
|
|
249
|
+
heading(),
|
|
246
250
|
hr(),
|
|
247
251
|
image(),
|
|
248
252
|
link({ onRender: onRenderLink, onHover: onHoverLinkTooltip }),
|
|
@@ -251,35 +255,25 @@ const extensions = [
|
|
|
251
255
|
];
|
|
252
256
|
|
|
253
257
|
export const Default = {
|
|
254
|
-
render: () => <Story text={document} extensions={
|
|
258
|
+
render: () => <Story text={document} extensions={defaults} />,
|
|
255
259
|
};
|
|
256
260
|
|
|
257
261
|
export const Readonly = {
|
|
258
|
-
render: () => <Story text={document} extensions={
|
|
262
|
+
render: () => <Story text={document} extensions={defaults} readonly />,
|
|
259
263
|
};
|
|
260
264
|
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
export const Large = {
|
|
264
|
-
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
265
|
+
export const NoExtensions = {
|
|
266
|
+
render: () => <Story text={document} />,
|
|
265
267
|
};
|
|
266
268
|
|
|
269
|
+
const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
|
|
270
|
+
|
|
267
271
|
export const Empty = {
|
|
268
272
|
render: () => <Story />,
|
|
269
273
|
};
|
|
270
274
|
|
|
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
|
-
),
|
|
275
|
+
export const Scrolling = {
|
|
276
|
+
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
283
277
|
};
|
|
284
278
|
|
|
285
279
|
export const Links = {
|
|
@@ -292,34 +286,36 @@ export const Links = {
|
|
|
292
286
|
};
|
|
293
287
|
|
|
294
288
|
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()]} />,
|
|
289
|
+
render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} />,
|
|
300
290
|
};
|
|
301
291
|
|
|
302
292
|
export const Image = {
|
|
303
|
-
render: () => <Story text={str(text.image, text.footer)}
|
|
293
|
+
render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
|
|
304
294
|
};
|
|
305
295
|
|
|
306
|
-
export const
|
|
307
|
-
render: () => <Story text={str(text.tasks, '', text.list, text.footer)} extensions={[tasklist()]} />,
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
export const Outliner = {
|
|
296
|
+
export const Lists = {
|
|
311
297
|
render: () => (
|
|
312
|
-
<Story
|
|
313
|
-
text={str('# Outliner', '', 'Block', ': this is a block', ': with multiple lines', text.footer)}
|
|
314
|
-
extensions={[outliner()]}
|
|
315
|
-
/>
|
|
298
|
+
<Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[tasklist()]} />
|
|
316
299
|
),
|
|
317
300
|
};
|
|
318
301
|
|
|
302
|
+
export const Table = {
|
|
303
|
+
render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// export const Outliner = {
|
|
307
|
+
// render: () => (
|
|
308
|
+
// <Story
|
|
309
|
+
// text={str('# Outliner', '', 'Block', ': this is a block', ': with multiple lines', text.footer)}
|
|
310
|
+
// extensions={[outliner()]}
|
|
311
|
+
// />
|
|
312
|
+
// ),
|
|
313
|
+
// };
|
|
314
|
+
|
|
319
315
|
export const Autocomplete = {
|
|
320
316
|
render: () => (
|
|
321
317
|
<Story
|
|
322
|
-
text={str('# Autocomplete', '', 'Press
|
|
318
|
+
text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
|
|
323
319
|
extensions={[
|
|
324
320
|
link({ onRender: onRenderLink }),
|
|
325
321
|
autocomplete({
|
|
@@ -330,8 +326,6 @@ export const Autocomplete = {
|
|
|
330
326
|
),
|
|
331
327
|
};
|
|
332
328
|
|
|
333
|
-
const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', 'shoko'];
|
|
334
|
-
|
|
335
329
|
export const Mention = {
|
|
336
330
|
render: () => (
|
|
337
331
|
<Story
|
|
@@ -347,18 +341,18 @@ export const Mention = {
|
|
|
347
341
|
|
|
348
342
|
export const Comments = {
|
|
349
343
|
render: () => {
|
|
350
|
-
const [
|
|
344
|
+
const [_comments, setComments] = useState<Comment[]>([]);
|
|
351
345
|
|
|
352
346
|
return (
|
|
353
347
|
<Story
|
|
354
348
|
text={str('# Comments', '', text.paragraphs, text.footer)}
|
|
355
|
-
comments={
|
|
349
|
+
comments={_comments}
|
|
356
350
|
extensions={[
|
|
357
351
|
comments({
|
|
358
352
|
onHover: onCommentsHover,
|
|
359
353
|
onCreate: (range) => {
|
|
360
354
|
const id = PublicKey.random().toHex();
|
|
361
|
-
|
|
355
|
+
setComments((commentRanges) => [...commentRanges, { id, cursor: range }]);
|
|
362
356
|
return id;
|
|
363
357
|
},
|
|
364
358
|
onSelect: (state) => {
|
|
@@ -367,9 +361,9 @@ export const Comments = {
|
|
|
367
361
|
console.log(
|
|
368
362
|
'update',
|
|
369
363
|
JSON.stringify({
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
364
|
+
comments: state.comments.length,
|
|
365
|
+
active: state.selection.current?.slice(0, 8),
|
|
366
|
+
closest: state.selection.closest?.slice(0, 8),
|
|
373
367
|
}),
|
|
374
368
|
);
|
|
375
369
|
}
|
|
@@ -381,6 +375,15 @@ export const Comments = {
|
|
|
381
375
|
},
|
|
382
376
|
};
|
|
383
377
|
|
|
378
|
+
export const HorizontalRule = {
|
|
379
|
+
render: () => (
|
|
380
|
+
<Story
|
|
381
|
+
text={str('# Horizontal Rule', '', text.paragraphs, '---', text.paragraphs, '---', text.paragraphs)}
|
|
382
|
+
extensions={[hr()]}
|
|
383
|
+
/>
|
|
384
|
+
),
|
|
385
|
+
};
|
|
386
|
+
|
|
384
387
|
const typewriterItems = localStorage.getItem('dxos.org/plugin/markdown/typewriter')?.split(',');
|
|
385
388
|
|
|
386
389
|
export const Typewriter = {
|
|
@@ -414,17 +417,3 @@ export const Blast = {
|
|
|
414
417
|
/>
|
|
415
418
|
),
|
|
416
419
|
};
|
|
417
|
-
|
|
418
|
-
export const Diagnostics = {
|
|
419
|
-
render: () => (
|
|
420
|
-
<Story
|
|
421
|
-
text={document}
|
|
422
|
-
extensions={[
|
|
423
|
-
// Cursor moved.
|
|
424
|
-
EditorView.updateListener.of((update) => {
|
|
425
|
-
console.log('update', update.view.state.selection.main.head);
|
|
426
|
-
}),
|
|
427
|
-
]}
|
|
428
|
-
/>
|
|
429
|
-
),
|
|
430
|
-
};
|
|
@@ -2,9 +2,9 @@
|
|
|
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
|
-
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
7
|
+
// import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
8
|
import { vim } from '@replit/codemirror-vim';
|
|
9
9
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
10
10
|
import React, {
|
|
@@ -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 { isNotFalsy } 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,129 @@ 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
|
+
// TODO(burdon): Hook causes error even if properties are not spread into div.
|
|
91
|
+
// Uncaught TypeError: Cannot read properties of undefined (reading 'relatedTarget')
|
|
92
|
+
// Uses event.detail, which is deprecated (not event.details). At runtime the event has a property `details`.
|
|
93
|
+
// https://github.com/microsoft/tabster/blob/master/src/State/FocusedElement.ts#L348 (e.detail.relatedTarget)
|
|
94
|
+
// https://github.com/microsoft/keyborg/blob/49e49b2c3ba0a5f6cc518ac46825d7551def8109/src/FocusEvent.ts#L58
|
|
95
|
+
// const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
72
96
|
const { themeMode } = useThemeContext();
|
|
73
97
|
|
|
74
|
-
// The editor view ref should only be used as an escape hatch.
|
|
75
98
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
76
99
|
const [view, setView] = useState<EditorView | null>(null);
|
|
100
|
+
|
|
101
|
+
// The view ref can be used to focus the editor.
|
|
77
102
|
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
78
103
|
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
79
104
|
|
|
105
|
+
// Set focus.
|
|
80
106
|
useEffect(() => {
|
|
81
|
-
if (
|
|
107
|
+
if (autoFocus) {
|
|
82
108
|
view?.focus();
|
|
83
109
|
}
|
|
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]);
|
|
110
|
+
}, [view, autoFocus]);
|
|
106
111
|
|
|
112
|
+
// Create editor state and view.
|
|
113
|
+
// The view is recreated if the model or extensions are changed.
|
|
107
114
|
useEffect(() => {
|
|
108
115
|
if (!model || !rootRef.current) {
|
|
109
116
|
return;
|
|
110
117
|
}
|
|
111
118
|
|
|
119
|
+
//
|
|
120
|
+
// EditorState
|
|
112
121
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
122
|
+
//
|
|
113
123
|
const state = EditorState.create({
|
|
114
124
|
doc: model.text(),
|
|
115
|
-
// TODO(burdon): Composer should store and set selection when switching documents.
|
|
116
125
|
selection,
|
|
117
126
|
extensions: [
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
128
|
+
EditorView.exceptionSink.of((err) => {
|
|
129
|
+
log.catch(err);
|
|
130
|
+
}),
|
|
122
131
|
|
|
123
132
|
// Theme.
|
|
124
|
-
// TODO(burdon): Make
|
|
133
|
+
// TODO(burdon): Make configurable.
|
|
125
134
|
EditorView.baseTheme(defaultTheme),
|
|
126
135
|
EditorView.theme(theme ?? {}),
|
|
127
136
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
137
|
+
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
138
|
+
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
139
|
+
|
|
140
|
+
// State.
|
|
141
|
+
EditorView.editable.of(!readonly),
|
|
142
|
+
EditorState.readOnly.of(!!readonly),
|
|
128
143
|
|
|
129
144
|
// Storage and replication.
|
|
145
|
+
// NOTE: This must come before user extensions.
|
|
130
146
|
model.extension,
|
|
131
147
|
|
|
148
|
+
// TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
|
|
149
|
+
editorMode === 'vim' && vim(),
|
|
150
|
+
|
|
132
151
|
// Custom.
|
|
133
152
|
...extensions,
|
|
134
|
-
].filter(
|
|
153
|
+
].filter(isNotFalsy),
|
|
135
154
|
});
|
|
136
155
|
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
156
|
+
//
|
|
157
|
+
// EditorView
|
|
158
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
159
|
+
//
|
|
141
160
|
const newView = new EditorView({
|
|
142
|
-
parent: rootRef.current,
|
|
143
161
|
state,
|
|
144
|
-
|
|
162
|
+
parent: rootRef.current,
|
|
163
|
+
scrollTo,
|
|
164
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
145
165
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
166
|
+
dispatchTransactions: (trs, view) => {
|
|
167
|
+
if (debug) {
|
|
168
|
+
logChanges(trs);
|
|
169
|
+
}
|
|
170
|
+
view.update(trs);
|
|
171
|
+
},
|
|
149
172
|
});
|
|
150
173
|
|
|
174
|
+
view?.destroy();
|
|
151
175
|
setView(newView);
|
|
176
|
+
|
|
152
177
|
return () => {
|
|
153
178
|
newView?.destroy();
|
|
154
179
|
setView(null);
|
|
155
180
|
};
|
|
156
181
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
157
182
|
|
|
183
|
+
// Handles tab/focus.
|
|
184
|
+
// Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
|
|
185
|
+
// TODO(burdon): Convert to keymap?
|
|
158
186
|
const handleKeyUp = useCallback(
|
|
159
187
|
(event: KeyboardEvent) => {
|
|
160
188
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
161
189
|
switch (key) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// }
|
|
190
|
+
case 'Enter': {
|
|
191
|
+
view?.focus();
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
167
194
|
|
|
168
195
|
case 'Escape': {
|
|
169
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
196
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
197
|
+
view?.focus();
|
|
198
|
+
}
|
|
170
199
|
break;
|
|
171
200
|
}
|
|
172
201
|
}
|
|
@@ -179,9 +208,9 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
179
208
|
key={model.id}
|
|
180
209
|
role='none'
|
|
181
210
|
tabIndex={0}
|
|
182
|
-
onKeyUp={handleKeyUp}
|
|
183
211
|
{...slots.root}
|
|
184
|
-
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
212
|
+
// {...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
213
|
+
onKeyUp={handleKeyUp}
|
|
185
214
|
ref={rootRef}
|
|
186
215
|
/>
|
|
187
216
|
);
|
|
@@ -189,14 +218,14 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
189
218
|
);
|
|
190
219
|
|
|
191
220
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
192
|
-
({ readonly, placeholder,
|
|
221
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
193
222
|
const { themeMode } = useThemeContext();
|
|
194
223
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
195
224
|
return (
|
|
196
225
|
<BaseTextEditor
|
|
197
226
|
ref={forwardedRef}
|
|
198
227
|
readonly={readonly}
|
|
199
|
-
extensions={[basicBundle({
|
|
228
|
+
extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
200
229
|
theme={theme}
|
|
201
230
|
slots={updatedSlots}
|
|
202
231
|
{...props}
|
|
@@ -205,16 +234,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
205
234
|
},
|
|
206
235
|
);
|
|
207
236
|
|
|
208
|
-
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
209
237
|
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
210
|
-
({ readonly, placeholder,
|
|
238
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
211
239
|
const { themeMode } = useThemeContext();
|
|
212
240
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
213
241
|
return (
|
|
214
242
|
<BaseTextEditor
|
|
215
243
|
ref={forwardedRef}
|
|
216
244
|
readonly={readonly}
|
|
217
|
-
extensions={[markdownBundle({
|
|
245
|
+
extensions={[markdownBundle({ themeMode, placeholder }), ...extensions]}
|
|
218
246
|
theme={theme}
|
|
219
247
|
slots={updatedSlots}
|
|
220
248
|
{...props}
|
|
@@ -225,7 +253,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
225
253
|
|
|
226
254
|
export const defaultSlots: TextEditorSlots = {
|
|
227
255
|
root: {
|
|
228
|
-
|
|
256
|
+
// TODO(burdon): Add focusRing by default/as property?
|
|
257
|
+
className: mx('flex flex-col grow overflow-y-auto', attentionSurface),
|
|
258
|
+
},
|
|
259
|
+
editor: {
|
|
260
|
+
className: 'h-full p-2',
|
|
229
261
|
},
|
|
230
262
|
};
|
|
231
263
|
|