@dxos/react-ui-editor 0.3.11-main.f14932f → 0.3.11-main.f4c1077
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 +2777 -1291
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +13 -15
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +16 -9
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +4 -4
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/{yjs.stories.d.ts → hooks.stories.d.ts} +13 -2
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +29 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +25 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +2 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- 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/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +9 -11
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +0 -1
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +8 -12
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts +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 +41 -20
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +2 -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 +58 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
- package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
- package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +8 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +25 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +4 -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/useEditorView.d.ts +17 -0
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +14 -32
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.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 +7 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +48 -37
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +83 -92
- package/src/components/TextEditor/TextEditor.stories.tsx +10 -43
- package/src/components/TextEditor/TextEditor.tsx +120 -86
- package/src/components/TextEditor/automerge.stories.tsx +1 -2
- package/src/components/TextEditor/hooks.stories.tsx +111 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +102 -0
- package/src/components/Toolbar/Toolbar.tsx +264 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +1 -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 +55 -46
- package/src/extensions/automerge/cursor.ts +5 -4
- package/src/extensions/automerge/defs.ts +13 -34
- package/src/extensions/automerge/index.ts +0 -1
- package/src/extensions/automerge/semaphore.ts +33 -32
- package/src/extensions/automerge/update-automerge.ts +14 -8
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +46 -49
- package/src/extensions/basic.ts +17 -10
- package/src/extensions/blast.ts +4 -1
- package/src/extensions/comments.ts +331 -265
- package/src/extensions/cursor.ts +50 -0
- package/src/extensions/index.ts +2 -7
- package/src/extensions/markdown/bundle.ts +23 -28
- package/src/extensions/markdown/code.ts +150 -0
- package/src/extensions/markdown/formatting.test.ts +482 -0
- package/src/extensions/markdown/formatting.ts +1237 -0
- package/src/extensions/markdown/heading.ts +59 -0
- package/src/extensions/markdown/highlight.ts +9 -23
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/{image.ts → markdown/image.ts} +2 -2
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +20 -11
- package/src/extensions/{table.ts → markdown/table.ts} +2 -2
- package/src/extensions/markdown/tasklist.ts +110 -0
- package/src/extensions/mention.ts +9 -2
- package/src/extensions/outliner.ts +12 -0
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +5 -2
- package/src/hooks/useActionHandler.ts +93 -0
- package/src/hooks/useEditorView.ts +29 -0
- package/src/hooks/useTextEditor.ts +153 -0
- package/src/hooks/useTextModel.ts +66 -59
- package/src/index.ts +1 -1
- package/src/styles/markdown.ts +9 -12
- package/src/themes/default.ts +26 -56
- package/src/util.ts +41 -0
- 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.map +0 -1
- package/dist/types/src/extensions/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -3
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -27
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.d.ts.map +0 -1
- package/dist/types/src/util/cursor.d.ts.map +0 -1
- package/dist/types/src/util/index.d.ts +0 -3
- package/dist/types/src/util/index.d.ts.map +0 -1
- package/dist/types/src/util/util.d.ts +0 -5
- package/dist/types/src/util/util.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 -98
- package/src/extensions/hr.ts +0 -73
- package/src/extensions/tasklist.ts +0 -119
- package/src/extensions/yjs/cursor.ts +0 -21
- 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 -178
- 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/src/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- package/src/util/util.ts +0 -18
- /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
- /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
- /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
- /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
- /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
- /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
|
@@ -4,17 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
|
-
import defaultsDeep from 'lodash.defaultsdeep';
|
|
8
7
|
import React, { useState } from 'react';
|
|
9
8
|
|
|
10
9
|
import { TextObject } from '@dxos/echo-schema';
|
|
11
10
|
import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
12
11
|
import { withTheme } from '@dxos/storybook-utils';
|
|
13
12
|
|
|
14
|
-
import {
|
|
13
|
+
import { TextEditor, type TextEditorProps } from './TextEditor';
|
|
15
14
|
import { listener } from '../../extensions';
|
|
16
15
|
import { useTextModel } from '../../hooks';
|
|
17
|
-
import { textTheme } from '../../themes';
|
|
18
16
|
|
|
19
17
|
const initialText = [
|
|
20
18
|
'# TextEditor',
|
|
@@ -30,7 +28,7 @@ const Story = ({
|
|
|
30
28
|
text,
|
|
31
29
|
automerge,
|
|
32
30
|
...props
|
|
33
|
-
}: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'slots'>) => {
|
|
31
|
+
}: { text?: string; automerge?: boolean } & Pick<TextEditorProps, 'extensions' | 'placeholder' | 'slots'>) => {
|
|
34
32
|
const [item] = useState({ text: new TextObject(text, undefined, undefined, { automerge }) });
|
|
35
33
|
const model = useTextModel({ text: item.text });
|
|
36
34
|
if (!model) {
|
|
@@ -39,10 +37,9 @@ const Story = ({
|
|
|
39
37
|
|
|
40
38
|
return (
|
|
41
39
|
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
42
|
-
<div className='flex justify-center
|
|
40
|
+
<div className='flex justify-center'>
|
|
43
41
|
<div className='flex flex-col w-[800px] py-16'>
|
|
44
42
|
<TextEditor model={model} {...props} />
|
|
45
|
-
<div className='flex shrink-0 h-[300px]'></div>
|
|
46
43
|
</div>
|
|
47
44
|
</div>
|
|
48
45
|
</div>
|
|
@@ -57,49 +54,19 @@ export default {
|
|
|
57
54
|
};
|
|
58
55
|
|
|
59
56
|
export const Default = {
|
|
60
|
-
render: () =>
|
|
61
|
-
<Story
|
|
62
|
-
slots={defaultsDeep(
|
|
63
|
-
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
64
|
-
defaultSlots,
|
|
65
|
-
)}
|
|
66
|
-
/>
|
|
67
|
-
),
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export const Listener = {
|
|
71
|
-
render: () => (
|
|
72
|
-
<Story
|
|
73
|
-
slots={defaultsDeep(
|
|
74
|
-
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
75
|
-
defaultSlots,
|
|
76
|
-
)}
|
|
77
|
-
extensions={[listener({ onChange: (text) => console.log(text) })]}
|
|
78
|
-
/>
|
|
79
|
-
),
|
|
57
|
+
render: () => <Story placeholder='Enter text...' />,
|
|
80
58
|
};
|
|
81
59
|
|
|
82
60
|
export const Text = {
|
|
83
|
-
render: () =>
|
|
84
|
-
<Story
|
|
85
|
-
text={initialText}
|
|
86
|
-
slots={defaultsDeep(
|
|
87
|
-
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
88
|
-
defaultSlots,
|
|
89
|
-
)}
|
|
90
|
-
/>
|
|
91
|
-
),
|
|
61
|
+
render: () => <Story text={initialText} placeholder='Enter text...' />,
|
|
92
62
|
};
|
|
93
63
|
|
|
94
64
|
export const Automerge = {
|
|
65
|
+
render: () => <Story text={initialText} placeholder='Enter text...' automerge />,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const Listener = {
|
|
95
69
|
render: () => (
|
|
96
|
-
<Story
|
|
97
|
-
automerge
|
|
98
|
-
text={initialText}
|
|
99
|
-
slots={defaultsDeep(
|
|
100
|
-
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
101
|
-
defaultSlots,
|
|
102
|
-
)}
|
|
103
|
-
/>
|
|
70
|
+
<Story placeholder='Enter text...' extensions={[listener({ onChange: (text) => console.log(text) })]} />
|
|
104
71
|
),
|
|
105
72
|
};
|
|
@@ -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 { focusRing, mx } from '@dxos/react-ui-theme';
|
|
24
|
+
import { isNotFalsy } from '@dxos/util';
|
|
24
25
|
|
|
25
|
-
import {
|
|
26
|
-
import { type
|
|
26
|
+
import { createBasicBundle, createMarkdownExtensions } 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
|
|
|
@@ -43,123 +46,161 @@ export type TextEditorSlots = {
|
|
|
43
46
|
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
44
47
|
editor?: {
|
|
45
48
|
className?: string;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
theme?: ThemeStyles;
|
|
49
|
+
};
|
|
50
|
+
content?: {
|
|
51
|
+
className?: string;
|
|
50
52
|
};
|
|
51
53
|
};
|
|
52
54
|
|
|
55
|
+
// TODO(burdon): Spellcheck?
|
|
53
56
|
export type TextEditorProps = {
|
|
54
|
-
model: EditorModel;
|
|
55
|
-
focus?: boolean;
|
|
57
|
+
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
56
58
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
autoFocus?: boolean;
|
|
60
|
+
lineWrapping?: boolean;
|
|
61
|
+
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
62
|
+
selection?: { anchor: number; head?: number };
|
|
63
|
+
editorMode?: EditorMode; // TODO(burdon): Factor out.
|
|
64
|
+
placeholder?: string;
|
|
65
|
+
theme?: ThemeStyles;
|
|
60
66
|
slots?: TextEditorSlots;
|
|
67
|
+
extensions?: Extension[];
|
|
68
|
+
debug?: boolean;
|
|
61
69
|
};
|
|
62
70
|
|
|
63
71
|
/**
|
|
64
72
|
* Base text editor.
|
|
65
73
|
*/
|
|
66
74
|
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
67
|
-
(
|
|
68
|
-
|
|
75
|
+
(
|
|
76
|
+
{
|
|
77
|
+
model,
|
|
78
|
+
readonly,
|
|
79
|
+
autoFocus,
|
|
80
|
+
scrollTo,
|
|
81
|
+
selection,
|
|
82
|
+
editorMode,
|
|
83
|
+
theme,
|
|
84
|
+
slots = defaultSlots,
|
|
85
|
+
extensions = [],
|
|
86
|
+
debug,
|
|
87
|
+
},
|
|
88
|
+
forwardedRef,
|
|
89
|
+
) => {
|
|
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' });
|
|
69
96
|
const { themeMode } = useThemeContext();
|
|
97
|
+
|
|
70
98
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
71
99
|
const [view, setView] = useState<EditorView | null>(null);
|
|
72
|
-
// NOTE: This doesn't update useRef.
|
|
73
|
-
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (focus && view) {
|
|
76
|
-
view.focus();
|
|
77
|
-
}
|
|
78
|
-
}, [focus, view]);
|
|
79
100
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (awareness && peer) {
|
|
84
|
-
awareness.setLocalStateField('user', {
|
|
85
|
-
name: peer.name ?? generateName(peer.id),
|
|
86
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
87
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}, [awareness, peer, themeMode]);
|
|
101
|
+
// The view ref can be used to focus the editor.
|
|
102
|
+
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
103
|
+
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
91
104
|
|
|
92
|
-
//
|
|
105
|
+
// Set focus.
|
|
93
106
|
useEffect(() => {
|
|
94
|
-
if (
|
|
95
|
-
view.
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
if (autoFocus && !view?.hasFocus) {
|
|
108
|
+
if (view?.state.selection.main?.from === 0) {
|
|
109
|
+
// Start at end of line.
|
|
110
|
+
const { to } = view.state.doc.lineAt(0);
|
|
111
|
+
view?.dispatch({ selection: { anchor: to } });
|
|
112
|
+
}
|
|
113
|
+
view?.focus();
|
|
98
114
|
}
|
|
99
|
-
}, [view,
|
|
115
|
+
}, [view, autoFocus]);
|
|
100
116
|
|
|
117
|
+
// Create editor state and view.
|
|
118
|
+
// The view is recreated if the model or extensions are changed.
|
|
101
119
|
useEffect(() => {
|
|
102
120
|
if (!model || !rootRef.current) {
|
|
103
121
|
return;
|
|
104
122
|
}
|
|
105
123
|
|
|
106
|
-
//
|
|
124
|
+
//
|
|
125
|
+
// EditorState
|
|
107
126
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
127
|
+
//
|
|
108
128
|
const state = EditorState.create({
|
|
109
129
|
doc: model.text(),
|
|
130
|
+
selection,
|
|
110
131
|
extensions: [
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
133
|
+
EditorView.exceptionSink.of((err) => {
|
|
134
|
+
log.catch(err);
|
|
135
|
+
}),
|
|
115
136
|
|
|
116
137
|
// Theme.
|
|
117
|
-
// TODO(burdon): Make
|
|
138
|
+
// TODO(burdon): Make configurable.
|
|
118
139
|
EditorView.baseTheme(defaultTheme),
|
|
119
|
-
EditorView.theme(
|
|
140
|
+
EditorView.theme(theme ?? {}),
|
|
120
141
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
142
|
+
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
143
|
+
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
144
|
+
|
|
145
|
+
// State.
|
|
146
|
+
EditorView.editable.of(!readonly),
|
|
147
|
+
EditorState.readOnly.of(!!readonly),
|
|
121
148
|
|
|
122
149
|
// Storage and replication.
|
|
150
|
+
// NOTE: This must come before user extensions.
|
|
123
151
|
model.extension,
|
|
124
152
|
|
|
153
|
+
// TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
|
|
154
|
+
editorMode === 'vim' && vim(),
|
|
155
|
+
|
|
125
156
|
// Custom.
|
|
126
157
|
...extensions,
|
|
127
|
-
].filter(
|
|
158
|
+
].filter(isNotFalsy),
|
|
128
159
|
});
|
|
129
160
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
161
|
+
//
|
|
162
|
+
// EditorView
|
|
163
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
164
|
+
//
|
|
134
165
|
const newView = new EditorView({
|
|
135
|
-
parent: rootRef.current,
|
|
136
166
|
state,
|
|
137
|
-
|
|
167
|
+
parent: rootRef.current,
|
|
168
|
+
scrollTo,
|
|
169
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
138
170
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
171
|
+
dispatchTransactions: (trs, view) => {
|
|
172
|
+
if (debug) {
|
|
173
|
+
logChanges(trs);
|
|
174
|
+
}
|
|
175
|
+
view.update(trs);
|
|
176
|
+
},
|
|
142
177
|
});
|
|
143
178
|
|
|
179
|
+
view?.destroy();
|
|
144
180
|
setView(newView);
|
|
181
|
+
|
|
145
182
|
return () => {
|
|
146
183
|
newView?.destroy();
|
|
147
184
|
setView(null);
|
|
148
185
|
};
|
|
149
186
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
150
187
|
|
|
188
|
+
// Handles tab/focus.
|
|
189
|
+
// Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
|
|
190
|
+
// TODO(burdon): Convert to keymap?
|
|
151
191
|
const handleKeyUp = useCallback(
|
|
152
192
|
(event: KeyboardEvent) => {
|
|
153
193
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
154
194
|
switch (key) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// }
|
|
195
|
+
case 'Enter': {
|
|
196
|
+
view?.focus();
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
160
199
|
|
|
161
200
|
case 'Escape': {
|
|
162
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
201
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
202
|
+
view?.focus();
|
|
203
|
+
}
|
|
163
204
|
break;
|
|
164
205
|
}
|
|
165
206
|
}
|
|
@@ -170,28 +211,27 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
170
211
|
return (
|
|
171
212
|
<div
|
|
172
213
|
key={model.id}
|
|
173
|
-
|
|
214
|
+
role='none'
|
|
174
215
|
tabIndex={0}
|
|
175
|
-
{...slots
|
|
176
|
-
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
216
|
+
{...slots.root}
|
|
217
|
+
// {...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
177
218
|
onKeyUp={handleKeyUp}
|
|
219
|
+
ref={rootRef}
|
|
178
220
|
/>
|
|
179
221
|
);
|
|
180
222
|
},
|
|
181
223
|
);
|
|
182
224
|
|
|
183
225
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
184
|
-
({ readonly, extensions = [],
|
|
226
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
185
227
|
const { themeMode } = useThemeContext();
|
|
186
228
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
187
229
|
return (
|
|
188
230
|
<BaseTextEditor
|
|
189
231
|
ref={forwardedRef}
|
|
190
232
|
readonly={readonly}
|
|
191
|
-
extensions={[
|
|
192
|
-
|
|
193
|
-
...extensions,
|
|
194
|
-
]}
|
|
233
|
+
extensions={[createBasicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
234
|
+
theme={theme}
|
|
195
235
|
slots={updatedSlots}
|
|
196
236
|
{...props}
|
|
197
237
|
/>
|
|
@@ -199,19 +239,16 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
199
239
|
},
|
|
200
240
|
);
|
|
201
241
|
|
|
202
|
-
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
203
242
|
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
204
|
-
({ readonly, extensions = [],
|
|
243
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
205
244
|
const { themeMode } = useThemeContext();
|
|
206
245
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
207
246
|
return (
|
|
208
247
|
<BaseTextEditor
|
|
209
248
|
ref={forwardedRef}
|
|
210
249
|
readonly={readonly}
|
|
211
|
-
extensions={[
|
|
212
|
-
|
|
213
|
-
...extensions,
|
|
214
|
-
]}
|
|
250
|
+
extensions={[createMarkdownExtensions({ themeMode, placeholder }), ...extensions]}
|
|
251
|
+
theme={theme}
|
|
215
252
|
slots={updatedSlots}
|
|
216
253
|
{...props}
|
|
217
254
|
/>
|
|
@@ -221,20 +258,17 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
221
258
|
|
|
222
259
|
export const defaultSlots: TextEditorSlots = {
|
|
223
260
|
root: {
|
|
224
|
-
className: mx('
|
|
261
|
+
className: mx('grow', focusRing),
|
|
262
|
+
},
|
|
263
|
+
editor: {
|
|
264
|
+
className: 'bs-full',
|
|
225
265
|
},
|
|
226
266
|
};
|
|
227
267
|
|
|
228
268
|
export const defaultTextSlots: TextEditorSlots = {
|
|
229
269
|
...defaultSlots,
|
|
230
|
-
editor: {
|
|
231
|
-
theme: textTheme,
|
|
232
|
-
},
|
|
233
270
|
};
|
|
234
271
|
|
|
235
272
|
export const defaultMarkdownSlots: TextEditorSlots = {
|
|
236
273
|
...defaultSlots,
|
|
237
|
-
editor: {
|
|
238
|
-
theme: markdownTheme,
|
|
239
|
-
},
|
|
240
274
|
};
|
|
@@ -13,7 +13,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
13
13
|
|
|
14
14
|
import { type Prop } from '@dxos/automerge/automerge';
|
|
15
15
|
import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
|
|
16
|
-
import { Filter
|
|
16
|
+
import { Filter } from '@dxos/echo-schema';
|
|
17
17
|
import { type PublicKey } from '@dxos/keys';
|
|
18
18
|
import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
|
|
19
19
|
import { useIdentity } from '@dxos/react-client/halo';
|
|
@@ -130,7 +130,6 @@ const EchoStory = ({ id, spaceKey }: { id: number; spaceKey: PublicKey }) => {
|
|
|
130
130
|
|
|
131
131
|
export const WithEcho = {
|
|
132
132
|
render: () => {
|
|
133
|
-
setGlobalAutomergePreference(true);
|
|
134
133
|
return (
|
|
135
134
|
<ClientRepeater
|
|
136
135
|
count={2}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
import { useThemeContext } from '@dxos/react-ui';
|
|
10
|
+
import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
11
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
12
|
+
|
|
13
|
+
import { TextEditor } from './TextEditor';
|
|
14
|
+
import {
|
|
15
|
+
code,
|
|
16
|
+
createMarkdownExtensions,
|
|
17
|
+
formatting,
|
|
18
|
+
heading,
|
|
19
|
+
hr,
|
|
20
|
+
image,
|
|
21
|
+
link,
|
|
22
|
+
table,
|
|
23
|
+
tasklist,
|
|
24
|
+
useFormattingState,
|
|
25
|
+
} from '../../extensions';
|
|
26
|
+
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
27
|
+
import { markdownTheme } from '../../themes';
|
|
28
|
+
import { Toolbar } from '../Toolbar';
|
|
29
|
+
|
|
30
|
+
// TODO(burdon): Demo toolbar with hooks.
|
|
31
|
+
// TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
|
|
32
|
+
// TODO(burdon): Remove BaseTextEditor.
|
|
33
|
+
// TODO(burdon): Move scrolling container layout/logic into TextEditor/MarkdownEditor components.
|
|
34
|
+
// TODO(burdon): Define keymap in composer framework format.
|
|
35
|
+
|
|
36
|
+
type StoryProps = {
|
|
37
|
+
autoFocus?: boolean;
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
doc?: string;
|
|
40
|
+
readonly?: boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
44
|
+
const { themeMode } = useThemeContext();
|
|
45
|
+
const [formattingState, trackFormatting] = useFormattingState();
|
|
46
|
+
const { parentRef, view } = useTextEditor({
|
|
47
|
+
autoFocus,
|
|
48
|
+
doc,
|
|
49
|
+
extensions: [
|
|
50
|
+
//
|
|
51
|
+
createDataExtensions({ readonly }),
|
|
52
|
+
createThemeExtensions({
|
|
53
|
+
themeMode,
|
|
54
|
+
theme: markdownTheme,
|
|
55
|
+
slots: {
|
|
56
|
+
// TODO(burdon): Document classes re base theme.
|
|
57
|
+
// Semantic tokens (e.g., replacement of input surface).
|
|
58
|
+
editor: {
|
|
59
|
+
className: 'h-full p-4 bg-white text-black dark:bg-black dark:text-white',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
// TODO(burdon): Move lineWrapping.
|
|
64
|
+
createMarkdownExtensions({ placeholder, lineWrapping: true }),
|
|
65
|
+
// TODO(burdon): Move into markdown bundle (with React callbacks).
|
|
66
|
+
code(),
|
|
67
|
+
formatting(),
|
|
68
|
+
heading(),
|
|
69
|
+
hr(),
|
|
70
|
+
image(),
|
|
71
|
+
link(),
|
|
72
|
+
table(),
|
|
73
|
+
tasklist(),
|
|
74
|
+
trackFormatting,
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const handleAction = useActionHandler(view);
|
|
79
|
+
|
|
80
|
+
// FIXME This doesn't update the state on view changes. Also not
|
|
81
|
+
// sure if view is even guaranteed to exist at this point.
|
|
82
|
+
return (
|
|
83
|
+
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
84
|
+
<div className='flex h-full justify-center'>
|
|
85
|
+
<div className='flex flex-col h-full w-[800px]'>
|
|
86
|
+
<Toolbar.Root onAction={handleAction} state={formattingState}>
|
|
87
|
+
<Toolbar.Markdown />
|
|
88
|
+
</Toolbar.Root>
|
|
89
|
+
|
|
90
|
+
{/* TODO(burdon): Handle scrolling in component wrapper (like this). */}
|
|
91
|
+
<div role='none' className='h-full overflow-y-auto' ref={parentRef} />
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default {
|
|
99
|
+
title: 'react-ui-editor/useTextEditor',
|
|
100
|
+
component: TextEditor,
|
|
101
|
+
decorators: [withTheme],
|
|
102
|
+
render: (args: StoryProps) => <Story {...args} />,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const Default = {
|
|
106
|
+
args: {
|
|
107
|
+
autoFocus: true,
|
|
108
|
+
placeholder: 'Text...',
|
|
109
|
+
doc: '# Demo\n\nThis is a document.\n\n',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import React, { type FC, useMemo, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { TextObject } from '@dxos/echo-schema';
|
|
10
|
+
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { faker } from '@dxos/random';
|
|
12
|
+
import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
13
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
14
|
+
|
|
15
|
+
import { Toolbar } from './Toolbar';
|
|
16
|
+
import {
|
|
17
|
+
code,
|
|
18
|
+
comments,
|
|
19
|
+
formatting,
|
|
20
|
+
heading,
|
|
21
|
+
image,
|
|
22
|
+
table,
|
|
23
|
+
tasklist,
|
|
24
|
+
useComments,
|
|
25
|
+
useFormattingState,
|
|
26
|
+
} from '../../extensions';
|
|
27
|
+
import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
|
|
28
|
+
import { MarkdownEditor } from '../TextEditor';
|
|
29
|
+
|
|
30
|
+
faker.seed(101);
|
|
31
|
+
|
|
32
|
+
const Story: FC<{ content: string }> = ({ content }) => {
|
|
33
|
+
const [item] = useState({ text: new TextObject(content) });
|
|
34
|
+
const [_comments, setComments] = useState<Comment[]>([]);
|
|
35
|
+
const [editorRef, editorView] = useEditorView();
|
|
36
|
+
const model = useTextModel({ text: item.text });
|
|
37
|
+
const [formattingState, formattingObserver] = useFormattingState();
|
|
38
|
+
const extensions = useMemo(
|
|
39
|
+
() => [
|
|
40
|
+
code(),
|
|
41
|
+
comments({
|
|
42
|
+
onCreate: ({ cursor }) => {
|
|
43
|
+
const id = PublicKey.random().toHex();
|
|
44
|
+
setComments((comments) => [...comments, { id, cursor }]);
|
|
45
|
+
return id;
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
formatting(),
|
|
49
|
+
heading(),
|
|
50
|
+
image(),
|
|
51
|
+
table(),
|
|
52
|
+
tasklist(),
|
|
53
|
+
formattingObserver,
|
|
54
|
+
],
|
|
55
|
+
[],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
useComments(editorView, _comments);
|
|
59
|
+
const handleAction = useActionHandler(editorView);
|
|
60
|
+
|
|
61
|
+
if (!model) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
67
|
+
<div className='flex h-full justify-center'>
|
|
68
|
+
<div className='flex flex-col h-full w-[800px]'>
|
|
69
|
+
<Toolbar.Root onAction={handleAction} state={formattingState}>
|
|
70
|
+
<Toolbar.Markdown />
|
|
71
|
+
<Toolbar.Separator />
|
|
72
|
+
<Toolbar.Extended />
|
|
73
|
+
</Toolbar.Root>
|
|
74
|
+
<MarkdownEditor ref={editorRef} model={model} extensions={extensions} />
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default {
|
|
82
|
+
title: 'react-ui-editor/Toolbar',
|
|
83
|
+
component: Toolbar,
|
|
84
|
+
render: (args: any) => <Story {...args} />,
|
|
85
|
+
decorators: [withTheme],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const content = [
|
|
89
|
+
'Demo',
|
|
90
|
+
'',
|
|
91
|
+
'The editor supports **Markdown** styles.',
|
|
92
|
+
'',
|
|
93
|
+
faker.lorem.paragraph({ min: 5, max: 8 }),
|
|
94
|
+
'',
|
|
95
|
+
'',
|
|
96
|
+
].join('\n');
|
|
97
|
+
|
|
98
|
+
export const Default = {
|
|
99
|
+
args: {
|
|
100
|
+
content,
|
|
101
|
+
},
|
|
102
|
+
};
|