@dxos/react-ui-editor 0.3.11-main.2c83f41 → 0.3.11-main.2cb95a4
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 +779 -760
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +16 -13
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -6
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +7 -9
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +0 -1
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +7 -12
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -2
- package/dist/types/src/extensions/awareness.d.ts +2 -0
- package/dist/types/src/extensions/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/basic.d.ts +4 -5
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/code.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +31 -18
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/{util → extensions}/cursor.d.ts +10 -2
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/hr.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +2 -5
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge.d.ts +2 -1
- package/dist/types/src/hooks/automerge.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +28 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +2 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +2 -32
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs.d.ts +2 -1
- package/dist/types/src/hooks/yjs.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +7 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +32 -31
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +77 -79
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +124 -92
- package/src/components/TextEditor/comments.stories.tsx +357 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.ts +40 -46
- package/src/extensions/automerge/cursor.ts +2 -1
- package/src/extensions/automerge/defs.ts +10 -31
- package/src/extensions/automerge/index.ts +0 -1
- package/src/extensions/automerge/semaphore.ts +23 -20
- package/src/extensions/automerge/update-automerge.ts +2 -2
- package/src/extensions/awareness.ts +45 -34
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/code.ts +24 -18
- package/src/extensions/comments.ts +242 -234
- package/src/extensions/cursor.ts +50 -0
- package/src/extensions/hr.ts +49 -50
- package/src/extensions/image.ts +2 -2
- package/src/extensions/index.ts +2 -0
- package/src/extensions/link.ts +7 -7
- package/src/extensions/markdown/bundle.ts +18 -24
- package/src/extensions/markdown/highlight.ts +0 -8
- package/src/extensions/mention.ts +9 -2
- package/src/extensions/outliner.ts +12 -0
- package/src/extensions/table.ts +2 -2
- package/src/extensions/tasklist.ts +78 -86
- package/src/extensions/yjs/cursor.ts +2 -1
- package/src/extensions/yjs/yjs.test.ts +1 -1
- package/src/extensions/yjs/yjs.ts +2 -2
- package/src/hooks/automerge.ts +2 -1
- package/src/hooks/defs.ts +47 -0
- package/src/hooks/index.ts +3 -1
- package/src/hooks/useTextModel.ts +3 -49
- package/src/hooks/yjs.ts +2 -23
- package/src/index.ts +1 -0
- package/src/styles/markdown.ts +0 -2
- package/src/themes/default.ts +17 -50
- package/src/util.ts +41 -0
- package/dist/types/src/util/cursor.d.ts.map +0 -1
- package/dist/types/src/util/index.d.ts +0 -3
- package/dist/types/src/util/index.d.ts.map +0 -1
- package/dist/types/src/util/util.d.ts +0 -5
- package/dist/types/src/util/util.d.ts.map +0 -1
- package/src/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- package/src/util/util.ts +0 -18
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { EditorState, type Extension } from '@codemirror/state';
|
|
5
|
+
import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
|
|
6
6
|
import { EditorView } from '@codemirror/view';
|
|
7
7
|
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
8
|
import { vim } from '@replit/codemirror-vim';
|
|
@@ -14,18 +14,21 @@ import React, {
|
|
|
14
14
|
useCallback,
|
|
15
15
|
useEffect,
|
|
16
16
|
useImperativeHandle,
|
|
17
|
+
useRef,
|
|
17
18
|
useState,
|
|
18
19
|
} from 'react';
|
|
19
20
|
|
|
20
|
-
import {
|
|
21
|
+
import { log } from '@dxos/log';
|
|
21
22
|
import { useThemeContext } from '@dxos/react-ui';
|
|
22
|
-
import {
|
|
23
|
+
import { inputSurface, mx } from '@dxos/react-ui-theme';
|
|
23
24
|
|
|
24
|
-
import { basicBundle, markdownBundle,
|
|
25
|
-
import { type
|
|
25
|
+
import { basicBundle, markdownBundle, useAwareness } from '../../extensions';
|
|
26
|
+
import { type EditorModel } from '../../hooks';
|
|
26
27
|
import { type ThemeStyles } from '../../styles';
|
|
27
28
|
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
29
|
+
import { logChanges } from '../../util';
|
|
28
30
|
|
|
31
|
+
// TODO(burdon): Change to enum?
|
|
29
32
|
export const EditorModes = ['default', 'vim'] as const;
|
|
30
33
|
export type EditorMode = (typeof EditorModes)[number];
|
|
31
34
|
|
|
@@ -34,133 +37,160 @@ export type CursorInfo = {
|
|
|
34
37
|
to: number;
|
|
35
38
|
line: number;
|
|
36
39
|
lines: number;
|
|
40
|
+
length: number;
|
|
37
41
|
after?: string;
|
|
38
42
|
};
|
|
39
43
|
|
|
40
|
-
export type TextEditorRef = {
|
|
41
|
-
root: HTMLDivElement | null;
|
|
42
|
-
state?: EditorState;
|
|
43
|
-
view?: EditorView;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
44
|
export type TextEditorSlots = {
|
|
47
45
|
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
48
46
|
editor?: {
|
|
49
47
|
className?: string;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
theme?: ThemeStyles;
|
|
48
|
+
};
|
|
49
|
+
content?: {
|
|
50
|
+
className?: string;
|
|
54
51
|
};
|
|
55
52
|
};
|
|
56
53
|
|
|
54
|
+
// TODO(burdon): Spellcheck?
|
|
57
55
|
export type TextEditorProps = {
|
|
58
56
|
model: EditorModel;
|
|
59
57
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
autofocus?: boolean;
|
|
59
|
+
multiline?: boolean;
|
|
60
|
+
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
61
|
+
selection?: { anchor: number; head?: number };
|
|
62
|
+
editorMode?: EditorMode; // TODO(burdon): Factor out.
|
|
63
|
+
placeholder?: string;
|
|
64
|
+
theme?: ThemeStyles;
|
|
63
65
|
slots?: TextEditorSlots;
|
|
66
|
+
extensions?: Extension[];
|
|
67
|
+
debug?: boolean;
|
|
64
68
|
};
|
|
65
69
|
|
|
66
70
|
/**
|
|
67
71
|
* Base text editor.
|
|
68
|
-
* NOTE: Rather than adding properties, try to create extensions that can be reused.
|
|
69
72
|
*/
|
|
70
|
-
export const BaseTextEditor = forwardRef<
|
|
71
|
-
(
|
|
72
|
-
|
|
73
|
+
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
74
|
+
(
|
|
75
|
+
{
|
|
76
|
+
model,
|
|
77
|
+
readonly,
|
|
78
|
+
autofocus,
|
|
79
|
+
scrollTo,
|
|
80
|
+
selection,
|
|
81
|
+
editorMode,
|
|
82
|
+
theme,
|
|
83
|
+
slots = defaultSlots,
|
|
84
|
+
extensions = [],
|
|
85
|
+
debug,
|
|
86
|
+
},
|
|
87
|
+
forwardedRef,
|
|
88
|
+
) => {
|
|
73
89
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
>();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (awareness && peer) {
|
|
84
|
-
awareness.setLocalStateField('user', {
|
|
85
|
-
name: peer.name ?? generateName(peer.id),
|
|
86
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
87
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}, [awareness, peer, themeMode]);
|
|
90
|
+
const { themeMode } = useThemeContext();
|
|
91
|
+
|
|
92
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
93
|
+
const [view, setView] = useState<EditorView | null>(null);
|
|
94
|
+
|
|
95
|
+
// The view ref can be used to focus the editor.
|
|
96
|
+
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
97
|
+
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
91
98
|
|
|
92
|
-
//
|
|
99
|
+
// Set focus.
|
|
93
100
|
useEffect(() => {
|
|
94
|
-
if (
|
|
95
|
-
view
|
|
96
|
-
effects: setCommentRange.of({ model, comments }),
|
|
97
|
-
});
|
|
101
|
+
if (autofocus) {
|
|
102
|
+
view?.focus();
|
|
98
103
|
}
|
|
99
|
-
}, [view,
|
|
104
|
+
}, [view, autofocus]);
|
|
100
105
|
|
|
106
|
+
// Monitor awareness.
|
|
107
|
+
useAwareness(model);
|
|
108
|
+
|
|
109
|
+
// Create editor state and view.
|
|
110
|
+
// The view is recreated if the model or extensions are changed.
|
|
101
111
|
useEffect(() => {
|
|
102
|
-
if (!
|
|
112
|
+
if (!model || !rootRef.current) {
|
|
103
113
|
return;
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
//
|
|
116
|
+
//
|
|
117
|
+
// EditorState
|
|
107
118
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
119
|
+
//
|
|
108
120
|
const state = EditorState.create({
|
|
109
121
|
doc: model.text(),
|
|
122
|
+
selection,
|
|
110
123
|
extensions: [
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
125
|
+
EditorView.exceptionSink.of((err) => {
|
|
126
|
+
log.catch(err);
|
|
127
|
+
}),
|
|
115
128
|
|
|
116
129
|
// Theme.
|
|
117
|
-
// TODO(burdon): Make
|
|
130
|
+
// TODO(burdon): Make configurable.
|
|
118
131
|
EditorView.baseTheme(defaultTheme),
|
|
119
|
-
EditorView.theme(
|
|
132
|
+
EditorView.theme(theme ?? {}),
|
|
120
133
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
134
|
+
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
135
|
+
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
136
|
+
|
|
137
|
+
// State.
|
|
138
|
+
EditorState.readOnly.of(!!readonly),
|
|
121
139
|
|
|
122
140
|
// Storage and replication.
|
|
141
|
+
// NOTE: This must come before user extensions.
|
|
123
142
|
model.extension,
|
|
124
143
|
|
|
144
|
+
// TODO(burdon): Factor out (requires special handling for Escape/focus).
|
|
145
|
+
editorMode === 'vim' && vim(),
|
|
146
|
+
|
|
125
147
|
// Custom.
|
|
126
148
|
...extensions,
|
|
127
149
|
].filter(Boolean) as Extension[],
|
|
128
150
|
});
|
|
129
151
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
152
|
+
//
|
|
153
|
+
// EditorView
|
|
154
|
+
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
155
|
+
//
|
|
156
|
+
const newView = new EditorView({
|
|
135
157
|
state,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
parent: rootRef.current,
|
|
159
|
+
scrollTo,
|
|
160
|
+
// NOTE: Uncomment to debug/monitor all transactions.
|
|
161
|
+
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
162
|
+
dispatchTransactions: (trs, view) => {
|
|
163
|
+
if (debug) {
|
|
164
|
+
logChanges(trs);
|
|
165
|
+
}
|
|
166
|
+
view.update(trs);
|
|
167
|
+
},
|
|
145
168
|
});
|
|
146
169
|
|
|
170
|
+
view?.destroy();
|
|
171
|
+
setView(newView);
|
|
172
|
+
|
|
147
173
|
return () => {
|
|
148
|
-
|
|
149
|
-
|
|
174
|
+
newView?.destroy();
|
|
175
|
+
setView(null);
|
|
150
176
|
};
|
|
151
|
-
}, [
|
|
177
|
+
}, [rootRef, model, readonly, editorMode, themeMode]);
|
|
152
178
|
|
|
179
|
+
// Handles tab/focus.
|
|
180
|
+
// Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
|
|
153
181
|
const handleKeyUp = useCallback(
|
|
154
182
|
(event: KeyboardEvent) => {
|
|
155
183
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
156
184
|
switch (key) {
|
|
157
185
|
case 'Enter': {
|
|
158
|
-
view?.
|
|
186
|
+
view?.focus();
|
|
159
187
|
break;
|
|
160
188
|
}
|
|
161
189
|
|
|
162
190
|
case 'Escape': {
|
|
163
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)
|
|
191
|
+
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
192
|
+
rootRef.current?.focus();
|
|
193
|
+
}
|
|
164
194
|
break;
|
|
165
195
|
}
|
|
166
196
|
}
|
|
@@ -171,42 +201,46 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
171
201
|
return (
|
|
172
202
|
<div
|
|
173
203
|
key={model.id}
|
|
174
|
-
|
|
204
|
+
role='none'
|
|
175
205
|
tabIndex={0}
|
|
176
|
-
{...slots?.root}
|
|
177
|
-
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
178
206
|
onKeyUp={handleKeyUp}
|
|
207
|
+
{...slots.root}
|
|
208
|
+
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
209
|
+
ref={rootRef}
|
|
179
210
|
/>
|
|
180
211
|
);
|
|
181
212
|
},
|
|
182
213
|
);
|
|
183
214
|
|
|
184
|
-
|
|
185
|
-
|
|
215
|
+
// TODO(burdon): Single-line/scroll.
|
|
216
|
+
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
217
|
+
({ readonly, placeholder, multiline, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
186
218
|
const { themeMode } = useThemeContext();
|
|
187
|
-
const
|
|
219
|
+
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
188
220
|
return (
|
|
189
221
|
<BaseTextEditor
|
|
190
222
|
ref={forwardedRef}
|
|
191
223
|
readonly={readonly}
|
|
192
|
-
extensions={[basicBundle({
|
|
193
|
-
|
|
224
|
+
extensions={[basicBundle({ themeMode, placeholder, multiline }), ...extensions]}
|
|
225
|
+
theme={theme}
|
|
226
|
+
slots={updatedSlots}
|
|
194
227
|
{...props}
|
|
195
228
|
/>
|
|
196
229
|
);
|
|
197
230
|
},
|
|
198
231
|
);
|
|
199
232
|
|
|
200
|
-
export const MarkdownEditor = forwardRef<
|
|
201
|
-
({ readonly,
|
|
233
|
+
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
234
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
202
235
|
const { themeMode } = useThemeContext();
|
|
203
|
-
const
|
|
236
|
+
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
204
237
|
return (
|
|
205
238
|
<BaseTextEditor
|
|
206
239
|
ref={forwardedRef}
|
|
207
240
|
readonly={readonly}
|
|
208
|
-
extensions={[markdownBundle({
|
|
209
|
-
|
|
241
|
+
extensions={[markdownBundle({ themeMode, readonly, placeholder }), ...extensions]}
|
|
242
|
+
theme={theme}
|
|
243
|
+
slots={updatedSlots}
|
|
210
244
|
{...props}
|
|
211
245
|
/>
|
|
212
246
|
);
|
|
@@ -215,20 +249,18 @@ export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
215
249
|
|
|
216
250
|
export const defaultSlots: TextEditorSlots = {
|
|
217
251
|
root: {
|
|
218
|
-
|
|
252
|
+
// TODO(burdon): Add focusRing by default/as property?
|
|
253
|
+
className: mx('flex flex-col grow overflow-y-auto', inputSurface),
|
|
254
|
+
},
|
|
255
|
+
editor: {
|
|
256
|
+
className: 'h-full p-2',
|
|
219
257
|
},
|
|
220
258
|
};
|
|
221
259
|
|
|
222
260
|
export const defaultTextSlots: TextEditorSlots = {
|
|
223
261
|
...defaultSlots,
|
|
224
|
-
editor: {
|
|
225
|
-
theme: textTheme,
|
|
226
|
-
},
|
|
227
262
|
};
|
|
228
263
|
|
|
229
264
|
export const defaultMarkdownSlots: TextEditorSlots = {
|
|
230
265
|
...defaultSlots,
|
|
231
|
-
editor: {
|
|
232
|
-
theme: markdownTheme,
|
|
233
|
-
},
|
|
234
266
|
};
|