@dxos/react-ui-editor 0.3.11-main.f437bee → 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 +2776 -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 +13 -10
- 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 +115 -89
- 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,22 +46,26 @@ 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;
|
|
56
|
-
selection?: { anchor: number; head?: number };
|
|
57
|
+
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
57
58
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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;
|
|
61
66
|
slots?: TextEditorSlots;
|
|
67
|
+
extensions?: Extension[];
|
|
68
|
+
debug?: boolean;
|
|
62
69
|
};
|
|
63
70
|
|
|
64
71
|
/**
|
|
@@ -66,108 +73,134 @@ export type TextEditorProps = {
|
|
|
66
73
|
*/
|
|
67
74
|
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
68
75
|
(
|
|
69
|
-
{
|
|
76
|
+
{
|
|
77
|
+
model,
|
|
78
|
+
readonly,
|
|
79
|
+
autoFocus,
|
|
80
|
+
scrollTo,
|
|
81
|
+
selection,
|
|
82
|
+
editorMode,
|
|
83
|
+
theme,
|
|
84
|
+
slots = defaultSlots,
|
|
85
|
+
extensions = [],
|
|
86
|
+
debug,
|
|
87
|
+
},
|
|
70
88
|
forwardedRef,
|
|
71
89
|
) => {
|
|
72
|
-
|
|
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' });
|
|
73
96
|
const { themeMode } = useThemeContext();
|
|
74
97
|
|
|
75
|
-
// The editor view ref should only be used as an escape hatch.
|
|
76
98
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
77
99
|
const [view, setView] = useState<EditorView | null>(null);
|
|
78
|
-
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
79
|
-
|
|
80
|
-
// Focus.
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
if (view && focus) {
|
|
83
|
-
view.focus();
|
|
84
|
-
}
|
|
85
|
-
}, [view, focus]);
|
|
86
100
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (awareness && peer) {
|
|
91
|
-
awareness.setLocalStateField('user', {
|
|
92
|
-
name: peer.name ?? generateName(peer.id),
|
|
93
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
94
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}, [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]);
|
|
98
104
|
|
|
99
|
-
//
|
|
105
|
+
// Set focus.
|
|
100
106
|
useEffect(() => {
|
|
101
|
-
if (
|
|
102
|
-
view.
|
|
103
|
-
|
|
104
|
-
|
|
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();
|
|
105
114
|
}
|
|
106
|
-
}, [view,
|
|
115
|
+
}, [view, autoFocus]);
|
|
107
116
|
|
|
117
|
+
// Create editor state and view.
|
|
118
|
+
// The view is recreated if the model or extensions are changed.
|
|
108
119
|
useEffect(() => {
|
|
109
120
|
if (!model || !rootRef.current) {
|
|
110
121
|
return;
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
//
|
|
124
|
+
//
|
|
125
|
+
// EditorState
|
|
114
126
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
127
|
+
//
|
|
115
128
|
const state = EditorState.create({
|
|
116
129
|
doc: model.text(),
|
|
117
130
|
selection,
|
|
118
131
|
extensions: [
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
133
|
+
EditorView.exceptionSink.of((err) => {
|
|
134
|
+
log.catch(err);
|
|
135
|
+
}),
|
|
123
136
|
|
|
124
137
|
// Theme.
|
|
125
|
-
// TODO(burdon): Make
|
|
138
|
+
// TODO(burdon): Make configurable.
|
|
126
139
|
EditorView.baseTheme(defaultTheme),
|
|
127
|
-
EditorView.theme(
|
|
140
|
+
EditorView.theme(theme ?? {}),
|
|
128
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),
|
|
129
148
|
|
|
130
149
|
// Storage and replication.
|
|
150
|
+
// NOTE: This must come before user extensions.
|
|
131
151
|
model.extension,
|
|
132
152
|
|
|
153
|
+
// TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
|
|
154
|
+
editorMode === 'vim' && vim(),
|
|
155
|
+
|
|
133
156
|
// Custom.
|
|
134
157
|
...extensions,
|
|
135
|
-
].filter(
|
|
158
|
+
].filter(isNotFalsy),
|
|
136
159
|
});
|
|
137
160
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
161
|
+
//
|
|
162
|
+
// EditorView
|
|
163
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
164
|
+
//
|
|
142
165
|
const newView = new EditorView({
|
|
143
|
-
parent: rootRef.current,
|
|
144
166
|
state,
|
|
145
|
-
|
|
167
|
+
parent: rootRef.current,
|
|
168
|
+
scrollTo,
|
|
169
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
146
170
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
171
|
+
dispatchTransactions: (trs, view) => {
|
|
172
|
+
if (debug) {
|
|
173
|
+
logChanges(trs);
|
|
174
|
+
}
|
|
175
|
+
view.update(trs);
|
|
176
|
+
},
|
|
150
177
|
});
|
|
151
178
|
|
|
179
|
+
view?.destroy();
|
|
152
180
|
setView(newView);
|
|
181
|
+
|
|
153
182
|
return () => {
|
|
154
183
|
newView?.destroy();
|
|
155
184
|
setView(null);
|
|
156
185
|
};
|
|
157
186
|
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
158
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?
|
|
159
191
|
const handleKeyUp = useCallback(
|
|
160
192
|
(event: KeyboardEvent) => {
|
|
161
193
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
162
194
|
switch (key) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// }
|
|
195
|
+
case 'Enter': {
|
|
196
|
+
view?.focus();
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
168
199
|
|
|
169
200
|
case 'Escape': {
|
|
170
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
201
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
202
|
+
view?.focus();
|
|
203
|
+
}
|
|
171
204
|
break;
|
|
172
205
|
}
|
|
173
206
|
}
|
|
@@ -178,28 +211,27 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
178
211
|
return (
|
|
179
212
|
<div
|
|
180
213
|
key={model.id}
|
|
181
|
-
|
|
214
|
+
role='none'
|
|
182
215
|
tabIndex={0}
|
|
183
|
-
{...slots
|
|
184
|
-
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
216
|
+
{...slots.root}
|
|
217
|
+
// {...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
185
218
|
onKeyUp={handleKeyUp}
|
|
219
|
+
ref={rootRef}
|
|
186
220
|
/>
|
|
187
221
|
);
|
|
188
222
|
},
|
|
189
223
|
);
|
|
190
224
|
|
|
191
225
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
192
|
-
({ readonly, extensions = [],
|
|
226
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
193
227
|
const { themeMode } = useThemeContext();
|
|
194
228
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
195
229
|
return (
|
|
196
230
|
<BaseTextEditor
|
|
197
231
|
ref={forwardedRef}
|
|
198
232
|
readonly={readonly}
|
|
199
|
-
extensions={[
|
|
200
|
-
|
|
201
|
-
...extensions,
|
|
202
|
-
]}
|
|
233
|
+
extensions={[createBasicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
234
|
+
theme={theme}
|
|
203
235
|
slots={updatedSlots}
|
|
204
236
|
{...props}
|
|
205
237
|
/>
|
|
@@ -207,19 +239,16 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
207
239
|
},
|
|
208
240
|
);
|
|
209
241
|
|
|
210
|
-
// TODO(burdon): Remove (Just provide bundle, slots).
|
|
211
242
|
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
212
|
-
({ readonly, extensions = [],
|
|
243
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
213
244
|
const { themeMode } = useThemeContext();
|
|
214
245
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
215
246
|
return (
|
|
216
247
|
<BaseTextEditor
|
|
217
248
|
ref={forwardedRef}
|
|
218
249
|
readonly={readonly}
|
|
219
|
-
extensions={[
|
|
220
|
-
|
|
221
|
-
...extensions,
|
|
222
|
-
]}
|
|
250
|
+
extensions={[createMarkdownExtensions({ themeMode, placeholder }), ...extensions]}
|
|
251
|
+
theme={theme}
|
|
223
252
|
slots={updatedSlots}
|
|
224
253
|
{...props}
|
|
225
254
|
/>
|
|
@@ -229,20 +258,17 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
229
258
|
|
|
230
259
|
export const defaultSlots: TextEditorSlots = {
|
|
231
260
|
root: {
|
|
232
|
-
className: mx('
|
|
261
|
+
className: mx('grow', focusRing),
|
|
262
|
+
},
|
|
263
|
+
editor: {
|
|
264
|
+
className: 'bs-full',
|
|
233
265
|
},
|
|
234
266
|
};
|
|
235
267
|
|
|
236
268
|
export const defaultTextSlots: TextEditorSlots = {
|
|
237
269
|
...defaultSlots,
|
|
238
|
-
editor: {
|
|
239
|
-
theme: textTheme,
|
|
240
|
-
},
|
|
241
270
|
};
|
|
242
271
|
|
|
243
272
|
export const defaultMarkdownSlots: TextEditorSlots = {
|
|
244
273
|
...defaultSlots,
|
|
245
|
-
editor: {
|
|
246
|
-
theme: markdownTheme,
|
|
247
|
-
},
|
|
248
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
|
+
};
|