@dxos/react-ui-editor 0.4.4-next.e0df51e → 0.4.5-main.17763ce
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 +459 -564
- 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 +5 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor-line-margin.d.ts +8 -0
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +1 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +5 -0
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -4
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +1 -11
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/styles/layout.d.ts +2 -2
- package/dist/types/src/styles/layout.d.ts.map +1 -1
- package/package.json +31 -28
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +35 -37
- package/src/components/TextEditor/TextEditor.tsx +18 -5
- package/src/components/TextEditor/automerge.stories.tsx +9 -6
- package/src/components/TextEditor/hooks.stories.tsx +49 -27
- package/src/components/Toolbar/Toolbar.stories.tsx +4 -7
- package/src/components/Toolbar/Toolbar.tsx +0 -1
- package/src/extensions/cursor-line-margin.ts +92 -0
- package/src/extensions/index.ts +1 -0
- package/src/extensions/markdown/decorate.ts +301 -0
- package/src/extensions/markdown/formatting.test.ts +17 -0
- package/src/extensions/markdown/formatting.ts +67 -76
- package/src/extensions/markdown/index.ts +1 -4
- package/src/extensions/markdown/link.ts +23 -185
- package/src/hooks/useTextEditor.ts +6 -2
- package/src/styles/layout.ts +3 -2
- package/dist/types/src/extensions/markdown/code.d.ts +0 -2
- package/dist/types/src/extensions/markdown/code.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/heading.d.ts +0 -6
- package/dist/types/src/extensions/markdown/heading.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/hr.d.ts +0 -2
- package/dist/types/src/extensions/markdown/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/tasklist.d.ts +0 -3
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +0 -1
- package/src/extensions/markdown/code.ts +0 -162
- package/src/extensions/markdown/heading.ts +0 -59
- package/src/extensions/markdown/hr.ts +0 -71
- package/src/extensions/markdown/tasklist.ts +0 -110
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-editor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5-main.17763ce",
|
|
4
4
|
"description": "Document editing experience within a DXOS shell.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@codemirror/state": "^6.4.0",
|
|
24
24
|
"@codemirror/theme-one-dark": "^6.1.2",
|
|
25
25
|
"@codemirror/view": "^6.23.0",
|
|
26
|
-
"@fluentui/react-tabster": "^9.
|
|
26
|
+
"@fluentui/react-tabster": "^9.19.0",
|
|
27
27
|
"@lezer/common": "^1.2.1",
|
|
28
28
|
"@lezer/highlight": "^1.2.0",
|
|
29
29
|
"@lezer/markdown": "^1.2.0",
|
|
@@ -36,20 +36,20 @@
|
|
|
36
36
|
"lodash.pick": "^4.4.0",
|
|
37
37
|
"lodash.sortby": "^4.7.0",
|
|
38
38
|
"style-mod": "^4.1.0",
|
|
39
|
-
"@dxos/async": "0.4.
|
|
40
|
-
"@dxos/automerge": "0.4.
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/invariant": "0.4.
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/react-async": "0.4.
|
|
48
|
-
"@dxos/react-ui": "0.4.
|
|
49
|
-
"@dxos/
|
|
50
|
-
"@dxos/react-ui
|
|
51
|
-
"@dxos/text-model": "0.4.
|
|
52
|
-
"@dxos/
|
|
39
|
+
"@dxos/async": "0.4.5-main.17763ce",
|
|
40
|
+
"@dxos/automerge": "0.4.5-main.17763ce",
|
|
41
|
+
"@dxos/display-name": "0.4.5-main.17763ce",
|
|
42
|
+
"@dxos/debug": "0.4.5-main.17763ce",
|
|
43
|
+
"@dxos/echo-schema": "0.4.5-main.17763ce",
|
|
44
|
+
"@dxos/protocols": "0.4.5-main.17763ce",
|
|
45
|
+
"@dxos/invariant": "0.4.5-main.17763ce",
|
|
46
|
+
"@dxos/context": "0.4.5-main.17763ce",
|
|
47
|
+
"@dxos/react-async": "0.4.5-main.17763ce",
|
|
48
|
+
"@dxos/react-ui-theme": "0.4.5-main.17763ce",
|
|
49
|
+
"@dxos/util": "0.4.5-main.17763ce",
|
|
50
|
+
"@dxos/react-ui": "0.4.5-main.17763ce",
|
|
51
|
+
"@dxos/text-model": "0.4.5-main.17763ce",
|
|
52
|
+
"@dxos/log": "0.4.5-main.17763ce"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@automerge/automerge-repo-network-broadcastchannel": "^1.1.1",
|
|
@@ -65,31 +65,34 @@
|
|
|
65
65
|
"@types/react": "^18.0.21",
|
|
66
66
|
"@types/react-dom": "^18.0.6",
|
|
67
67
|
"@types/react-test-renderer": "^17.0.2",
|
|
68
|
-
"@vitest/
|
|
68
|
+
"@vitest/browser": "^1.2.2",
|
|
69
|
+
"@vitest/ui": "^1.2.2",
|
|
69
70
|
"deepsignal": "^1.5.0",
|
|
70
71
|
"happy-dom": "^13.3.1",
|
|
71
72
|
"jsdom": "^20.0.1",
|
|
72
73
|
"react": "^18.2.0",
|
|
73
74
|
"react-dom": "^18.2.0",
|
|
74
75
|
"react-test-renderer": "^18.2.0",
|
|
75
|
-
"vite": "^5.
|
|
76
|
+
"vite": "^5.1.3",
|
|
76
77
|
"vite-plugin-top-level-await": "^1.4.1",
|
|
77
78
|
"vite-plugin-wasm": "^3.3.0",
|
|
78
|
-
"vitest": "^1.2.
|
|
79
|
-
"@dxos/
|
|
80
|
-
"@dxos/
|
|
81
|
-
"@dxos/
|
|
82
|
-
"@dxos/random": "0.4.
|
|
83
|
-
"@dxos/
|
|
84
|
-
"@dxos/react-ui": "0.4.
|
|
85
|
-
"@dxos/storybook-utils": "0.4.
|
|
86
|
-
"@dxos/
|
|
79
|
+
"vitest": "^1.2.2",
|
|
80
|
+
"@dxos/config": "0.4.5-main.17763ce",
|
|
81
|
+
"@dxos/echo-signals": "0.4.5-main.17763ce",
|
|
82
|
+
"@dxos/echo-typegen": "0.4.5-main.17763ce",
|
|
83
|
+
"@dxos/random": "0.4.5-main.17763ce",
|
|
84
|
+
"@dxos/react-client": "0.4.5-main.17763ce",
|
|
85
|
+
"@dxos/react-ui": "0.4.5-main.17763ce",
|
|
86
|
+
"@dxos/storybook-utils": "0.4.5-main.17763ce",
|
|
87
|
+
"@dxos/keyboard": "0.4.5-main.17763ce"
|
|
87
88
|
},
|
|
88
89
|
"peerDependencies": {
|
|
89
90
|
"@phosphor-icons/react": "^2.0.5",
|
|
91
|
+
"@vitest/browser": "^1.2.2",
|
|
92
|
+
"@vitest/ui": "^1.2.2",
|
|
90
93
|
"react": "^18.2.0",
|
|
91
94
|
"react-dom": "^18.2.0",
|
|
92
|
-
"@dxos/react-client": "0.4.
|
|
95
|
+
"@dxos/react-client": "0.4.5-main.17763ce"
|
|
93
96
|
},
|
|
94
97
|
"publishConfig": {
|
|
95
98
|
"access": "public"
|
|
@@ -19,19 +19,15 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
19
19
|
import { MarkdownEditor, type TextEditorProps } from './TextEditor';
|
|
20
20
|
import {
|
|
21
21
|
type CommentsOptions,
|
|
22
|
-
type LinkOptions,
|
|
23
22
|
autocomplete,
|
|
24
23
|
blast,
|
|
25
|
-
|
|
24
|
+
decorateMarkdown,
|
|
26
25
|
comments,
|
|
27
26
|
defaultOptions,
|
|
28
|
-
heading,
|
|
29
|
-
hr,
|
|
30
27
|
image,
|
|
31
|
-
|
|
28
|
+
linkTooltip,
|
|
32
29
|
mention,
|
|
33
30
|
table,
|
|
34
|
-
tasklist,
|
|
35
31
|
typewriter,
|
|
36
32
|
useComments,
|
|
37
33
|
formatting,
|
|
@@ -47,9 +43,11 @@ const str = (...lines: string[]) => lines.join('\n');
|
|
|
47
43
|
|
|
48
44
|
const num = () => faker.number.int({ min: 0, max: 9999 }).toLocaleString();
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
const img = '';
|
|
47
|
+
|
|
51
48
|
const text = {
|
|
52
49
|
tasks: str(
|
|
50
|
+
//
|
|
53
51
|
'## Tasks',
|
|
54
52
|
'',
|
|
55
53
|
'- [x] decorator',
|
|
@@ -61,6 +59,7 @@ const text = {
|
|
|
61
59
|
),
|
|
62
60
|
|
|
63
61
|
list: str(
|
|
62
|
+
//
|
|
64
63
|
'## List',
|
|
65
64
|
'',
|
|
66
65
|
'- new york',
|
|
@@ -70,12 +69,13 @@ const text = {
|
|
|
70
69
|
),
|
|
71
70
|
|
|
72
71
|
numbered: str(
|
|
72
|
+
//
|
|
73
73
|
'## Numbered',
|
|
74
74
|
'',
|
|
75
75
|
'1. one',
|
|
76
76
|
'2. two',
|
|
77
77
|
'3. three',
|
|
78
|
-
''
|
|
78
|
+
'',
|
|
79
79
|
),
|
|
80
80
|
|
|
81
81
|
code: str(
|
|
@@ -115,7 +115,7 @@ const text = {
|
|
|
115
115
|
'',
|
|
116
116
|
),
|
|
117
117
|
|
|
118
|
-
image: str('# Image', '',
|
|
118
|
+
image: str('# Image', '', img),
|
|
119
119
|
|
|
120
120
|
headings: str(
|
|
121
121
|
...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
|
|
@@ -123,7 +123,7 @@ const text = {
|
|
|
123
123
|
|
|
124
124
|
paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
|
|
125
125
|
|
|
126
|
-
footer: str('', '', '', '', '')
|
|
126
|
+
footer: str('', '', '', '', ''),
|
|
127
127
|
};
|
|
128
128
|
|
|
129
129
|
const document = str(
|
|
@@ -131,6 +131,12 @@ const document = str(
|
|
|
131
131
|
'',
|
|
132
132
|
'> This is a block quote.',
|
|
133
133
|
'',
|
|
134
|
+
'> This is a long wrapping block quote. Neque reiciendis ullam quae error labore sit, at, et, nulla, aut at nostrum omnis quas nostrum, at consectetur vitae eos asperiores non omnis ullam in beatae at vitae deserunt asperiores sapiente.',
|
|
135
|
+
'',
|
|
136
|
+
'> This is',
|
|
137
|
+
'> a multi-line',
|
|
138
|
+
'> block quote.',
|
|
139
|
+
'',
|
|
134
140
|
'This is all about https://dxos.org and related technologies.',
|
|
135
141
|
'',
|
|
136
142
|
'This this is **bold**, ~~strikethrough~~, _italic_, and `f(INLINE)`.',
|
|
@@ -167,7 +173,7 @@ const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', '
|
|
|
167
173
|
const hover =
|
|
168
174
|
'rounded-sm text-base text-primary-600 hover:text-primary-500 dark:text-primary-300 hover:dark:text-primary-200';
|
|
169
175
|
|
|
170
|
-
const
|
|
176
|
+
const renderLinkTooltip = (el: Element, url: string) => {
|
|
171
177
|
const web = new URL(url);
|
|
172
178
|
createRoot(el).render(
|
|
173
179
|
<StrictMode>
|
|
@@ -200,7 +206,7 @@ const onCommentsHover: CommentsOptions['onHover'] = (el, shortcut) => {
|
|
|
200
206
|
);
|
|
201
207
|
};
|
|
202
208
|
|
|
203
|
-
const
|
|
209
|
+
const renderLinkButton = (el: Element, url: string) => {
|
|
204
210
|
createRoot(el).render(
|
|
205
211
|
<StrictMode>
|
|
206
212
|
<a href={url} target='_blank' rel='noreferrer' className={hover}>
|
|
@@ -251,14 +257,11 @@ const defaults = [
|
|
|
251
257
|
autocomplete({
|
|
252
258
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
253
259
|
}),
|
|
254
|
-
|
|
260
|
+
decorateMarkdown({ renderLinkButton }),
|
|
255
261
|
formatting(),
|
|
256
|
-
heading(),
|
|
257
|
-
hr(),
|
|
258
262
|
image(),
|
|
259
|
-
|
|
263
|
+
linkTooltip(renderLinkTooltip),
|
|
260
264
|
table(),
|
|
261
|
-
tasklist(),
|
|
262
265
|
];
|
|
263
266
|
|
|
264
267
|
export const Default = {
|
|
@@ -275,6 +278,11 @@ export const NoExtensions = {
|
|
|
275
278
|
|
|
276
279
|
const large = faker.helpers.multiple(() => faker.lorem.paragraph({ min: 8, max: 16 }), { count: 20 }).join('\n\n');
|
|
277
280
|
|
|
281
|
+
const largeWithImages = faker.helpers
|
|
282
|
+
.multiple(() => [faker.lorem.paragraph({ min: 12, max: 16 }), img], { count: 20 })
|
|
283
|
+
.flatMap((x) => x)
|
|
284
|
+
.join('\n\n');
|
|
285
|
+
|
|
278
286
|
export const Empty = {
|
|
279
287
|
render: () => <Story />,
|
|
280
288
|
};
|
|
@@ -283,26 +291,25 @@ export const Scrolling = {
|
|
|
283
291
|
render: () => <Story text={str('# Large Document', '', large)} extensions={[]} />,
|
|
284
292
|
};
|
|
285
293
|
|
|
286
|
-
export const
|
|
287
|
-
render: () => (
|
|
288
|
-
<Story
|
|
289
|
-
text={str(text.links, text.footer)}
|
|
290
|
-
extensions={[link({ onHover: onHoverLinkTooltip, onRender: onRenderLink })]}
|
|
291
|
-
/>
|
|
292
|
-
),
|
|
294
|
+
export const ScrollingWithImages = {
|
|
295
|
+
render: () => <Story text={str('# Large Document', '', largeWithImages)} extensions={[image()]} />,
|
|
293
296
|
};
|
|
294
297
|
|
|
295
|
-
export const
|
|
296
|
-
render: () => <Story text={str(text.
|
|
298
|
+
export const Links = {
|
|
299
|
+
render: () => <Story text={str(text.links, text.footer)} extensions={[linkTooltip(renderLinkTooltip)]} />,
|
|
297
300
|
};
|
|
298
301
|
|
|
299
302
|
export const Image = {
|
|
300
303
|
render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
|
|
301
304
|
};
|
|
302
305
|
|
|
306
|
+
export const Code = {
|
|
307
|
+
render: () => <Story text={str(text.code, text.footer)} extensions={[decorateMarkdown()]} />,
|
|
308
|
+
};
|
|
309
|
+
|
|
303
310
|
export const Lists = {
|
|
304
311
|
render: () => (
|
|
305
|
-
<Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[
|
|
312
|
+
<Story text={str(text.tasks, '', text.list, '', text.numbered, text.footer)} extensions={[decorateMarkdown()]} />
|
|
306
313
|
),
|
|
307
314
|
};
|
|
308
315
|
|
|
@@ -324,7 +331,7 @@ export const Autocomplete = {
|
|
|
324
331
|
<Story
|
|
325
332
|
text={str('# Autocomplete', '', 'Press Ctrl-Space...', text.footer)}
|
|
326
333
|
extensions={[
|
|
327
|
-
|
|
334
|
+
decorateMarkdown({ renderLinkButton }),
|
|
328
335
|
autocomplete({
|
|
329
336
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
330
337
|
}),
|
|
@@ -382,15 +389,6 @@ export const Comments = {
|
|
|
382
389
|
},
|
|
383
390
|
};
|
|
384
391
|
|
|
385
|
-
export const HorizontalRule = {
|
|
386
|
-
render: () => (
|
|
387
|
-
<Story
|
|
388
|
-
text={str('# Horizontal Rule', '', text.paragraphs, '---', text.paragraphs, '---', text.paragraphs)}
|
|
389
|
-
extensions={[hr()]}
|
|
390
|
-
/>
|
|
391
|
-
),
|
|
392
|
-
};
|
|
393
|
-
|
|
394
392
|
export const Vim = {
|
|
395
393
|
render: () => (
|
|
396
394
|
<Story
|
|
@@ -15,6 +15,7 @@ import React, {
|
|
|
15
15
|
useImperativeHandle,
|
|
16
16
|
useRef,
|
|
17
17
|
useState,
|
|
18
|
+
useMemo,
|
|
18
19
|
} from 'react';
|
|
19
20
|
|
|
20
21
|
import { log } from '@dxos/log';
|
|
@@ -172,7 +173,8 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
172
173
|
newView?.destroy();
|
|
173
174
|
setView(null);
|
|
174
175
|
};
|
|
175
|
-
|
|
176
|
+
// TODO(wittjosiah): Does `rootRef` ever change? Only `.current` changes?
|
|
177
|
+
}, [rootRef, extensions, model, readonly, themeMode]);
|
|
176
178
|
|
|
177
179
|
// Focus editor on Enter (e.g., when tabbing to this component).
|
|
178
180
|
const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
|
@@ -203,14 +205,21 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
203
205
|
);
|
|
204
206
|
|
|
205
207
|
export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
206
|
-
(
|
|
208
|
+
(
|
|
209
|
+
{ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions: _extensions, ...props },
|
|
210
|
+
forwardedRef,
|
|
211
|
+
) => {
|
|
207
212
|
const { themeMode } = useThemeContext();
|
|
208
213
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
214
|
+
const extensions = useMemo(
|
|
215
|
+
() => [createBasicBundle({ themeMode, placeholder, lineWrapping }), ...(_extensions ?? [])],
|
|
216
|
+
[themeMode, placeholder, lineWrapping, _extensions],
|
|
217
|
+
);
|
|
209
218
|
return (
|
|
210
219
|
<BaseTextEditor
|
|
211
220
|
ref={forwardedRef}
|
|
212
221
|
readonly={readonly}
|
|
213
|
-
extensions={
|
|
222
|
+
extensions={extensions}
|
|
214
223
|
theme={theme}
|
|
215
224
|
slots={updatedSlots}
|
|
216
225
|
{...props}
|
|
@@ -220,14 +229,18 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
220
229
|
);
|
|
221
230
|
|
|
222
231
|
export const MarkdownEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
223
|
-
({ readonly, placeholder, theme = markdownTheme, slots, extensions
|
|
232
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions: _extensions, ...props }, forwardedRef) => {
|
|
224
233
|
const { themeMode } = useThemeContext();
|
|
225
234
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
235
|
+
const extensions = useMemo(
|
|
236
|
+
() => [createMarkdownExtensions({ themeMode, placeholder }), ...(_extensions ?? [])],
|
|
237
|
+
[themeMode, placeholder, _extensions],
|
|
238
|
+
);
|
|
226
239
|
return (
|
|
227
240
|
<BaseTextEditor
|
|
228
241
|
ref={forwardedRef}
|
|
229
242
|
readonly={readonly}
|
|
230
|
-
extensions={
|
|
243
|
+
extensions={extensions}
|
|
231
244
|
theme={theme}
|
|
232
245
|
slots={updatedSlots}
|
|
233
246
|
{...props}
|
|
@@ -8,7 +8,7 @@ import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-networ
|
|
|
8
8
|
import '@preact/signals-react';
|
|
9
9
|
import { EditorView } from '@codemirror/view'; // Register react integration.
|
|
10
10
|
import get from 'lodash.get';
|
|
11
|
-
import React, { useEffect, useState } from 'react';
|
|
11
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
12
12
|
|
|
13
13
|
import { type Prop } from '@dxos/automerge/automerge';
|
|
14
14
|
import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
|
|
@@ -37,17 +37,20 @@ type EditorProps = {
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
const Editor = ({ handle, path = ['text'] }: EditorProps) => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
doc: get(handle.docSync()!, path),
|
|
43
|
-
extensions: [
|
|
44
|
-
//
|
|
40
|
+
const extensions = useMemo(
|
|
41
|
+
() => [
|
|
45
42
|
EditorView.baseTheme(defaultTheme),
|
|
46
43
|
EditorView.editorAttributes.of({ class: 'p-2 bg-white' }),
|
|
47
44
|
createBasicBundle({ placeholder: 'Type here...' }),
|
|
48
45
|
automerge({ handle, path }),
|
|
49
46
|
awareness(),
|
|
50
47
|
],
|
|
48
|
+
[handle, path],
|
|
49
|
+
);
|
|
50
|
+
const { parentRef } = useTextEditor({
|
|
51
|
+
autoFocus: true,
|
|
52
|
+
doc: get(handle.docSync()!, path),
|
|
53
|
+
extensions,
|
|
51
54
|
});
|
|
52
55
|
|
|
53
56
|
return <div ref={parentRef} />;
|
|
@@ -4,27 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
|
-
import React from 'react';
|
|
7
|
+
import React, { useMemo, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import { useThemeContext } from '@dxos/react-ui';
|
|
9
|
+
import { Toolbar as NaturalToolbar, Select, useThemeContext } from '@dxos/react-ui';
|
|
10
10
|
import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
11
|
import { withTheme } from '@dxos/storybook-utils';
|
|
12
12
|
|
|
13
13
|
import { TextEditor } from './TextEditor';
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
type EditorMode,
|
|
16
|
+
EditorModes,
|
|
17
|
+
decorateMarkdown,
|
|
16
18
|
createMarkdownExtensions,
|
|
17
19
|
formatting,
|
|
18
|
-
heading,
|
|
19
|
-
hr,
|
|
20
20
|
image,
|
|
21
|
-
link,
|
|
22
21
|
table,
|
|
23
|
-
tasklist,
|
|
24
22
|
useFormattingState,
|
|
25
23
|
} from '../../extensions';
|
|
26
24
|
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
27
|
-
import { editorFillLayoutEditor,
|
|
25
|
+
import { editorFillLayoutEditor, editorWithToolbarLayout } from '../../styles';
|
|
28
26
|
import { markdownTheme } from '../../themes';
|
|
29
27
|
import translations from '../../translations';
|
|
30
28
|
import { Toolbar } from '../Toolbar';
|
|
@@ -45,54 +43,78 @@ type StoryProps = {
|
|
|
45
43
|
const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
46
44
|
const { themeMode } = useThemeContext();
|
|
47
45
|
const [formattingState, trackFormatting] = useFormattingState();
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
46
|
+
const [editorMode, setEditorMode] = useState<EditorMode>('default');
|
|
47
|
+
const extensions = useMemo(
|
|
48
|
+
() => [
|
|
49
|
+
editorMode ? EditorModes[editorMode] : [],
|
|
53
50
|
createDataExtensions({ readonly }),
|
|
54
51
|
createThemeExtensions({
|
|
55
52
|
themeMode,
|
|
56
53
|
theme: markdownTheme,
|
|
57
54
|
slots: {
|
|
58
|
-
|
|
55
|
+
content: { className: '!p-4' },
|
|
59
56
|
},
|
|
60
57
|
}),
|
|
61
58
|
// TODO(burdon): Move lineWrapping.
|
|
62
59
|
createMarkdownExtensions({ placeholder, lineWrapping: true }),
|
|
63
60
|
// TODO(burdon): Move into markdown bundle (with React callbacks).
|
|
64
|
-
|
|
61
|
+
decorateMarkdown(),
|
|
65
62
|
formatting(),
|
|
66
|
-
heading(),
|
|
67
|
-
hr(),
|
|
68
63
|
image(),
|
|
69
|
-
link(),
|
|
70
64
|
table(),
|
|
71
|
-
tasklist(),
|
|
72
65
|
trackFormatting,
|
|
73
66
|
],
|
|
74
|
-
|
|
67
|
+
[editorMode, themeMode, placeholder, trackFormatting, readonly],
|
|
68
|
+
);
|
|
69
|
+
const { parentRef, view } = useTextEditor({ autoFocus, doc, extensions });
|
|
75
70
|
|
|
76
71
|
const handleAction = useActionHandler(view);
|
|
77
72
|
|
|
78
|
-
//
|
|
79
|
-
// sure if view is even guaranteed to exist at this point.
|
|
73
|
+
// TODO(marijn) This doesn't update the state on view changes.
|
|
74
|
+
// Also not sure if view is even guaranteed to exist at this point.
|
|
80
75
|
return (
|
|
81
76
|
<div role='none' className={mx('fixed inset-0', editorWithToolbarLayout)}>
|
|
82
77
|
<Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
|
|
83
78
|
<Toolbar.Markdown />
|
|
79
|
+
<EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
|
|
84
80
|
</Toolbar.Root>
|
|
85
81
|
<div role='none' className='overflow-y-auto'>
|
|
86
|
-
<div
|
|
87
|
-
role='textbox'
|
|
88
|
-
className={mx(textBlockWidth, attentionSurface, editorFillLayoutRoot, 'p-4')}
|
|
89
|
-
ref={parentRef}
|
|
90
|
-
/>
|
|
82
|
+
<div role='textbox' className={mx(textBlockWidth, attentionSurface, editorFillLayoutEditor)} ref={parentRef} />
|
|
91
83
|
</div>
|
|
92
84
|
</div>
|
|
93
85
|
);
|
|
94
86
|
};
|
|
95
87
|
|
|
88
|
+
const EditorModeToolbar = ({
|
|
89
|
+
editorMode,
|
|
90
|
+
setEditorMode,
|
|
91
|
+
}: {
|
|
92
|
+
editorMode: EditorMode;
|
|
93
|
+
setEditorMode: (mode: EditorMode) => void;
|
|
94
|
+
}) => {
|
|
95
|
+
return (
|
|
96
|
+
<Select.Root value={editorMode} onValueChange={(value) => setEditorMode(value as EditorMode)}>
|
|
97
|
+
<NaturalToolbar.Button asChild>
|
|
98
|
+
<Select.TriggerButton variant='ghost'>{editorMode}</Select.TriggerButton>
|
|
99
|
+
</NaturalToolbar.Button>
|
|
100
|
+
<Select.Portal>
|
|
101
|
+
<Select.Content>
|
|
102
|
+
<Select.ScrollUpButton />
|
|
103
|
+
<Select.Viewport>
|
|
104
|
+
{['default', 'vim'].map((mode) => (
|
|
105
|
+
<Select.Option key={mode} value={mode}>
|
|
106
|
+
{mode}
|
|
107
|
+
</Select.Option>
|
|
108
|
+
))}
|
|
109
|
+
</Select.Viewport>
|
|
110
|
+
<Select.ScrollDownButton />
|
|
111
|
+
<Select.Arrow />
|
|
112
|
+
</Select.Content>
|
|
113
|
+
</Select.Portal>
|
|
114
|
+
</Select.Root>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
96
118
|
export default {
|
|
97
119
|
title: 'react-ui-editor/useTextEditor',
|
|
98
120
|
component: TextEditor,
|
|
@@ -14,13 +14,11 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
14
14
|
|
|
15
15
|
import { Toolbar } from './Toolbar';
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
decorateMarkdown,
|
|
18
18
|
comments,
|
|
19
19
|
formatting,
|
|
20
|
-
heading,
|
|
21
20
|
image,
|
|
22
21
|
table,
|
|
23
|
-
tasklist,
|
|
24
22
|
useComments,
|
|
25
23
|
useFormattingState,
|
|
26
24
|
} from '../../extensions';
|
|
@@ -39,7 +37,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
39
37
|
const [formattingState, formattingObserver] = useFormattingState();
|
|
40
38
|
const extensions = useMemo(
|
|
41
39
|
() => [
|
|
42
|
-
|
|
40
|
+
decorateMarkdown(),
|
|
43
41
|
comments({
|
|
44
42
|
onCreate: ({ cursor }) => {
|
|
45
43
|
const id = PublicKey.random().toHex();
|
|
@@ -48,10 +46,8 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
48
46
|
},
|
|
49
47
|
}),
|
|
50
48
|
formatting(),
|
|
51
|
-
heading(),
|
|
52
49
|
image(),
|
|
53
50
|
table(),
|
|
54
|
-
tasklist(),
|
|
55
51
|
formattingObserver,
|
|
56
52
|
],
|
|
57
53
|
[],
|
|
@@ -76,8 +72,9 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
76
72
|
model={model}
|
|
77
73
|
extensions={extensions}
|
|
78
74
|
slots={{
|
|
79
|
-
root: { className: mx(textBlockWidth, editorFillLayoutRoot
|
|
75
|
+
root: { className: mx(textBlockWidth, editorFillLayoutRoot) },
|
|
80
76
|
editor: { className: editorFillLayoutEditor },
|
|
77
|
+
content: { className: '!pli-2' },
|
|
81
78
|
}}
|
|
82
79
|
/>
|
|
83
80
|
</div>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// Based upon @acheronfail’s implementation, fetched 16 Feb 2024
|
|
6
|
+
// https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448/5
|
|
7
|
+
|
|
8
|
+
import { type EditorState, type Extension, Facet, Transaction } from '@codemirror/state';
|
|
9
|
+
import { EditorView } from '@codemirror/view';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Number of lines above/below the cursor to keep visible in the viewport.
|
|
13
|
+
* Defaults to 3.
|
|
14
|
+
*/
|
|
15
|
+
export const cursorLineMarginFacet = Facet.define<number, number>({
|
|
16
|
+
combine: (input) => input[0] ?? 3,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const annotation = 'cursorLineMargin';
|
|
20
|
+
const lineAtPos = (s: EditorState, pos: number) => s.doc.lineAt(pos).number;
|
|
21
|
+
// seems to the best approximation of CM5's `cursorScrollMargin`
|
|
22
|
+
// https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448
|
|
23
|
+
export const cursorLineMargin: Extension = EditorView.updateListener.of(
|
|
24
|
+
({ transactions, state, selectionSet, startState, view }) => {
|
|
25
|
+
// make sure we don't trigger an infinite loop and ignore our own changes
|
|
26
|
+
if (transactions.length < 1 || transactions.some((tr) => tr.isUserEvent(annotation))) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const s = state;
|
|
31
|
+
const { main } = s.selection;
|
|
32
|
+
const cursorLineMargin = s.facet(cursorLineMarginFacet);
|
|
33
|
+
|
|
34
|
+
// editor rect
|
|
35
|
+
const rect = view.dom.getBoundingClientRect();
|
|
36
|
+
// top and bottom pixel positions of the visible region of the editor
|
|
37
|
+
const viewportTop = rect.top - view.documentTop;
|
|
38
|
+
const viewportBottom = rect.bottom - view.documentTop;
|
|
39
|
+
|
|
40
|
+
// line with main selection
|
|
41
|
+
const mainLine = lineAtPos(s, main.head);
|
|
42
|
+
// top most visible line
|
|
43
|
+
const _visTopLine = lineAtPos(s, view.lineBlockAtHeight(viewportTop).from);
|
|
44
|
+
|
|
45
|
+
// bottom most visible line
|
|
46
|
+
// NOTE: calculating this is slightly more complex - if the editor is
|
|
47
|
+
// larger than all the lines in it, and the `scrollPastEnd()` extension is
|
|
48
|
+
// enabled, then we need to calculate where the bottom most visible line
|
|
49
|
+
// should be if it extended all the way to the bottom of the editor
|
|
50
|
+
const botLineBlk = view.lineBlockAtHeight(viewportBottom);
|
|
51
|
+
const visBotLineDoc = lineAtPos(s, Math.min(botLineBlk.to, s.doc.length));
|
|
52
|
+
const visBotLine =
|
|
53
|
+
botLineBlk.bottom >= viewportBottom
|
|
54
|
+
? visBotLineDoc
|
|
55
|
+
: visBotLineDoc + Math.floor((viewportBottom - botLineBlk.bottom) / view.defaultLineHeight);
|
|
56
|
+
|
|
57
|
+
// const needsScrollTop = mainLine <= visTopLine + cursorLineMargin;
|
|
58
|
+
const needsScrollTop = false;
|
|
59
|
+
const needsScrollBot = mainLine >= visBotLine - cursorLineMargin;
|
|
60
|
+
|
|
61
|
+
// the scroll margins are overlapping
|
|
62
|
+
if (needsScrollTop && needsScrollBot) {
|
|
63
|
+
console.warn('is cursorScrollMargin too large for the editor size?');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// no need to scroll, we're in between scroll regions
|
|
68
|
+
if (!needsScrollTop && !needsScrollBot) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// if we're within the margin, but moving the other way, then don't scroll
|
|
73
|
+
if (selectionSet) {
|
|
74
|
+
const { head } = startState.selection.main;
|
|
75
|
+
const oldMainLine = lineAtPos(startState, head);
|
|
76
|
+
if (needsScrollTop && oldMainLine < mainLine) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (needsScrollBot && oldMainLine > mainLine) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
view.dispatch({
|
|
85
|
+
annotations: Transaction.userEvent.of(annotation),
|
|
86
|
+
effects: EditorView.scrollIntoView(s.selection.main.head, {
|
|
87
|
+
y: needsScrollTop ? 'start' : 'end',
|
|
88
|
+
yMargin: view.defaultLineHeight * cursorLineMargin,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
);
|