@dxos/react-ui-editor 0.3.9-main.9c71aa7 → 0.3.9-main.9da1d41
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 +674 -613
- 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 +1 -0
- 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 +3 -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/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/hyperlink/hyperlinkTooltip.d.ts +8 -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 +10 -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 +5 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -0
- 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 -2
- 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} +1 -13
- 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 +32 -26
- 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 +31 -46
- package/src/components/TextEditor/TextEditor.stories.tsx +51 -7
- package/src/components/TextEditor/TextEditor.tsx +108 -56
- package/src/components/TextEditor/codemirror.stories.ts +114 -0
- package/src/components/TextEditor/extensions/basic/bundle.ts +26 -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/hyperlink/hyperlinkTooltip.tsx +61 -0
- package/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.tsx +119 -0
- package/src/components/TextEditor/extensions/hyperlink/index.ts +7 -0
- package/src/components/TextEditor/extensions/index.ts +8 -0
- 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} +42 -27
- package/src/components/TextEditor/index.ts +1 -2
- 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 +35 -0
- package/src/{model.ts → hooks/useTextModel.ts} +4 -14
- 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 -28
- 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 -19
- package/dist/types/src/components/Markdown/Markdown.d.ts.map +0 -1
- package/dist/types/src/components/Markdown/Markdown.stories.d.ts +0 -19
- 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 -107
- 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 -26
- 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/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 -80
- package/src/components/Editor/Editor.tsx +0 -33
- package/src/components/Editor/index.ts +0 -5
- package/src/components/Markdown/Markdown.stories.tsx +0 -57
- package/src/components/Markdown/Markdown.tsx +0 -210
- package/src/components/Markdown/index.ts +0 -5
- package/src/components/RichText/RichText.stories.tsx +0 -68
- package/src/components/RichText/RichText.tsx +0 -181
- package/src/components/RichText/index.ts +0 -5
- package/src/components/TextEditor/theme.ts +0 -29
- /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
|
@@ -2,28 +2,29 @@
|
|
|
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
10
|
import React, {
|
|
11
11
|
type KeyboardEvent,
|
|
12
12
|
forwardRef,
|
|
13
|
+
useCallback,
|
|
13
14
|
useEffect,
|
|
14
15
|
useImperativeHandle,
|
|
15
16
|
useState,
|
|
16
|
-
|
|
17
|
-
type HTMLAttributes,
|
|
17
|
+
type ComponentProps,
|
|
18
18
|
} from 'react';
|
|
19
|
-
import { type StyleSpec } from 'style-mod';
|
|
20
|
-
import { yCollab } from 'y-codemirror.next';
|
|
21
19
|
|
|
22
20
|
import { useThemeContext } from '@dxos/react-ui';
|
|
23
|
-
import { YText } from '@dxos/text-model';
|
|
24
21
|
|
|
25
|
-
import {
|
|
26
|
-
import { type EditorModel,
|
|
22
|
+
import { basicBundle, basicTheme, markdownBundle, markdownTheme } from './extensions';
|
|
23
|
+
import { type EditorModel, useCollaboration } from '../../hooks';
|
|
24
|
+
import type { ThemeStyles } from '../../styles';
|
|
25
|
+
|
|
26
|
+
export const EditorModes = ['default', 'vim'] as const;
|
|
27
|
+
export type EditorMode = (typeof EditorModes)[number];
|
|
27
28
|
|
|
28
29
|
export type CursorInfo = {
|
|
29
30
|
from: number;
|
|
@@ -39,71 +40,77 @@ export type TextEditorRef = {
|
|
|
39
40
|
view?: EditorView;
|
|
40
41
|
};
|
|
41
42
|
|
|
43
|
+
export type TextEditorSlots = {
|
|
44
|
+
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
45
|
+
editor?: {
|
|
46
|
+
className?: string;
|
|
47
|
+
placeholder?: string;
|
|
48
|
+
spellCheck?: boolean;
|
|
49
|
+
tabIndex?: number;
|
|
50
|
+
theme?: ThemeStyles;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
42
54
|
export type TextEditorProps = {
|
|
43
|
-
model
|
|
55
|
+
model: EditorModel;
|
|
44
56
|
extensions?: Extension[];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
slots?: EditorSlots;
|
|
49
|
-
onKeyDown?: (event: KeyboardEvent, info: CursorInfo) => void;
|
|
50
|
-
} & Pick<HTMLAttributes<HTMLDivElement>, 'onBlur' | 'onFocus'>;
|
|
57
|
+
slots?: TextEditorSlots;
|
|
58
|
+
editorMode?: EditorMode;
|
|
59
|
+
};
|
|
51
60
|
|
|
52
61
|
/**
|
|
53
|
-
*
|
|
62
|
+
* Base text editor.
|
|
63
|
+
* NOTE: Rather than adding properties, try to create extensions that can be reused.
|
|
54
64
|
*/
|
|
55
|
-
export const
|
|
56
|
-
({ model, extensions = [],
|
|
57
|
-
const { id, content } = model ?? {};
|
|
65
|
+
export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
66
|
+
({ model, extensions = [], slots, editorMode }, forwardedRef) => {
|
|
58
67
|
const { themeMode } = useThemeContext();
|
|
68
|
+
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
59
69
|
|
|
60
70
|
const [parent, setParent] = useState<HTMLDivElement | null>(null);
|
|
61
71
|
const [state, setState] = useState<EditorState>();
|
|
62
72
|
const [view, setView] = useState<EditorView>();
|
|
63
|
-
|
|
64
|
-
// TODO(burdon): The ref may be instantiated before the view is created.
|
|
65
73
|
useImperativeHandle(
|
|
66
74
|
forwardedRef,
|
|
67
|
-
() => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
},
|
|
74
|
-
[view],
|
|
75
|
+
() => ({
|
|
76
|
+
editor: parent,
|
|
77
|
+
state,
|
|
78
|
+
view,
|
|
79
|
+
}),
|
|
80
|
+
[view, state, parent],
|
|
75
81
|
);
|
|
76
82
|
|
|
83
|
+
// TODO(burdon): Pass in extension?
|
|
84
|
+
const collaboration = useCollaboration(model, themeMode);
|
|
85
|
+
|
|
77
86
|
useEffect(() => {
|
|
78
87
|
if (!parent) {
|
|
79
88
|
return;
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
view?.destroy();
|
|
83
|
-
|
|
84
91
|
const state = EditorState.create({
|
|
85
|
-
doc: content?.toString(),
|
|
92
|
+
doc: model.content?.toString(),
|
|
86
93
|
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)]),
|
|
94
|
+
// TODO(burdon): Factor out VIM mode?
|
|
95
|
+
editorMode === 'vim' && vim(),
|
|
97
96
|
|
|
98
97
|
// Replication.
|
|
99
|
-
|
|
98
|
+
collaboration,
|
|
99
|
+
|
|
100
|
+
// Theme.
|
|
101
|
+
slots?.editor?.theme && EditorView.theme(slots?.editor?.theme),
|
|
100
102
|
|
|
101
103
|
// Custom.
|
|
102
104
|
...extensions,
|
|
103
|
-
],
|
|
105
|
+
].filter(Boolean) as Extension[],
|
|
104
106
|
});
|
|
105
107
|
|
|
106
108
|
setState(state);
|
|
109
|
+
|
|
110
|
+
// NOTE: This repaints the editor.
|
|
111
|
+
// If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
|
|
112
|
+
// Ideally this should not be hit except when changing between text objects.
|
|
113
|
+
view?.destroy();
|
|
107
114
|
setView(new EditorView({ state, parent }));
|
|
108
115
|
|
|
109
116
|
return () => {
|
|
@@ -111,20 +118,65 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
111
118
|
setView(undefined);
|
|
112
119
|
setState(undefined);
|
|
113
120
|
};
|
|
114
|
-
}, [parent, content, themeMode]);
|
|
121
|
+
}, [parent, model.content, themeMode, editorMode]);
|
|
115
122
|
|
|
116
|
-
const
|
|
123
|
+
const handleKeyUp = useCallback(
|
|
117
124
|
(event: KeyboardEvent) => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
126
|
+
switch (key) {
|
|
127
|
+
case 'Enter': {
|
|
128
|
+
view?.contentDOM.focus();
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'Escape': {
|
|
133
|
+
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && parent?.focus();
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
123
136
|
}
|
|
124
137
|
},
|
|
125
|
-
[view],
|
|
138
|
+
[view, editorMode],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div
|
|
143
|
+
key={model.id}
|
|
144
|
+
ref={setParent}
|
|
145
|
+
tabIndex={0}
|
|
146
|
+
{...slots?.root}
|
|
147
|
+
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
148
|
+
onKeyUp={handleKeyUp}
|
|
149
|
+
/>
|
|
126
150
|
);
|
|
151
|
+
},
|
|
152
|
+
);
|
|
127
153
|
|
|
128
|
-
|
|
154
|
+
export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
155
|
+
({ extensions: _extensions, slots, ...props }, forwardedRef) => {
|
|
156
|
+
const { themeMode } = useThemeContext();
|
|
157
|
+
const extensions = [...(_extensions ?? []), basicBundle({ themeMode, placeholder: slots?.editor?.placeholder })];
|
|
158
|
+
return (
|
|
159
|
+
<BaseTextEditor
|
|
160
|
+
ref={forwardedRef}
|
|
161
|
+
extensions={extensions}
|
|
162
|
+
slots={defaultsDeep({}, slots, { editor: { theme: basicTheme } })}
|
|
163
|
+
{...props}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
170
|
+
({ extensions: _extensions, slots, ...props }, forwardedRef) => {
|
|
171
|
+
const { themeMode } = useThemeContext();
|
|
172
|
+
const extensions = [...(_extensions ?? []), markdownBundle({ themeMode })];
|
|
173
|
+
return (
|
|
174
|
+
<BaseTextEditor
|
|
175
|
+
ref={forwardedRef}
|
|
176
|
+
extensions={extensions}
|
|
177
|
+
slots={defaultsDeep({}, slots, { editor: { theme: markdownTheme } })}
|
|
178
|
+
{...props}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
129
181
|
},
|
|
130
182
|
);
|
|
@@ -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,26 @@
|
|
|
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 { 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
|
+
// TODO(burdon): Is this required?
|
|
25
|
+
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
26
|
+
].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
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
// Copyright CodeMirror
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
import { syntaxTree } from '@codemirror/language';
|
|
7
|
+
import { type EditorState, type RangeSet, RangeSetBuilder, StateField, type Transaction } from '@codemirror/state';
|
|
8
|
+
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
9
|
+
|
|
10
|
+
// Adapted from:
|
|
11
|
+
// https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
|
|
12
|
+
// https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
|
|
13
|
+
|
|
14
|
+
// TODO(burdon): Adapt to make all markdown editable?
|
|
15
|
+
|
|
16
|
+
// TODO(burdon): Research overlays.
|
|
17
|
+
// https://stackoverflow.com/questions/26933344/codemirror-detect-and-create-links-inside-editor
|
|
18
|
+
// https://codemirror.net/5/doc/manual.html#addOverlay
|
|
19
|
+
// https://codemirror.net/5/demo/simplemode.html
|
|
20
|
+
// https://codemirror.net/5/demo/search.html (Search dialog).
|
|
21
|
+
|
|
22
|
+
class Link extends WidgetType {
|
|
23
|
+
constructor(private readonly _anchor: number, private readonly _text: string, private readonly _url?: string) {
|
|
24
|
+
super();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override eq(other: any) {
|
|
28
|
+
return this._text === other.text && this._url === other.url;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
toDOM(view: EditorView) {
|
|
32
|
+
const link = document.createElement('a');
|
|
33
|
+
link.setAttribute('class', 'cm-link');
|
|
34
|
+
link.textContent = this._text;
|
|
35
|
+
if (this._url) {
|
|
36
|
+
link.setAttribute('target', '_blank');
|
|
37
|
+
link.href = this._url;
|
|
38
|
+
} else {
|
|
39
|
+
link.onclick = () => {
|
|
40
|
+
view.dispatch({
|
|
41
|
+
selection: {
|
|
42
|
+
anchor: this._anchor,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return link;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type HyperlinkDecorationConfig = {
|
|
53
|
+
link?: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Creates a state field to replace AST elements with a hyperlink widget.
|
|
58
|
+
* https://codemirror.net/docs/ref/#state.StateField
|
|
59
|
+
*/
|
|
60
|
+
export const hyperlinkDecoration = ({ link }: HyperlinkDecorationConfig = {}) => {
|
|
61
|
+
return StateField.define<RangeSet<any>>({
|
|
62
|
+
create: (state) => update(state, link),
|
|
63
|
+
update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, link),
|
|
64
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const update = (state: EditorState, link?: boolean) => {
|
|
69
|
+
const builder = new RangeSetBuilder();
|
|
70
|
+
const cursor = state.selection.main.head;
|
|
71
|
+
syntaxTree(state).iterate({
|
|
72
|
+
enter: (node) => {
|
|
73
|
+
// Check if cursor is inside text.
|
|
74
|
+
if (cursor < node.from || cursor > node.to) {
|
|
75
|
+
if (node.name === 'Link') {
|
|
76
|
+
const marks = node.node.getChildren('LinkMark');
|
|
77
|
+
const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
|
|
78
|
+
|
|
79
|
+
const urlNode = node.node.getChild('URL');
|
|
80
|
+
const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
|
|
81
|
+
|
|
82
|
+
builder.add(
|
|
83
|
+
node.from,
|
|
84
|
+
node.to,
|
|
85
|
+
Decoration.replace({
|
|
86
|
+
widget: new Link(node.from, text, link ? url : undefined),
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return builder.finish();
|
|
99
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { hoverTooltip } from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
import { tooltipContent } from '@dxos/react-ui-theme';
|
|
8
|
+
|
|
9
|
+
const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
|
|
10
|
+
|
|
11
|
+
export type OnHyperlinkHover = (el: Element, url: string) => void;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* https://codemirror.net/examples/tooltip
|
|
15
|
+
* https://codemirror.net/docs/ref/#view.hoverTooltip
|
|
16
|
+
* https://github.com/codemirror/view/blob/main/src/tooltip.ts
|
|
17
|
+
*/
|
|
18
|
+
// TODO(burdon): Create config object.
|
|
19
|
+
export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markdownLinkRegexp) =>
|
|
20
|
+
hoverTooltip((view, pos) => {
|
|
21
|
+
const { from, text } = view.state.doc.lineAt(pos);
|
|
22
|
+
const p = pos - from;
|
|
23
|
+
|
|
24
|
+
let idx = 0;
|
|
25
|
+
let match;
|
|
26
|
+
do {
|
|
27
|
+
match = text.substring(idx).match(regexp);
|
|
28
|
+
if (!match) {
|
|
29
|
+
match = undefined;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
idx = text.indexOf(match[0]);
|
|
34
|
+
if (p >= idx && p < idx + match[0].length) {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
idx += match[0].length;
|
|
39
|
+
} while (true);
|
|
40
|
+
|
|
41
|
+
if (!match) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const [, , url] = match ?? [];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
pos: idx + from,
|
|
49
|
+
end: idx + from + match[0].length,
|
|
50
|
+
above: true,
|
|
51
|
+
create: () => {
|
|
52
|
+
const el = document.createElement('a');
|
|
53
|
+
el.innerText = '_'; // Required so doesn't collapse.
|
|
54
|
+
el.className = tooltipContent({}, 'mb-2 p-1');
|
|
55
|
+
el.setAttribute('target', '_blank');
|
|
56
|
+
el.setAttribute('href', url);
|
|
57
|
+
onHover(el, url);
|
|
58
|
+
return { dom: el };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
});
|