@dxos/react-ui-editor 0.3.9-next.4584ca9 → 0.3.9-next.8ad58eb
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 +1027 -827
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts +2 -2
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts +2 -2
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-plugin/handle.d.ts +9 -0
- package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-plugin/index.d.ts +2 -2
- package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts +6 -5
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge.stories.d.ts +2 -1
- package/dist/types/src/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +24 -20
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +7 -24
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/{stories/html/code-mirror.stories.d.ts → src/components/TextEditor/codemirror.stories.d.ts} +2 -2
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts +8 -0
- package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +3 -0
- package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +10 -0
- package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/change.d.ts +7 -0
- package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +11 -0
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/{hyperlinkTooltip.d.ts → hyperlink/hyperlinkTooltip.d.ts} +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/{hyperlink.d.ts → hyperlink/hyperlinkWidget.d.ts} +3 -3
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +4 -0
- package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +3 -1
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +8 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +3 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -0
- package/dist/types/src/components/{Markdown/markdownTags.d.ts → TextEditor/extensions/markdown/tags.d.ts} +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +21 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/index.d.ts +1 -3
- package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +11 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +0 -3
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +4 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useCollaboration.d.ts +9 -0
- package/dist/types/src/hooks/useCollaboration.d.ts.map +1 -0
- package/dist/types/src/{model.d.ts → hooks/useTextModel.d.ts} +3 -15
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -0
- package/dist/types/src/hooks/yjs/cursors.d.ts.map +1 -0
- package/dist/types/src/hooks/yjs/index.d.ts.map +1 -0
- package/dist/types/src/hooks/yjs/space-provider.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts +4 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts +1 -1
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/package.json +31 -28
- package/src/automerge/automerge-plugin/PatchSemaphore.ts +5 -5
- package/src/automerge/automerge-plugin/codeMirrorToAm.ts +10 -7
- package/src/automerge/automerge-plugin/handle.ts +14 -0
- package/src/automerge/automerge-plugin/index.ts +2 -2
- package/src/automerge/automerge-plugin/plugin.ts +39 -10
- package/src/automerge/automerge.stories.tsx +32 -47
- package/src/components/TextEditor/TextEditor.stories.tsx +51 -55
- package/src/components/TextEditor/TextEditor.tsx +113 -55
- package/src/components/TextEditor/codemirror.stories.ts +114 -0
- package/src/components/TextEditor/extensions/basic/bundle.ts +28 -0
- package/src/components/TextEditor/extensions/basic/index.ts +6 -0
- package/src/components/TextEditor/extensions/basic/theme.ts +49 -0
- package/src/components/TextEditor/extensions/change.ts +22 -0
- package/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.ts +99 -0
- package/src/components/TextEditor/extensions/{hyperlinkTooltip.tsx → hyperlink/hyperlinkTooltip.tsx} +2 -17
- package/src/components/TextEditor/extensions/{hyperlink.tsx → hyperlink/hyperlinkWidget.tsx} +21 -10
- package/src/components/TextEditor/extensions/hyperlink/index.ts +7 -0
- package/src/components/TextEditor/extensions/index.ts +3 -1
- package/src/components/TextEditor/extensions/markdown/bundle.ts +99 -0
- package/src/components/TextEditor/extensions/markdown/index.ts +6 -0
- package/src/components/{Markdown/markdownTheme.ts → TextEditor/extensions/markdown/theme.ts} +23 -12
- package/src/components/TextEditor/index.ts +1 -4
- package/src/components/TextEditor/yjs.stories.tsx +55 -0
- package/src/components/index.ts +0 -3
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useCollaboration.ts +45 -0
- package/src/{model.ts → hooks/useTextModel.ts} +42 -17
- package/src/index.ts +3 -3
- package/src/styles/tokens.ts +6 -0
- package/src/testing/replicator.ts +1 -2
- package/dist/types/src/automerge/demo.d.ts +0 -25
- package/dist/types/src/automerge/demo.d.ts.map +0 -1
- package/dist/types/src/components/Editor/Editor.d.ts +0 -15
- package/dist/types/src/components/Editor/Editor.d.ts.map +0 -1
- package/dist/types/src/components/Editor/Editor.stories.d.ts +0 -29
- package/dist/types/src/components/Editor/Editor.stories.d.ts.map +0 -1
- package/dist/types/src/components/Editor/index.d.ts +0 -2
- package/dist/types/src/components/Editor/index.d.ts.map +0 -1
- package/dist/types/src/components/Markdown/Markdown.d.ts +0 -20
- package/dist/types/src/components/Markdown/Markdown.d.ts.map +0 -1
- package/dist/types/src/components/Markdown/Markdown.stories.d.ts +0 -20
- package/dist/types/src/components/Markdown/Markdown.stories.d.ts.map +0 -1
- package/dist/types/src/components/Markdown/index.d.ts +0 -2
- package/dist/types/src/components/Markdown/index.d.ts.map +0 -1
- package/dist/types/src/components/Markdown/markdownTags.d.ts.map +0 -1
- package/dist/types/src/components/Markdown/markdownTheme.d.ts +0 -93
- package/dist/types/src/components/Markdown/markdownTheme.d.ts.map +0 -1
- package/dist/types/src/components/RichText/RichText.d.ts +0 -20
- package/dist/types/src/components/RichText/RichText.d.ts.map +0 -1
- package/dist/types/src/components/RichText/RichText.stories.d.ts +0 -27
- package/dist/types/src/components/RichText/RichText.stories.d.ts.map +0 -1
- package/dist/types/src/components/RichText/index.d.ts +0 -2
- package/dist/types/src/components/RichText/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlinkTooltip.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/theme.d.ts +0 -8
- package/dist/types/src/components/TextEditor/theme.d.ts.map +0 -1
- package/dist/types/src/model.d.ts.map +0 -1
- package/dist/types/src/yjs/cursors.d.ts.map +0 -1
- package/dist/types/src/yjs/index.d.ts.map +0 -1
- package/dist/types/src/yjs/space-provider.d.ts.map +0 -1
- package/dist/types/stories/html/code-mirror.stories.d.ts.map +0 -1
- package/src/automerge/demo.ts +0 -106
- package/src/components/Editor/Editor.stories.tsx +0 -82
- package/src/components/Editor/Editor.tsx +0 -33
- package/src/components/Editor/index.ts +0 -5
- package/src/components/Markdown/Markdown.stories.tsx +0 -59
- package/src/components/Markdown/Markdown.tsx +0 -214
- package/src/components/Markdown/index.ts +0 -5
- package/src/components/RichText/RichText.stories.tsx +0 -70
- package/src/components/RichText/RichText.tsx +0 -181
- package/src/components/RichText/index.ts +0 -5
- package/src/components/TextEditor/theme.ts +0 -33
- /package/dist/types/src/{yjs → hooks/yjs}/cursors.d.ts +0 -0
- /package/dist/types/src/{yjs → hooks/yjs}/index.d.ts +0 -0
- /package/dist/types/src/{yjs → hooks/yjs}/space-provider.d.ts +0 -0
- /package/src/components/{Markdown/markdownTags.ts → TextEditor/extensions/markdown/tags.ts} +0 -0
- /package/src/{yjs → hooks/yjs}/cursors.ts +0 -0
- /package/src/{yjs → hooks/yjs}/index.ts +0 -0
- /package/src/{yjs → hooks/yjs}/space-provider.ts +0 -0
|
@@ -3,73 +3,65 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
6
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
7
|
+
import { ArrowCircleUp } from '@phosphor-icons/react';
|
|
8
|
+
import React, { StrictMode, useState } from 'react';
|
|
9
|
+
import { createRoot } from 'react-dom/client';
|
|
10
10
|
|
|
11
11
|
import { TextObject } from '@dxos/echo-schema';
|
|
12
|
-
import { fixedInsetFlexLayout, groupSurface, inputSurface, mx } from '@dxos/react-ui-theme';
|
|
12
|
+
import { fixedInsetFlexLayout, getSize, groupSurface, inputSurface, mx } from '@dxos/react-ui-theme';
|
|
13
13
|
import { withTheme } from '@dxos/storybook-utils';
|
|
14
14
|
|
|
15
15
|
import { TextEditor } from './TextEditor';
|
|
16
|
-
import {
|
|
17
|
-
import { useTextModel } from '../../
|
|
18
|
-
|
|
19
|
-
export const nameRegex = /\{([\w_]+)}/;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Simple Monaco language extension.
|
|
23
|
-
* https://github.com/codemirror/stream-parser/blob/main/test/test-stream-parser.ts
|
|
24
|
-
*/
|
|
25
|
-
export const promptLanguage = StreamLanguage.define<{ count: number }>({
|
|
26
|
-
startState: () => ({ count: 0 }),
|
|
16
|
+
import { createHyperlinkTooltip, hyperlinkDecoration } from './extensions';
|
|
17
|
+
import { useTextModel } from '../../hooks';
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
stream.next();
|
|
37
|
-
return null;
|
|
38
|
-
},
|
|
39
|
-
});
|
|
19
|
+
const text = [
|
|
20
|
+
'',
|
|
21
|
+
'',
|
|
22
|
+
'This is all about [DXOS](https://dxos.org); read more [here](https://docs.dxos.org/guide/getting-started.html).',
|
|
23
|
+
'',
|
|
24
|
+
'',
|
|
25
|
+
].join('\n');
|
|
40
26
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
),
|
|
53
|
-
},
|
|
54
|
-
]);
|
|
55
|
-
|
|
56
|
-
const Story = () => {
|
|
57
|
-
const [item] = useState({
|
|
58
|
-
text: new TextObject(
|
|
59
|
-
'\n\n\nThis is all about [DXOS](https://dxos.org); read more [here](https://docs.dxos.org/guide/getting-started.html).\n\n\n',
|
|
60
|
-
),
|
|
27
|
+
const hyperLinkTooltip = () =>
|
|
28
|
+
createHyperlinkTooltip((el, url) => {
|
|
29
|
+
const web = new URL(url);
|
|
30
|
+
createRoot(el).render(
|
|
31
|
+
<StrictMode>
|
|
32
|
+
<div className='flex gap-1 items-center'>
|
|
33
|
+
<ArrowCircleUp className={mx(getSize(6), 'text-blue-500')} />
|
|
34
|
+
<p className='pr-1'>{web.origin}</p>
|
|
35
|
+
</div>
|
|
36
|
+
</StrictMode>,
|
|
37
|
+
);
|
|
61
38
|
});
|
|
62
39
|
|
|
40
|
+
const Story = ({ automerge }: { automerge?: boolean }) => {
|
|
41
|
+
const [item] = useState({ text: new TextObject(text, undefined, undefined, { useAutomergeBackend: !!automerge }) });
|
|
63
42
|
const model = useTextModel({ text: item.text });
|
|
43
|
+
if (!model) {
|
|
44
|
+
return <></>;
|
|
45
|
+
}
|
|
64
46
|
|
|
65
47
|
return (
|
|
66
|
-
<div className={mx(fixedInsetFlexLayout, groupSurface
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
48
|
+
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
49
|
+
<div className='flex justify-center p-8'>
|
|
50
|
+
<div className='w-[800px]'>
|
|
51
|
+
<TextEditor
|
|
52
|
+
model={model}
|
|
53
|
+
extensions={[
|
|
54
|
+
markdown(),
|
|
55
|
+
hyperlinkDecoration({ link: false }),
|
|
56
|
+
hyperLinkTooltip(),
|
|
57
|
+
// EditorView.domEventHandlers({
|
|
58
|
+
// mousedown: (e, view) => {},
|
|
59
|
+
// }),
|
|
60
|
+
]}
|
|
61
|
+
slots={{ root: { className: mx(inputSurface, 'p-2') } }}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
73
65
|
</div>
|
|
74
66
|
);
|
|
75
67
|
};
|
|
@@ -81,3 +73,7 @@ export default {
|
|
|
81
73
|
};
|
|
82
74
|
|
|
83
75
|
export const Default = {};
|
|
76
|
+
|
|
77
|
+
export const Automerge = {
|
|
78
|
+
render: () => <Story automerge />,
|
|
79
|
+
};
|
|
@@ -2,28 +2,31 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { closeBrackets } from '@codemirror/autocomplete';
|
|
6
|
-
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
7
5
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
|
+
import { vim } from '@replit/codemirror-vim';
|
|
9
|
+
import defaultsDeep from 'lodash.defaultsdeep';
|
|
10
|
+
import get from 'lodash.get';
|
|
10
11
|
import React, {
|
|
11
12
|
type KeyboardEvent,
|
|
12
13
|
forwardRef,
|
|
14
|
+
useCallback,
|
|
13
15
|
useEffect,
|
|
14
16
|
useImperativeHandle,
|
|
15
17
|
useState,
|
|
16
|
-
|
|
17
|
-
type HTMLAttributes,
|
|
18
|
+
type ComponentProps,
|
|
18
19
|
} from 'react';
|
|
19
|
-
import { type StyleSpec } from 'style-mod';
|
|
20
|
-
import { yCollab } from 'y-codemirror.next';
|
|
21
20
|
|
|
21
|
+
import { isDocAccessor } from '@dxos/echo-schema';
|
|
22
22
|
import { useThemeContext } from '@dxos/react-ui';
|
|
23
|
-
import { YText } from '@dxos/text-model';
|
|
24
23
|
|
|
25
|
-
import {
|
|
26
|
-
import { type EditorModel,
|
|
24
|
+
import { basicBundle, basicTheme, markdownBundle, markdownTheme } from './extensions';
|
|
25
|
+
import { type EditorModel, useCollaboration } from '../../hooks';
|
|
26
|
+
import type { ThemeStyles } from '../../styles';
|
|
27
|
+
|
|
28
|
+
export const EditorModes = ['default', 'vim'] as const;
|
|
29
|
+
export type EditorMode = (typeof EditorModes)[number];
|
|
27
30
|
|
|
28
31
|
export type CursorInfo = {
|
|
29
32
|
from: number;
|
|
@@ -39,71 +42,81 @@ export type TextEditorRef = {
|
|
|
39
42
|
view?: EditorView;
|
|
40
43
|
};
|
|
41
44
|
|
|
45
|
+
export type TextEditorSlots = {
|
|
46
|
+
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
47
|
+
editor?: {
|
|
48
|
+
className?: string;
|
|
49
|
+
placeholder?: string;
|
|
50
|
+
spellCheck?: boolean;
|
|
51
|
+
tabIndex?: number;
|
|
52
|
+
theme?: ThemeStyles;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
42
56
|
export type TextEditorProps = {
|
|
43
|
-
model
|
|
57
|
+
model: EditorModel;
|
|
44
58
|
extensions?: Extension[];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
slots?: EditorSlots;
|
|
49
|
-
onKeyDown?: (event: KeyboardEvent, info: CursorInfo) => void;
|
|
50
|
-
} & Pick<HTMLAttributes<HTMLDivElement>, 'onBlur' | 'onFocus'>;
|
|
59
|
+
slots?: TextEditorSlots;
|
|
60
|
+
editorMode?: EditorMode;
|
|
61
|
+
};
|
|
51
62
|
|
|
52
63
|
/**
|
|
53
|
-
*
|
|
64
|
+
* Base text editor.
|
|
65
|
+
* NOTE: Rather than adding properties, try to create extensions that can be reused.
|
|
54
66
|
*/
|
|
55
|
-
export const
|
|
56
|
-
({ model, extensions = [],
|
|
57
|
-
const { id, content } = model ?? {};
|
|
67
|
+
export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
68
|
+
({ model, extensions = [], slots, editorMode }, forwardedRef) => {
|
|
58
69
|
const { themeMode } = useThemeContext();
|
|
70
|
+
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
59
71
|
|
|
60
72
|
const [parent, setParent] = useState<HTMLDivElement | null>(null);
|
|
61
73
|
const [state, setState] = useState<EditorState>();
|
|
62
74
|
const [view, setView] = useState<EditorView>();
|
|
63
|
-
|
|
64
|
-
// TODO(burdon): The ref may be instantiated before the view is created.
|
|
65
75
|
useImperativeHandle(
|
|
66
76
|
forwardedRef,
|
|
67
|
-
() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
[view],
|
|
77
|
+
() => ({
|
|
78
|
+
editor: parent,
|
|
79
|
+
state,
|
|
80
|
+
view,
|
|
81
|
+
}),
|
|
82
|
+
[view, state, parent],
|
|
75
83
|
);
|
|
76
84
|
|
|
85
|
+
// TODO(burdon): Pass in extension?
|
|
86
|
+
const collaboration = useCollaboration(model, themeMode);
|
|
87
|
+
|
|
77
88
|
useEffect(() => {
|
|
78
89
|
if (!parent) {
|
|
79
90
|
return;
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
|
|
93
|
+
const initialText = isDocAccessor(model.content)
|
|
94
|
+
? get(model.content.handle.docSync(), model.content.path)
|
|
95
|
+
: model.content?.toString();
|
|
83
96
|
|
|
84
97
|
const state = EditorState.create({
|
|
85
|
-
doc:
|
|
98
|
+
doc: initialText,
|
|
86
99
|
extensions: [
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
placeholder(slots.editor?.placeholder ?? ''),
|
|
90
|
-
EditorView.lineWrapping,
|
|
91
|
-
|
|
92
|
-
// Themes.
|
|
93
|
-
EditorView.theme(theme),
|
|
94
|
-
...(themeMode === 'dark'
|
|
95
|
-
? [syntaxHighlighting(oneDarkHighlightStyle)]
|
|
96
|
-
: [syntaxHighlighting(defaultHighlightStyle)]),
|
|
100
|
+
// TODO(burdon): Factor out VIM mode?
|
|
101
|
+
editorMode === 'vim' && vim(),
|
|
97
102
|
|
|
98
103
|
// Replication.
|
|
99
|
-
|
|
104
|
+
collaboration,
|
|
105
|
+
|
|
106
|
+
// Theme.
|
|
107
|
+
slots?.editor?.theme && EditorView.theme(slots?.editor?.theme),
|
|
100
108
|
|
|
101
109
|
// Custom.
|
|
102
110
|
...extensions,
|
|
103
|
-
],
|
|
111
|
+
].filter(Boolean) as Extension[],
|
|
104
112
|
});
|
|
105
113
|
|
|
106
114
|
setState(state);
|
|
115
|
+
|
|
116
|
+
// NOTE: This repaints the editor.
|
|
117
|
+
// If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
|
|
118
|
+
// Ideally this should not be hit except when changing between text objects.
|
|
119
|
+
view?.destroy();
|
|
107
120
|
setView(new EditorView({ state, parent }));
|
|
108
121
|
|
|
109
122
|
return () => {
|
|
@@ -111,20 +124,65 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
111
124
|
setView(undefined);
|
|
112
125
|
setState(undefined);
|
|
113
126
|
};
|
|
114
|
-
}, [parent, content, themeMode]);
|
|
127
|
+
}, [parent, model.content, themeMode, editorMode]);
|
|
115
128
|
|
|
116
|
-
const
|
|
129
|
+
const handleKeyUp = useCallback(
|
|
117
130
|
(event: KeyboardEvent) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
131
|
+
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
132
|
+
switch (key) {
|
|
133
|
+
case 'Enter': {
|
|
134
|
+
view?.contentDOM.focus();
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
case 'Escape': {
|
|
139
|
+
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && parent?.focus();
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
123
142
|
}
|
|
124
143
|
},
|
|
125
|
-
[view],
|
|
144
|
+
[view, editorMode],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
key={model.id}
|
|
150
|
+
ref={setParent}
|
|
151
|
+
tabIndex={0}
|
|
152
|
+
{...slots?.root}
|
|
153
|
+
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
154
|
+
onKeyUp={handleKeyUp}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
161
|
+
({ extensions: _extensions, slots, ...props }, forwardedRef) => {
|
|
162
|
+
const { themeMode } = useThemeContext();
|
|
163
|
+
const extensions = [...(_extensions ?? []), basicBundle({ themeMode, placeholder: slots?.editor?.placeholder })];
|
|
164
|
+
return (
|
|
165
|
+
<BaseTextEditor
|
|
166
|
+
ref={forwardedRef}
|
|
167
|
+
extensions={extensions}
|
|
168
|
+
slots={defaultsDeep({}, slots, { editor: { theme: basicTheme } })}
|
|
169
|
+
{...props}
|
|
170
|
+
/>
|
|
126
171
|
);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
127
174
|
|
|
128
|
-
|
|
175
|
+
export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
176
|
+
({ extensions: _extensions, slots, ...props }, forwardedRef) => {
|
|
177
|
+
const { themeMode } = useThemeContext();
|
|
178
|
+
const extensions = [...(_extensions ?? []), markdownBundle({ themeMode })];
|
|
179
|
+
return (
|
|
180
|
+
<BaseTextEditor
|
|
181
|
+
ref={forwardedRef}
|
|
182
|
+
extensions={extensions}
|
|
183
|
+
slots={defaultsDeep({}, slots, { editor: { theme: markdownTheme } })}
|
|
184
|
+
{...props}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
129
187
|
},
|
|
130
188
|
);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
|
6
|
+
import { EditorState } from '@codemirror/state';
|
|
7
|
+
import { EditorView } from '@codemirror/view';
|
|
8
|
+
import { type StoryObj } from '@storybook/html';
|
|
9
|
+
import { basicSetup } from 'codemirror';
|
|
10
|
+
import { yCollab } from 'y-codemirror.next';
|
|
11
|
+
|
|
12
|
+
import { EventSubscriptions } from '@dxos/async';
|
|
13
|
+
import { generateName } from '@dxos/display-name';
|
|
14
|
+
import { Client, type PublicKey } from '@dxos/react-client';
|
|
15
|
+
import { type Space, TextObject } from '@dxos/react-client/echo';
|
|
16
|
+
import { type Identity } from '@dxos/react-client/halo';
|
|
17
|
+
import { joinCommonSpace, TestBuilder, textGenerator } from '@dxos/react-client/testing';
|
|
18
|
+
import { YText } from '@dxos/text-model';
|
|
19
|
+
|
|
20
|
+
import { cursorColor, SpaceAwarenessProvider } from '../../hooks';
|
|
21
|
+
import { EditorDocument, types as schema } from '../../testing';
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
title: 'CodeMirror',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const testBuilder = new TestBuilder();
|
|
28
|
+
const views: Record<number, EditorView> = {};
|
|
29
|
+
const subscriptions = new EventSubscriptions();
|
|
30
|
+
|
|
31
|
+
const updateEditor = async (id: number, editor: HTMLDivElement, identity: Identity, space: Space, text: TextObject) => {
|
|
32
|
+
const doc = text.doc;
|
|
33
|
+
const ytext = text.content;
|
|
34
|
+
if (!(ytext instanceof YText) || !doc) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { awareness } = new SpaceAwarenessProvider({ space, doc, channel: `yjs.awareness.${text.id}` });
|
|
39
|
+
awareness.setLocalStateField('user', {
|
|
40
|
+
name: identity.profile?.displayName ?? generateName(identity.identityKey.toHex()),
|
|
41
|
+
// TODO(wittjosiah): Pick colours from theme based on identity key.
|
|
42
|
+
color: cursorColor.color,
|
|
43
|
+
colorLight: cursorColor.light,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const state = EditorState.create({
|
|
47
|
+
doc: ytext.toString(),
|
|
48
|
+
extensions: [basicSetup, markdown({ base: markdownLanguage }), yCollab(ytext, awareness)],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const view = views[id];
|
|
52
|
+
if (view) {
|
|
53
|
+
view.setState(state);
|
|
54
|
+
} else {
|
|
55
|
+
views[id] = new EditorView({ state, parent: editor });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (id === 0) {
|
|
59
|
+
subscriptions.clear();
|
|
60
|
+
subscriptions.add(textGenerator({ text: ytext }));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const setupEditor = async (id: number, client: Client, spaceKey: PublicKey, editor: HTMLDivElement) => {
|
|
65
|
+
const space = await client.spaces.get(spaceKey)!;
|
|
66
|
+
const query = space.db.query(EditorDocument.filter());
|
|
67
|
+
query.subscribe(({ objects }) => {
|
|
68
|
+
const text = objects[0]?.content;
|
|
69
|
+
void (text && updateEditor(id, editor, client.halo.identity.get()!, space, text));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const text = query.objects[0]?.content;
|
|
73
|
+
void (text && updateEditor(id, editor, client.halo.identity.get()!, space, text));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const setupSpace = async (count: number) => {
|
|
77
|
+
const clients = [...Array(count)].map(() => new Client({ services: testBuilder.createLocal() }));
|
|
78
|
+
await Promise.all(clients.map((client) => client.initialize()));
|
|
79
|
+
await Promise.all(clients.map((client) => client.halo.createIdentity()));
|
|
80
|
+
clients.map((client) => client.spaces.addSchema(schema));
|
|
81
|
+
|
|
82
|
+
const space = await clients[0].spaces.create();
|
|
83
|
+
const text = new TextObject('Hello, Storybook!');
|
|
84
|
+
const document = new EditorDocument({ content: text });
|
|
85
|
+
await space.db.add(document);
|
|
86
|
+
|
|
87
|
+
await joinCommonSpace(clients, space.key);
|
|
88
|
+
|
|
89
|
+
return { clients, spaceKey: space.key };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const count = 2;
|
|
93
|
+
|
|
94
|
+
export const Default: StoryObj<{ id: number; client: Client; spaceKey: PublicKey }> = {
|
|
95
|
+
render: ({ id, client, spaceKey }) => {
|
|
96
|
+
const editor = document.createElement('div');
|
|
97
|
+
editor.setAttribute('style', 'min-width: 0; flex: 1; padding: 1rem;');
|
|
98
|
+
void setupEditor(id, client, spaceKey, editor);
|
|
99
|
+
return editor;
|
|
100
|
+
},
|
|
101
|
+
decorators: [
|
|
102
|
+
(story) => {
|
|
103
|
+
const container = document.createElement('div');
|
|
104
|
+
container.setAttribute('style', 'display: flex; justify-content: space-evenly');
|
|
105
|
+
void setupSpace(count).then(({ clients, spaceKey }) => {
|
|
106
|
+
clients.forEach((client, id) => {
|
|
107
|
+
// TODO(wittjosiah): According to the docs these types should be compatible.
|
|
108
|
+
container.appendChild(story({ args: { id, client, spaceKey } }) as HTMLElement);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
return container;
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { closeBrackets } from '@codemirror/autocomplete';
|
|
6
|
+
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
7
|
+
import { type Extension } from '@codemirror/state';
|
|
8
|
+
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
9
|
+
import { EditorView, placeholder } from '@codemirror/view';
|
|
10
|
+
|
|
11
|
+
import type { ThemeMode } from '@dxos/react-ui';
|
|
12
|
+
|
|
13
|
+
export type BasicBundleOptions = {
|
|
14
|
+
themeMode?: ThemeMode;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const basicBundle = ({ themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
|
|
19
|
+
[
|
|
20
|
+
bracketMatching(),
|
|
21
|
+
closeBrackets(),
|
|
22
|
+
_placeholder && placeholder(_placeholder),
|
|
23
|
+
|
|
24
|
+
EditorView.lineWrapping,
|
|
25
|
+
|
|
26
|
+
// TODO(burdon): Is this required?
|
|
27
|
+
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
28
|
+
].filter(Boolean) as Extension[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import get from 'lodash.get';
|
|
6
|
+
import { type StyleSpec } from 'style-mod';
|
|
7
|
+
|
|
8
|
+
import { tokens } from '../../../../styles';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Minimal styles.
|
|
12
|
+
* NOTE: The '&' prefix denotes the CM editor root.
|
|
13
|
+
* https://codemirror.net/examples/styling
|
|
14
|
+
*/
|
|
15
|
+
// TODO(burdon): Remove?
|
|
16
|
+
export const basicTheme: {
|
|
17
|
+
[selector: string]: StyleSpec;
|
|
18
|
+
} = {
|
|
19
|
+
'&.cm-focused': {
|
|
20
|
+
outline: 'none',
|
|
21
|
+
},
|
|
22
|
+
'& .cm-line': {
|
|
23
|
+
paddingInline: 0,
|
|
24
|
+
lineHeight: 1.6,
|
|
25
|
+
minBlockSize: '1.6em',
|
|
26
|
+
},
|
|
27
|
+
'& .cm-line *': {
|
|
28
|
+
lineHeight: 1.6,
|
|
29
|
+
},
|
|
30
|
+
'& .cm-scroller': {
|
|
31
|
+
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
32
|
+
overflow: 'visible',
|
|
33
|
+
},
|
|
34
|
+
'& .cm-content': {
|
|
35
|
+
padding: 0,
|
|
36
|
+
caretColor: 'black',
|
|
37
|
+
},
|
|
38
|
+
'.dark & .cm-content': {
|
|
39
|
+
caretColor: 'white',
|
|
40
|
+
},
|
|
41
|
+
'& .cm-tooltip': {
|
|
42
|
+
backgroundColor: 'transparent',
|
|
43
|
+
border: 'none',
|
|
44
|
+
},
|
|
45
|
+
'& .cm-link': {
|
|
46
|
+
color: get(tokens, 'extend.colors.primary.500'),
|
|
47
|
+
textDecoration: 'underline',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { StateField } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
|
|
9
|
+
* @param onChange
|
|
10
|
+
*/
|
|
11
|
+
// TODO(burdon): Name?
|
|
12
|
+
export const onChangeExtension = (onChange: (text: string) => void) =>
|
|
13
|
+
StateField.define({
|
|
14
|
+
create: () => null,
|
|
15
|
+
update: (_value, transaction) => {
|
|
16
|
+
if (transaction.docChanged && onChange) {
|
|
17
|
+
onChange(transaction.newDoc.toString());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return null;
|
|
21
|
+
},
|
|
22
|
+
});
|