@dxos/react-ui-editor 0.3.11-main.08776d7 → 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 +1211 -1005
- 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 -7
- 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 +6 -6
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +6 -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 +11 -3
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +0 -8
- 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 +1 -4
- package/dist/types/src/hooks/defs.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/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.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 +61 -72
- package/src/components/TextEditor/TextEditor.tsx +101 -77
- package/src/components/TextEditor/comments.stories.tsx +16 -21
- 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 +53 -13
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +9 -29
- package/src/extensions/automerge/semaphore.ts +24 -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 +149 -92
- package/src/extensions/index.ts +0 -8
- 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 +1 -6
- package/src/hooks/index.ts +2 -2
- package/src/hooks/useActionHandler.ts +45 -0
- package/src/hooks/useTextEditor.ts +29 -0
- package/src/hooks/useTextModel.ts +48 -11
- 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/sync.d.ts +0 -7
- package/dist/types/src/extensions/sync.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 -4
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -28
- 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/sync.ts +0 -15
- 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 -53
- package/src/hooks/yjs.ts +0 -157
- 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,16 +34,11 @@ import {
|
|
|
31
34
|
table,
|
|
32
35
|
tasklist,
|
|
33
36
|
typewriter,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
outliner,
|
|
37
|
+
useComments,
|
|
38
|
+
formatting,
|
|
37
39
|
} from '../../extensions';
|
|
38
40
|
import { type Comment, useTextModel } from '../../hooks';
|
|
39
41
|
|
|
40
|
-
// Extensions:
|
|
41
|
-
// TODO(burdon): Table of contents.
|
|
42
|
-
// TODO(burdon): Front-matter
|
|
43
|
-
|
|
44
42
|
faker.seed(101);
|
|
45
43
|
|
|
46
44
|
const str = (...lines: string[]) => lines.join('\n');
|
|
@@ -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) => {
|
|
@@ -368,7 +362,7 @@ export const Comments = {
|
|
|
368
362
|
'update',
|
|
369
363
|
JSON.stringify({
|
|
370
364
|
comments: state.comments.length,
|
|
371
|
-
active: state.selection.
|
|
365
|
+
active: state.selection.current?.slice(0, 8),
|
|
372
366
|
closest: state.selection.closest?.slice(0, 8),
|
|
373
367
|
}),
|
|
374
368
|
);
|
|
@@ -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,45 +44,28 @@ export type CursorInfo = {
|
|
|
41
44
|
|
|
42
45
|
export type TextEditorSlots = {
|
|
43
46
|
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
47
|
+
editor?: {
|
|
48
|
+
className?: string;
|
|
49
|
+
};
|
|
50
|
+
content?: {
|
|
51
|
+
className?: string;
|
|
52
|
+
};
|
|
44
53
|
};
|
|
45
54
|
|
|
46
55
|
// TODO(burdon): Spellcheck?
|
|
47
56
|
export type TextEditorProps = {
|
|
48
|
-
model: EditorModel;
|
|
49
|
-
autofocus?: boolean;
|
|
57
|
+
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
50
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().
|
|
51
62
|
selection?: { anchor: number; head?: number };
|
|
52
|
-
|
|
53
|
-
extensions?: Extension[];
|
|
54
|
-
editorMode?: EditorMode;
|
|
63
|
+
editorMode?: EditorMode; // TODO(burdon): Factor out.
|
|
55
64
|
placeholder?: string;
|
|
56
65
|
theme?: ThemeStyles;
|
|
57
66
|
slots?: TextEditorSlots;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// TODO(burdon): Factor out?
|
|
61
|
-
export const useComments = (view?: EditorView | null, comments?: Comment[]) => {
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (view && comments !== undefined) {
|
|
64
|
-
view.dispatch({
|
|
65
|
-
effects: setComments.of(comments),
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}, [view, comments]);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// TODO(burdon): Factor out?
|
|
72
|
-
export const useAwareness = ({ awareness, peer }: EditorModel) => {
|
|
73
|
-
const { themeMode } = useThemeContext();
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (awareness && peer) {
|
|
76
|
-
awareness.setLocalStateField('user', {
|
|
77
|
-
name: peer.name ?? generateName(peer.id),
|
|
78
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
79
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}, [awareness, peer, themeMode]);
|
|
67
|
+
extensions?: Extension[];
|
|
68
|
+
debug?: boolean;
|
|
83
69
|
};
|
|
84
70
|
|
|
85
71
|
/**
|
|
@@ -87,93 +73,128 @@ export const useAwareness = ({ awareness, peer }: EditorModel) => {
|
|
|
87
73
|
*/
|
|
88
74
|
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
89
75
|
(
|
|
90
|
-
{
|
|
76
|
+
{
|
|
77
|
+
model,
|
|
78
|
+
readonly,
|
|
79
|
+
autoFocus,
|
|
80
|
+
scrollTo,
|
|
81
|
+
selection,
|
|
82
|
+
editorMode,
|
|
83
|
+
theme,
|
|
84
|
+
slots = defaultSlots,
|
|
85
|
+
extensions = [],
|
|
86
|
+
debug,
|
|
87
|
+
},
|
|
91
88
|
forwardedRef,
|
|
92
89
|
) => {
|
|
93
|
-
|
|
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' });
|
|
94
96
|
const { themeMode } = useThemeContext();
|
|
95
97
|
|
|
96
|
-
// The editor view ref should only be used as an escape hatch.
|
|
97
98
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
98
99
|
const [view, setView] = useState<EditorView | null>(null);
|
|
100
|
+
|
|
101
|
+
// The view ref can be used to focus the editor.
|
|
99
102
|
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
100
103
|
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
101
104
|
|
|
102
|
-
//
|
|
105
|
+
// Set focus.
|
|
103
106
|
useEffect(() => {
|
|
104
|
-
if (
|
|
107
|
+
if (autoFocus) {
|
|
105
108
|
view?.focus();
|
|
106
109
|
}
|
|
107
|
-
}, [view,
|
|
108
|
-
|
|
109
|
-
// TODO(burdon): Factor out as extension/hook.
|
|
110
|
-
useAwareness(model);
|
|
111
|
-
|
|
112
|
-
// TODO(burdon): Factor out as extension/hook.
|
|
113
|
-
useComments(view, comments);
|
|
110
|
+
}, [view, autoFocus]);
|
|
114
111
|
|
|
112
|
+
// Create editor state and view.
|
|
113
|
+
// The view is recreated if the model or extensions are changed.
|
|
115
114
|
useEffect(() => {
|
|
116
115
|
if (!model || !rootRef.current) {
|
|
117
116
|
return;
|
|
118
117
|
}
|
|
119
118
|
|
|
119
|
+
//
|
|
120
|
+
// EditorState
|
|
120
121
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
122
|
+
//
|
|
121
123
|
const state = EditorState.create({
|
|
122
124
|
doc: model.text(),
|
|
123
|
-
// TODO(burdon): Composer should store and set selection when switching documents.
|
|
124
125
|
selection,
|
|
125
126
|
extensions: [
|
|
126
|
-
|
|
127
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
128
|
+
EditorView.exceptionSink.of((err) => {
|
|
129
|
+
log.catch(err);
|
|
130
|
+
}),
|
|
127
131
|
|
|
128
132
|
// Theme.
|
|
129
|
-
// TODO(burdon): Make
|
|
133
|
+
// TODO(burdon): Make configurable.
|
|
130
134
|
EditorView.baseTheme(defaultTheme),
|
|
131
135
|
EditorView.theme(theme ?? {}),
|
|
132
136
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
137
|
+
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
138
|
+
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
133
139
|
|
|
134
|
-
//
|
|
135
|
-
|
|
140
|
+
// State.
|
|
141
|
+
EditorView.editable.of(!readonly),
|
|
142
|
+
EditorState.readOnly.of(!!readonly),
|
|
136
143
|
|
|
137
|
-
// Storage and replication
|
|
144
|
+
// Storage and replication.
|
|
145
|
+
// NOTE: This must come before user extensions.
|
|
138
146
|
model.extension,
|
|
139
147
|
|
|
148
|
+
// TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
|
|
149
|
+
editorMode === 'vim' && vim(),
|
|
150
|
+
|
|
140
151
|
// Custom.
|
|
141
152
|
...extensions,
|
|
142
|
-
].filter(
|
|
153
|
+
].filter(isNotFalsy),
|
|
143
154
|
});
|
|
144
155
|
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
156
|
+
//
|
|
157
|
+
// EditorView
|
|
158
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
159
|
+
//
|
|
149
160
|
const newView = new EditorView({
|
|
150
|
-
parent: rootRef.current,
|
|
151
161
|
state,
|
|
152
|
-
|
|
162
|
+
parent: rootRef.current,
|
|
163
|
+
scrollTo,
|
|
164
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
153
165
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
view.
|
|
166
|
+
dispatchTransactions: (trs, view) => {
|
|
167
|
+
if (debug) {
|
|
168
|
+
logChanges(trs);
|
|
169
|
+
}
|
|
170
|
+
view.update(trs);
|
|
159
171
|
},
|
|
160
172
|
});
|
|
161
173
|
|
|
174
|
+
view?.destroy();
|
|
162
175
|
setView(newView);
|
|
176
|
+
|
|
163
177
|
return () => {
|
|
164
178
|
newView?.destroy();
|
|
165
179
|
setView(null);
|
|
166
180
|
};
|
|
167
181
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
168
182
|
|
|
169
|
-
//
|
|
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?
|
|
170
186
|
const handleKeyUp = useCallback(
|
|
171
187
|
(event: KeyboardEvent) => {
|
|
172
188
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
173
189
|
switch (key) {
|
|
190
|
+
case 'Enter': {
|
|
191
|
+
view?.focus();
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
174
195
|
case 'Escape': {
|
|
175
196
|
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
176
|
-
|
|
197
|
+
view?.focus();
|
|
177
198
|
}
|
|
178
199
|
break;
|
|
179
200
|
}
|
|
@@ -187,9 +208,9 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
187
208
|
key={model.id}
|
|
188
209
|
role='none'
|
|
189
210
|
tabIndex={0}
|
|
190
|
-
onKeyUp={handleKeyUp}
|
|
191
211
|
{...slots.root}
|
|
192
|
-
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
212
|
+
// {...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
213
|
+
onKeyUp={handleKeyUp}
|
|
193
214
|
ref={rootRef}
|
|
194
215
|
/>
|
|
195
216
|
);
|
|
@@ -197,14 +218,14 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
197
218
|
);
|
|
198
219
|
|
|
199
220
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
200
|
-
({ readonly, placeholder,
|
|
221
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
201
222
|
const { themeMode } = useThemeContext();
|
|
202
223
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
203
224
|
return (
|
|
204
225
|
<BaseTextEditor
|
|
205
226
|
ref={forwardedRef}
|
|
206
227
|
readonly={readonly}
|
|
207
|
-
extensions={[basicBundle({
|
|
228
|
+
extensions={[basicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
208
229
|
theme={theme}
|
|
209
230
|
slots={updatedSlots}
|
|
210
231
|
{...props}
|
|
@@ -213,16 +234,15 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
213
234
|
},
|
|
214
235
|
);
|
|
215
236
|
|
|
216
|
-
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
217
237
|
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
218
|
-
({ readonly, placeholder,
|
|
238
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
219
239
|
const { themeMode } = useThemeContext();
|
|
220
240
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
221
241
|
return (
|
|
222
242
|
<BaseTextEditor
|
|
223
243
|
ref={forwardedRef}
|
|
224
244
|
readonly={readonly}
|
|
225
|
-
extensions={[markdownBundle({
|
|
245
|
+
extensions={[markdownBundle({ themeMode, placeholder }), ...extensions]}
|
|
226
246
|
theme={theme}
|
|
227
247
|
slots={updatedSlots}
|
|
228
248
|
{...props}
|
|
@@ -233,7 +253,11 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
233
253
|
|
|
234
254
|
export const defaultSlots: TextEditorSlots = {
|
|
235
255
|
root: {
|
|
236
|
-
|
|
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',
|
|
237
261
|
},
|
|
238
262
|
};
|
|
239
263
|
|