@dxos/react-ui-editor 0.3.9-next.7522f7a → 0.3.9
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 +930 -811
- 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 -10
- 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/{theme.d.ts → extensions/basic/theme.d.ts} +2 -1
- 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.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +1 -0
- 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 +4 -3
- 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 +30 -27
- 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 +10 -3
- package/src/components/TextEditor/TextEditor.tsx +113 -56
- 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/{theme.ts → extensions/basic/theme.ts} +22 -8
- package/src/components/TextEditor/extensions/change.ts +22 -0
- package/src/components/TextEditor/extensions/hyperlink/index.ts +7 -0
- package/src/components/TextEditor/extensions/index.ts +4 -3
- 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} +16 -16
- 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 -14
- 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 -21
- 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 -100
- 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/hyperlinkDecoration.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlinkTooltip.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlinkWidget.d.ts.map +0 -1
- 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 -36
- package/src/components/Editor/index.ts +0 -5
- package/src/components/Markdown/Markdown.stories.tsx +0 -70
- package/src/components/Markdown/Markdown.tsx +0 -239
- 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/dist/types/src/components/TextEditor/extensions/{hyperlinkDecoration.d.ts → hyperlink/hyperlinkDecoration.d.ts} +0 -0
- /package/dist/types/src/components/TextEditor/extensions/{hyperlinkTooltip.d.ts → hyperlink/hyperlinkTooltip.d.ts} +0 -0
- /package/dist/types/src/components/TextEditor/extensions/{hyperlinkWidget.d.ts → hyperlink/hyperlinkWidget.d.ts} +0 -0
- /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/TextEditor/extensions/{hyperlinkDecoration.ts → hyperlink/hyperlinkDecoration.ts} +0 -0
- /package/src/components/TextEditor/extensions/{hyperlinkTooltip.tsx → hyperlink/hyperlinkTooltip.tsx} +0 -0
- /package/src/components/TextEditor/extensions/{hyperlinkWidget.tsx → hyperlink/hyperlinkWidget.tsx} +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
|
@@ -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,72 +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
|
-
|
|
100
|
-
|
|
104
|
+
collaboration,
|
|
105
|
+
|
|
106
|
+
// Theme.
|
|
107
|
+
slots?.editor?.theme && EditorView.theme(slots?.editor?.theme),
|
|
101
108
|
|
|
102
109
|
// Custom.
|
|
103
110
|
...extensions,
|
|
104
|
-
],
|
|
111
|
+
].filter(Boolean) as Extension[],
|
|
105
112
|
});
|
|
106
113
|
|
|
107
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();
|
|
108
120
|
setView(new EditorView({ state, parent }));
|
|
109
121
|
|
|
110
122
|
return () => {
|
|
@@ -112,20 +124,65 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
112
124
|
setView(undefined);
|
|
113
125
|
setState(undefined);
|
|
114
126
|
};
|
|
115
|
-
}, [parent, content, themeMode]);
|
|
127
|
+
}, [parent, model.content, themeMode, editorMode]);
|
|
116
128
|
|
|
117
|
-
const
|
|
129
|
+
const handleKeyUp = useCallback(
|
|
118
130
|
(event: KeyboardEvent) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
}
|
|
124
142
|
}
|
|
125
143
|
},
|
|
126
|
-
[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
|
+
/>
|
|
127
171
|
);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
128
174
|
|
|
129
|
-
|
|
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
|
+
);
|
|
130
187
|
},
|
|
131
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[];
|
|
@@ -5,30 +5,44 @@
|
|
|
5
5
|
import get from 'lodash.get';
|
|
6
6
|
import { type StyleSpec } from 'style-mod';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const tokens: TailwindConfig['theme'] = tailwindConfig({}).theme;
|
|
8
|
+
import { tokens } from '../../../../styles';
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
11
|
* Minimal styles.
|
|
12
|
+
* NOTE: The '&' prefix denotes the CM editor root.
|
|
14
13
|
* https://codemirror.net/examples/styling
|
|
15
14
|
*/
|
|
16
|
-
// TODO(burdon):
|
|
17
|
-
export const
|
|
15
|
+
// TODO(burdon): Remove?
|
|
16
|
+
export const basicTheme: {
|
|
18
17
|
[selector: string]: StyleSpec;
|
|
19
18
|
} = {
|
|
20
19
|
'&.cm-focused': {
|
|
21
20
|
outline: 'none',
|
|
22
21
|
},
|
|
23
|
-
'.cm-
|
|
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': {
|
|
24
31
|
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
25
32
|
overflow: 'visible',
|
|
26
33
|
},
|
|
27
|
-
'.cm-
|
|
34
|
+
'& .cm-content': {
|
|
35
|
+
padding: 0,
|
|
36
|
+
caretColor: 'black',
|
|
37
|
+
},
|
|
38
|
+
'.dark & .cm-content': {
|
|
39
|
+
caretColor: 'white',
|
|
40
|
+
},
|
|
41
|
+
'& .cm-tooltip': {
|
|
28
42
|
backgroundColor: 'transparent',
|
|
29
43
|
border: 'none',
|
|
30
44
|
},
|
|
31
|
-
'.cm-link': {
|
|
45
|
+
'& .cm-link': {
|
|
32
46
|
color: get(tokens, 'extend.colors.primary.500'),
|
|
33
47
|
textDecoration: 'underline',
|
|
34
48
|
},
|
|
@@ -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
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
5
|
+
export * from './basic';
|
|
6
|
+
export * from './change';
|
|
7
|
+
export * from './hyperlink';
|
|
8
|
+
export * from './markdown';
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
|
7
|
+
import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
|
|
8
|
+
import {
|
|
9
|
+
bracketMatching,
|
|
10
|
+
defaultHighlightStyle,
|
|
11
|
+
foldKeymap,
|
|
12
|
+
indentOnInput,
|
|
13
|
+
syntaxHighlighting,
|
|
14
|
+
} from '@codemirror/language';
|
|
15
|
+
import { languages } from '@codemirror/language-data';
|
|
16
|
+
import { lintKeymap } from '@codemirror/lint';
|
|
17
|
+
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
|
18
|
+
import { EditorState, type Extension } from '@codemirror/state';
|
|
19
|
+
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
20
|
+
import {
|
|
21
|
+
crosshairCursor,
|
|
22
|
+
drawSelection,
|
|
23
|
+
dropCursor,
|
|
24
|
+
EditorView,
|
|
25
|
+
highlightActiveLine,
|
|
26
|
+
highlightActiveLineGutter,
|
|
27
|
+
highlightSpecialChars,
|
|
28
|
+
keymap,
|
|
29
|
+
placeholder,
|
|
30
|
+
rectangularSelection,
|
|
31
|
+
} from '@codemirror/view';
|
|
32
|
+
// import { GFM } from '@lezer/markdown';
|
|
33
|
+
|
|
34
|
+
import type { ThemeMode } from '@dxos/react-ui';
|
|
35
|
+
|
|
36
|
+
import { markdownTagsExtension } from './tags';
|
|
37
|
+
import { markdownHighlightStyle } from './theme';
|
|
38
|
+
|
|
39
|
+
export type MarkdownBundleOptions = {
|
|
40
|
+
themeMode?: ThemeMode;
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const markdownBundle = ({ themeMode, placeholder: _placeholder }: MarkdownBundleOptions): Extension[] => {
|
|
45
|
+
// All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
|
|
46
|
+
// https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
47
|
+
return [
|
|
48
|
+
bracketMatching(),
|
|
49
|
+
closeBrackets(),
|
|
50
|
+
_placeholder && placeholder(_placeholder),
|
|
51
|
+
|
|
52
|
+
EditorState.allowMultipleSelections.of(true),
|
|
53
|
+
EditorView.lineWrapping,
|
|
54
|
+
|
|
55
|
+
// autocompletion(),
|
|
56
|
+
crosshairCursor(),
|
|
57
|
+
dropCursor(),
|
|
58
|
+
drawSelection(),
|
|
59
|
+
highlightActiveLine(),
|
|
60
|
+
highlightActiveLineGutter(),
|
|
61
|
+
highlightSelectionMatches(),
|
|
62
|
+
highlightSpecialChars(),
|
|
63
|
+
history(),
|
|
64
|
+
indentOnInput(),
|
|
65
|
+
rectangularSelection(),
|
|
66
|
+
|
|
67
|
+
keymap.of([
|
|
68
|
+
...closeBracketsKeymap,
|
|
69
|
+
// ...completionKeymap,
|
|
70
|
+
...defaultKeymap,
|
|
71
|
+
...foldKeymap,
|
|
72
|
+
...historyKeymap,
|
|
73
|
+
...lintKeymap,
|
|
74
|
+
...searchKeymap,
|
|
75
|
+
indentWithTab,
|
|
76
|
+
]),
|
|
77
|
+
|
|
78
|
+
// Main extension.
|
|
79
|
+
// https://github.com/codemirror/lang-markdown
|
|
80
|
+
markdown({
|
|
81
|
+
base: markdownLanguage,
|
|
82
|
+
codeLanguages: languages,
|
|
83
|
+
extensions: [
|
|
84
|
+
// TODO(burdon): This seems to upgrade the parser.
|
|
85
|
+
// GitHub flavored markdown bundle: Table, TaskList, Strikethrough, and Autolink.
|
|
86
|
+
// https://github.com/lezer-parser/markdown?tab=readme-ov-file#github-flavored-markdown
|
|
87
|
+
// https://github.github.com/gfm
|
|
88
|
+
// GFM,
|
|
89
|
+
|
|
90
|
+
// Custom styling.
|
|
91
|
+
markdownTagsExtension,
|
|
92
|
+
],
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
// TODO(thure): All but one rule here apply to both themes; rename or refactor.
|
|
96
|
+
syntaxHighlighting(markdownHighlightStyle),
|
|
97
|
+
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
98
|
+
].filter(Boolean) as Extension[];
|
|
99
|
+
};
|
package/src/components/{Markdown/markdownTheme.ts → TextEditor/extensions/markdown/theme.ts}
RENAMED
|
@@ -7,12 +7,10 @@ import { HighlightStyle } from '@codemirror/language';
|
|
|
7
7
|
import { tags } from '@lezer/highlight';
|
|
8
8
|
import get from 'lodash.get';
|
|
9
9
|
|
|
10
|
-
import { markdownTags } from './
|
|
11
|
-
import { bold, heading, italic, mark, strikethrough, tokens } from '
|
|
12
|
-
|
|
13
|
-
// TODO(burdon): Use theme colors.
|
|
14
|
-
// TODO(burdon): Light mode.
|
|
10
|
+
import { markdownTags } from './tags';
|
|
11
|
+
import { bold, heading, italic, mark, strikethrough, tokens, type ThemeStyles } from '../../../../styles';
|
|
15
12
|
|
|
13
|
+
// TODO(burdon): Rationalize theme/colors. Hoist onto single property object.
|
|
16
14
|
export const chalky = '#e5c07b';
|
|
17
15
|
export const coral = '#e06c75';
|
|
18
16
|
export const cyan = '#56b6c2';
|
|
@@ -28,16 +26,15 @@ export const highlightBackground = '#2c313a';
|
|
|
28
26
|
const _background = '#282c34';
|
|
29
27
|
export const tooltipBackground = '#353a42';
|
|
30
28
|
const _selection = '#3E4451';
|
|
31
|
-
export const cursor = '#ffffff';
|
|
32
29
|
|
|
33
30
|
const monospace = get(tokens, 'fontFamily.mono', ['monospace']).join(',');
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export const markdownTheme = {
|
|
40
|
-
// TODO(thure):
|
|
32
|
+
/**
|
|
33
|
+
* NOTE: The '&' prefix denotes the CM editor root.
|
|
34
|
+
* https://codemirror.net/examples/styling
|
|
35
|
+
*/
|
|
36
|
+
export const markdownTheme: ThemeStyles = {
|
|
37
|
+
// TODO(thure): Consider whether these commented-out rules from one-dark-theme should be integrated.
|
|
41
38
|
// '&': {
|
|
42
39
|
// color: ivory,
|
|
43
40
|
// backgroundColor: background
|
|
@@ -75,7 +72,7 @@ export const markdownTheme = {
|
|
|
75
72
|
// color: '#ddd'
|
|
76
73
|
// },
|
|
77
74
|
|
|
78
|
-
'.cm-tooltip': {
|
|
75
|
+
'& .cm-tooltip': {
|
|
79
76
|
backgroundColor: 'transparent',
|
|
80
77
|
border: 'none',
|
|
81
78
|
},
|
|
@@ -102,6 +99,7 @@ export const markdownTheme = {
|
|
|
102
99
|
},
|
|
103
100
|
'& .cm-line': {
|
|
104
101
|
paddingInline: 0,
|
|
102
|
+
lineHeight: 1.6,
|
|
105
103
|
minBlockSize: '1.6em',
|
|
106
104
|
},
|
|
107
105
|
'& .cm-line *': {
|
|
@@ -120,16 +118,17 @@ export const markdownTheme = {
|
|
|
120
118
|
background: get(tokens, 'extend.colors.primary.600', '#00ffff') + '44',
|
|
121
119
|
},
|
|
122
120
|
'& .cm-content': {
|
|
121
|
+
padding: 0,
|
|
123
122
|
caretColor: 'black',
|
|
124
123
|
},
|
|
125
124
|
'.dark & .cm-content': {
|
|
126
|
-
caretColor:
|
|
125
|
+
caretColor: 'white',
|
|
127
126
|
},
|
|
128
127
|
'& .cm-cursor': {
|
|
129
128
|
borderLeftColor: 'black',
|
|
130
129
|
},
|
|
131
130
|
'.dark & .cm-cursor': {
|
|
132
|
-
borderLeftColor:
|
|
131
|
+
borderLeftColor: 'white',
|
|
133
132
|
},
|
|
134
133
|
'.cm-placeholder': {
|
|
135
134
|
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
@@ -169,12 +168,13 @@ export const markdownTheme = {
|
|
|
169
168
|
margin: '0',
|
|
170
169
|
},
|
|
171
170
|
'.cm-link': {
|
|
172
|
-
color: 'rgb(20 89 208)',
|
|
171
|
+
color: 'rgb(20 89 208)', // TODO(burdon): Rationalize.
|
|
173
172
|
textDecorationLine: 'underline',
|
|
174
173
|
textDecorationThickness: '1px',
|
|
175
174
|
textUnderlineOffset: '2px',
|
|
176
175
|
borderRadius: '.125rem',
|
|
177
176
|
},
|
|
177
|
+
// TODO(burdon): Document.
|
|
178
178
|
...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
|
|
179
179
|
const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
|
|
180
180
|
// TODO(thure): This appears to be the best or only way to set selection caret heights, but it's far more verbose than it needs to be.
|