@dxos/react-ui-editor 0.4.8-main.97e86fe → 0.4.8-main.a4c603b
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 +941 -776
- 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 +103 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +34 -18
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +5 -79
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.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 +15 -1
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +1 -0
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +1 -2
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +1 -2
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts +2 -2
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor-line-margin.d.ts +7 -6
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +1 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -3
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +4 -5
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/table.d.ts +2 -2
- package/dist/types/src/extensions/markdown/table.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/awareness → hooks}/awareness-provider.d.ts +1 -1
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
- package/dist/types/src/hooks/defs.d.ts +20 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +4 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useEditorView.d.ts +20 -0
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +44 -3
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts +0 -6
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +33 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -0
- package/dist/types/src/themes/default.d.ts +3 -0
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +24 -24
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +524 -0
- package/src/components/TextEditor/TextEditor.stories.tsx +23 -508
- package/src/components/TextEditor/TextEditor.tsx +149 -37
- package/src/components/Toolbar/Toolbar.stories.tsx +42 -42
- package/src/extensions/automerge/automerge.stories.tsx +17 -16
- package/src/extensions/automerge/automerge.ts +1 -2
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/automerge/defs.ts +22 -1
- package/src/extensions/automerge/index.ts +1 -0
- package/src/extensions/automerge/sync.ts +9 -2
- package/src/extensions/automerge/update-automerge.ts +1 -2
- package/src/extensions/{awareness/awareness.ts → awareness.ts} +1 -1
- package/src/extensions/command/state.ts +1 -1
- package/src/extensions/comments.ts +5 -9
- package/src/extensions/cursor-line-margin.ts +18 -17
- package/src/extensions/cursor.ts +1 -1
- package/src/extensions/index.ts +1 -3
- package/src/extensions/markdown/bundle.ts +28 -11
- package/src/extensions/markdown/decorate.ts +4 -2
- package/src/extensions/markdown/formatting.ts +10 -15
- package/src/extensions/markdown/image.ts +0 -1
- package/src/extensions/markdown/table.ts +3 -11
- package/src/extensions/outliner.ts +12 -0
- package/src/{extensions/awareness → hooks}/awareness-provider.ts +1 -1
- package/src/hooks/defs.ts +30 -0
- package/src/hooks/index.ts +5 -1
- package/src/hooks/useEditorView.ts +32 -0
- package/src/hooks/useTextEditor.stories.tsx +34 -39
- package/src/hooks/useTextEditor.ts +120 -11
- package/src/hooks/useTextModel.ts +121 -0
- package/src/index.ts +1 -1
- package/src/themes/default.ts +28 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +0 -1
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +0 -1
- package/dist/types/src/extensions/awareness/index.d.ts +0 -3
- package/dist/types/src/extensions/awareness/index.d.ts.map +0 -1
- package/dist/types/src/extensions/doc.d.ts +0 -3
- package/dist/types/src/extensions/doc.d.ts.map +0 -1
- package/dist/types/src/extensions/factories.d.ts +0 -49
- package/dist/types/src/extensions/factories.d.ts.map +0 -1
- package/dist/types/src/extensions/types.d.ts +0 -9
- package/dist/types/src/extensions/types.d.ts.map +0 -1
- package/dist/types/src/hooks/useDocAccessor.d.ts +0 -7
- package/dist/types/src/hooks/useDocAccessor.d.ts.map +0 -1
- package/src/extensions/awareness/index.ts +0 -6
- package/src/extensions/doc.ts +0 -14
- package/src/extensions/factories.ts +0 -167
- package/src/extensions/types.ts +0 -16
- package/src/hooks/useDocAccessor.ts +0 -21
- /package/dist/types/src/extensions/{awareness/awareness.d.ts → awareness.d.ts} +0 -0
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { EditorState, type
|
|
6
|
-
import { EditorView } from '@codemirror/view';
|
|
5
|
+
import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
|
|
6
|
+
import { EditorView, scrollPastEnd } from '@codemirror/view';
|
|
7
7
|
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
|
+
import defaultsDeep from 'lodash.defaultsdeep';
|
|
8
9
|
import React, {
|
|
10
|
+
type ComponentProps,
|
|
9
11
|
type KeyboardEventHandler,
|
|
10
12
|
forwardRef,
|
|
11
13
|
useCallback,
|
|
@@ -13,12 +15,19 @@ import React, {
|
|
|
13
15
|
useImperativeHandle,
|
|
14
16
|
useRef,
|
|
15
17
|
useState,
|
|
18
|
+
useMemo,
|
|
16
19
|
} from 'react';
|
|
17
20
|
|
|
21
|
+
import { DocAccessor } from '@dxos/echo-schema';
|
|
18
22
|
import { log } from '@dxos/log';
|
|
23
|
+
import { useThemeContext } from '@dxos/react-ui';
|
|
24
|
+
import { focusRing } from '@dxos/react-ui-theme';
|
|
19
25
|
import { isNotFalsy } from '@dxos/util';
|
|
20
26
|
|
|
21
|
-
import {
|
|
27
|
+
import { createMarkdownExtensions, editorMode } from '../../extensions';
|
|
28
|
+
import { createBasicExtensions, createThemeExtensions, type EditorModel } from '../../hooks';
|
|
29
|
+
import { type ThemeStyles } from '../../styles';
|
|
30
|
+
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
22
31
|
import { logChanges } from '../../util';
|
|
23
32
|
|
|
24
33
|
export type CursorInfo = {
|
|
@@ -30,49 +39,61 @@ export type CursorInfo = {
|
|
|
30
39
|
after?: string;
|
|
31
40
|
};
|
|
32
41
|
|
|
33
|
-
export type
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
export type TextEditorSlots = {
|
|
43
|
+
root?: Omit<ComponentProps<'div'>, 'ref'>;
|
|
44
|
+
editor?: {
|
|
45
|
+
className?: string;
|
|
46
|
+
};
|
|
47
|
+
content?: {
|
|
48
|
+
className?: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type TextEditorProps = {
|
|
53
|
+
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
54
|
+
readonly?: boolean; // TODO(burdon): Move into model.
|
|
36
55
|
autoFocus?: boolean;
|
|
37
|
-
|
|
56
|
+
scrollPastEnd?: boolean;
|
|
38
57
|
moveToEndOfLine?: boolean;
|
|
58
|
+
lineWrapping?: boolean;
|
|
59
|
+
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
60
|
+
selection?: { anchor: number; head?: number };
|
|
61
|
+
placeholder?: string;
|
|
62
|
+
theme?: ThemeStyles;
|
|
63
|
+
slots?: TextEditorSlots;
|
|
64
|
+
extensions?: Extension[];
|
|
39
65
|
debug?: boolean;
|
|
40
|
-
dataTestId?: string;
|
|
41
66
|
};
|
|
42
67
|
|
|
43
|
-
let instanceCount = 0;
|
|
44
|
-
|
|
45
68
|
/**
|
|
46
|
-
*
|
|
47
|
-
* Handles tabster and focus management.
|
|
69
|
+
* Base text editor.
|
|
48
70
|
*/
|
|
49
|
-
// TODO(burdon):
|
|
50
|
-
export const
|
|
71
|
+
// TODO(burdon): Replace with useTextEditor.
|
|
72
|
+
export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
51
73
|
(
|
|
52
74
|
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
selection,
|
|
56
|
-
extensions,
|
|
57
|
-
className,
|
|
75
|
+
model,
|
|
76
|
+
readonly,
|
|
58
77
|
autoFocus,
|
|
59
78
|
scrollTo = EditorView.scrollIntoView(0),
|
|
60
79
|
moveToEndOfLine,
|
|
80
|
+
selection,
|
|
81
|
+
theme,
|
|
82
|
+
slots = defaultSlots,
|
|
83
|
+
extensions = [],
|
|
84
|
+
scrollPastEnd: _scrollPastEnd,
|
|
61
85
|
debug,
|
|
62
|
-
dataTestId,
|
|
63
86
|
},
|
|
64
87
|
forwardedRef,
|
|
65
88
|
) => {
|
|
66
|
-
// TODO(burdon): Increments by 2!
|
|
67
|
-
const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
|
|
68
|
-
|
|
69
|
-
// TODO(burdon): Make tabster optional.
|
|
70
89
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
90
|
+
const { themeMode } = useThemeContext();
|
|
91
|
+
|
|
71
92
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
72
93
|
const [view, setView] = useState<EditorView | null>(null);
|
|
73
94
|
|
|
74
95
|
// The view ref can be used to focus the editor.
|
|
75
|
-
// NOTE:
|
|
96
|
+
// NOTE: This does not cause the parent to re-render, so the ref is not available immediately.
|
|
76
97
|
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
77
98
|
|
|
78
99
|
// Set focus.
|
|
@@ -85,22 +106,34 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
85
106
|
// Create editor state and view.
|
|
86
107
|
// The view is recreated if the model or extensions are changed.
|
|
87
108
|
useEffect(() => {
|
|
88
|
-
|
|
109
|
+
if (!model || !rootRef.current) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
89
112
|
|
|
90
113
|
//
|
|
91
114
|
// EditorState
|
|
92
115
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
93
116
|
//
|
|
94
117
|
const state = EditorState.create({
|
|
95
|
-
doc,
|
|
118
|
+
doc: typeof model.content === 'string' ? model.content : DocAccessor.getValue(model.content),
|
|
96
119
|
selection,
|
|
97
120
|
extensions: [
|
|
98
|
-
|
|
99
|
-
// TODO(burdon): NOTE: Doesn't catch errors in keymap functions.
|
|
121
|
+
// TODO(burdon): Doesn't catch errors in keymap functions.
|
|
100
122
|
EditorView.exceptionSink.of((err) => {
|
|
101
123
|
log.catch(err);
|
|
102
124
|
}),
|
|
103
125
|
|
|
126
|
+
// Theme.
|
|
127
|
+
// TODO(burdon): Make base theme configurable.
|
|
128
|
+
EditorView.baseTheme(defaultTheme),
|
|
129
|
+
theme && EditorView.theme(theme),
|
|
130
|
+
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
131
|
+
slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
|
132
|
+
slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
|
|
133
|
+
|
|
134
|
+
// NOTE: Assumes default line height.
|
|
135
|
+
_scrollPastEnd && scrollPastEnd(),
|
|
136
|
+
|
|
104
137
|
// Focus.
|
|
105
138
|
EditorView.updateListener.of((update) => {
|
|
106
139
|
update.transactions.forEach((transaction) => {
|
|
@@ -110,7 +143,16 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
110
143
|
});
|
|
111
144
|
}),
|
|
112
145
|
|
|
113
|
-
|
|
146
|
+
// State.
|
|
147
|
+
EditorView.editable.of(!readonly),
|
|
148
|
+
EditorState.readOnly.of(!!readonly),
|
|
149
|
+
|
|
150
|
+
// Storage and replication.
|
|
151
|
+
// NOTE: This must come before user extensions.
|
|
152
|
+
model.extension,
|
|
153
|
+
|
|
154
|
+
// Custom.
|
|
155
|
+
...extensions,
|
|
114
156
|
].filter(isNotFalsy),
|
|
115
157
|
});
|
|
116
158
|
|
|
@@ -120,7 +162,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
120
162
|
//
|
|
121
163
|
const view = new EditorView({
|
|
122
164
|
state,
|
|
123
|
-
parent: rootRef.current
|
|
165
|
+
parent: rootRef.current,
|
|
124
166
|
scrollTo,
|
|
125
167
|
// NOTE: Uncomment to debug/monitor all transactions.
|
|
126
168
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
@@ -134,7 +176,6 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
134
176
|
|
|
135
177
|
setView(view);
|
|
136
178
|
|
|
137
|
-
// Position cursor at end of line.
|
|
138
179
|
if (moveToEndOfLine) {
|
|
139
180
|
const { to } = view.state.doc.lineAt(0);
|
|
140
181
|
view?.dispatch({ selection: { anchor: to } });
|
|
@@ -142,14 +183,15 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
142
183
|
|
|
143
184
|
// Remove tabster attribute (rely on custom keymap).
|
|
144
185
|
if (state.facet(editorMode).noTabster) {
|
|
145
|
-
rootRef.current
|
|
186
|
+
rootRef.current.removeAttribute('data-tabster');
|
|
146
187
|
}
|
|
147
188
|
|
|
148
189
|
return () => {
|
|
149
190
|
view?.destroy();
|
|
150
191
|
setView(null);
|
|
151
192
|
};
|
|
152
|
-
|
|
193
|
+
// TODO(wittjosiah): Does `rootRef` ever change? Only `.current` changes?
|
|
194
|
+
}, [rootRef, extensions, model, readonly, themeMode]);
|
|
153
195
|
|
|
154
196
|
// Focus editor on Enter (e.g., when tabbing to this component).
|
|
155
197
|
const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
|
@@ -169,12 +211,82 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
169
211
|
<div
|
|
170
212
|
role='none'
|
|
171
213
|
ref={rootRef}
|
|
214
|
+
key={model.id} // TODO(burdon): Remove?
|
|
172
215
|
tabIndex={0}
|
|
173
|
-
className={className}
|
|
174
|
-
data-testid={dataTestId}
|
|
175
|
-
{...tabsterDOMAttribute}
|
|
176
216
|
onKeyUp={handleKeyUp}
|
|
217
|
+
{...slots.root}
|
|
218
|
+
{...tabsterDOMAttribute}
|
|
177
219
|
/>
|
|
178
220
|
);
|
|
179
221
|
},
|
|
180
222
|
);
|
|
223
|
+
|
|
224
|
+
// TODO(burdon): Replace with hooks.
|
|
225
|
+
export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
226
|
+
(
|
|
227
|
+
{ readonly, placeholder, lineWrapping = true, theme = textTheme, slots, extensions: _extensions, ...props },
|
|
228
|
+
forwardedRef,
|
|
229
|
+
) => {
|
|
230
|
+
const { themeMode } = useThemeContext();
|
|
231
|
+
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
232
|
+
const extensions = useMemo(
|
|
233
|
+
() => [
|
|
234
|
+
createBasicExtensions({ lineWrapping, placeholder }),
|
|
235
|
+
createThemeExtensions({ themeMode }),
|
|
236
|
+
...(_extensions ?? []),
|
|
237
|
+
],
|
|
238
|
+
[themeMode, placeholder, lineWrapping, _extensions],
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<BaseTextEditor
|
|
243
|
+
ref={forwardedRef}
|
|
244
|
+
readonly={readonly}
|
|
245
|
+
extensions={extensions}
|
|
246
|
+
theme={theme}
|
|
247
|
+
slots={updatedSlots}
|
|
248
|
+
{...props}
|
|
249
|
+
/>
|
|
250
|
+
);
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// TODO(burdon): Replace with hooks.
|
|
255
|
+
export const MarkdownEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
256
|
+
({ readonly, placeholder, theme = markdownTheme, slots, extensions: _extensions, ...props }, forwardedRef) => {
|
|
257
|
+
const { themeMode } = useThemeContext();
|
|
258
|
+
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
259
|
+
const extensions = useMemo(
|
|
260
|
+
() => [createMarkdownExtensions({ themeMode, placeholder }), ...(_extensions ?? [])],
|
|
261
|
+
[themeMode, placeholder, _extensions],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<BaseTextEditor
|
|
266
|
+
ref={forwardedRef}
|
|
267
|
+
readonly={readonly}
|
|
268
|
+
extensions={extensions}
|
|
269
|
+
theme={theme}
|
|
270
|
+
slots={updatedSlots}
|
|
271
|
+
{...props}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
export const defaultSlots: TextEditorSlots = {
|
|
278
|
+
root: {
|
|
279
|
+
className: focusRing,
|
|
280
|
+
},
|
|
281
|
+
editor: {
|
|
282
|
+
className: 'bs-full',
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export const defaultTextSlots: TextEditorSlots = {
|
|
287
|
+
...defaultSlots,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const defaultMarkdownSlots: TextEditorSlots = {
|
|
291
|
+
...defaultSlots,
|
|
292
|
+
};
|
|
@@ -4,70 +4,61 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
|
-
import React, { type FC, useState } from 'react';
|
|
7
|
+
import React, { type FC, useMemo, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { TextObject } from '@dxos/echo-schema';
|
|
10
10
|
import { PublicKey } from '@dxos/keys';
|
|
11
11
|
import { faker } from '@dxos/random';
|
|
12
|
-
import { Tooltip
|
|
13
|
-
import { textBlockWidth } from '@dxos/react-ui-theme';
|
|
12
|
+
import { Tooltip } from '@dxos/react-ui';
|
|
13
|
+
import { mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
14
14
|
import { withTheme } from '@dxos/storybook-utils';
|
|
15
15
|
|
|
16
16
|
import { Toolbar } from './Toolbar';
|
|
17
17
|
import {
|
|
18
|
-
type Comment,
|
|
19
18
|
decorateMarkdown,
|
|
20
19
|
comments,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
createThemeExtensions,
|
|
24
|
-
formattingKeymap,
|
|
20
|
+
formatting,
|
|
21
|
+
image,
|
|
25
22
|
table,
|
|
26
23
|
useComments,
|
|
27
24
|
useFormattingState,
|
|
28
|
-
createMarkdownExtensions,
|
|
29
|
-
image,
|
|
30
25
|
} from '../../extensions';
|
|
31
|
-
import { useActionHandler,
|
|
26
|
+
import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
|
|
32
27
|
import translations from '../../translations';
|
|
28
|
+
import { MarkdownEditor } from '../TextEditor';
|
|
33
29
|
|
|
34
30
|
faker.seed(101);
|
|
35
31
|
|
|
36
|
-
const Story: FC<{ content: string }> = ({ content }) => {
|
|
37
|
-
const { themeMode } = useThemeContext();
|
|
32
|
+
const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) => {
|
|
38
33
|
const [item] = useState({ text: new TextObject(content) });
|
|
39
|
-
const
|
|
34
|
+
const [_comments, setComments] = useState<Comment[]>([]);
|
|
35
|
+
const [editorRef, viewInvalidated] = useEditorView(id);
|
|
36
|
+
const model = useTextModel({ text: item.text });
|
|
40
37
|
const [formattingState, formattingObserver] = useFormattingState();
|
|
41
|
-
const
|
|
42
|
-
() =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
decorateMarkdown(),
|
|
58
|
-
formattingKeymap(),
|
|
59
|
-
image(),
|
|
60
|
-
table(),
|
|
61
|
-
formattingObserver,
|
|
62
|
-
],
|
|
63
|
-
}),
|
|
64
|
-
[id, accessor, themeMode],
|
|
38
|
+
const extensions = useMemo(
|
|
39
|
+
() => [
|
|
40
|
+
decorateMarkdown(),
|
|
41
|
+
comments({
|
|
42
|
+
onCreate: ({ cursor }) => {
|
|
43
|
+
const id = PublicKey.random().toHex();
|
|
44
|
+
setComments((comments) => [...comments, { id, cursor }]);
|
|
45
|
+
return id;
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
formatting(),
|
|
49
|
+
image(),
|
|
50
|
+
table(),
|
|
51
|
+
formattingObserver,
|
|
52
|
+
],
|
|
53
|
+
[],
|
|
65
54
|
);
|
|
66
55
|
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
useComments(viewInvalidated ? null : editorRef.current, id, _comments);
|
|
57
|
+
const handleAction = useActionHandler(editorRef.current);
|
|
69
58
|
|
|
70
|
-
|
|
59
|
+
if (!model) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
71
62
|
|
|
72
63
|
return (
|
|
73
64
|
<div role='none' className='fixed inset-0 flex flex-col'>
|
|
@@ -76,7 +67,16 @@ const Story: FC<{ content: string }> = ({ content }) => {
|
|
|
76
67
|
<Toolbar.Separator />
|
|
77
68
|
<Toolbar.Extended />
|
|
78
69
|
</Toolbar.Root>
|
|
79
|
-
|
|
70
|
+
|
|
71
|
+
<MarkdownEditor
|
|
72
|
+
ref={editorRef}
|
|
73
|
+
model={model}
|
|
74
|
+
extensions={extensions}
|
|
75
|
+
slots={{
|
|
76
|
+
root: { className: mx(textBlockWidth) },
|
|
77
|
+
editor: { className: 'p-2' },
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
80
|
</div>
|
|
81
81
|
);
|
|
82
82
|
};
|
|
@@ -9,7 +9,7 @@ import '@preact/signals-react';
|
|
|
9
9
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
10
10
|
|
|
11
11
|
import { Repo } from '@dxos/automerge/automerge-repo';
|
|
12
|
-
import { createDocAccessor, Filter
|
|
12
|
+
import { createDocAccessor, Filter } from '@dxos/echo-schema';
|
|
13
13
|
import { type PublicKey } from '@dxos/keys';
|
|
14
14
|
import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
|
|
15
15
|
import { ClientRepeater } from '@dxos/react-client/testing';
|
|
@@ -17,9 +17,9 @@ import { useThemeContext } from '@dxos/react-ui';
|
|
|
17
17
|
import { withTheme } from '@dxos/storybook-utils';
|
|
18
18
|
|
|
19
19
|
import { automerge } from './automerge';
|
|
20
|
-
import {
|
|
20
|
+
import { DocAccessor } from './defs';
|
|
21
|
+
import { createBasicExtensions, createThemeExtensions, useTextEditor } from '../../hooks';
|
|
21
22
|
import translations from '../../translations';
|
|
22
|
-
import { createBasicExtensions, createThemeExtensions } from '../factories';
|
|
23
23
|
|
|
24
24
|
const initialContent = 'Hello world!';
|
|
25
25
|
|
|
@@ -34,19 +34,21 @@ type EditorProps = {
|
|
|
34
34
|
|
|
35
35
|
const Editor = ({ source, autoFocus }: EditorProps) => {
|
|
36
36
|
const { themeMode } = useThemeContext();
|
|
37
|
-
const
|
|
38
|
-
() =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
],
|
|
45
|
-
autoFocus,
|
|
46
|
-
}),
|
|
47
|
-
[source, themeMode],
|
|
37
|
+
const extensions = useMemo(
|
|
38
|
+
() => [
|
|
39
|
+
createBasicExtensions({ placeholder: 'Type here...' }),
|
|
40
|
+
createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2 bg-white dark:bg-black' } } }),
|
|
41
|
+
automerge(source),
|
|
42
|
+
],
|
|
43
|
+
[themeMode, source],
|
|
48
44
|
);
|
|
49
45
|
|
|
46
|
+
const { parentRef } = useTextEditor({
|
|
47
|
+
doc: DocAccessor.getValue(source),
|
|
48
|
+
extensions,
|
|
49
|
+
autoFocus,
|
|
50
|
+
});
|
|
51
|
+
|
|
50
52
|
return <div ref={parentRef} />;
|
|
51
53
|
};
|
|
52
54
|
|
|
@@ -112,8 +114,7 @@ const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
|
|
|
112
114
|
|
|
113
115
|
export const Default = {};
|
|
114
116
|
|
|
115
|
-
// TODO(burdon):
|
|
116
|
-
// chunk-6NX3RPDS.mjs:2021 ControlPipeline#5 Error: invariant violation: Feed already added
|
|
117
|
+
// TODO(burdon): Feed already added error.
|
|
117
118
|
export const WithEcho = {
|
|
118
119
|
decorators: [withTheme],
|
|
119
120
|
render: () => {
|
|
@@ -8,10 +8,9 @@ import { StateField, type Extension } from '@codemirror/state';
|
|
|
8
8
|
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
9
9
|
|
|
10
10
|
import { next as A } from '@dxos/automerge/automerge';
|
|
11
|
-
import { type DocAccessor } from '@dxos/echo-schema';
|
|
12
11
|
|
|
13
12
|
import { cursorConverter } from './cursor';
|
|
14
|
-
import { updateHeadsEffect, isReconcile, type State } from './defs';
|
|
13
|
+
import { updateHeadsEffect, isReconcile, type State, type DocAccessor } from './defs';
|
|
15
14
|
import { Syncer } from './sync';
|
|
16
15
|
import { Cursor } from '../cursor';
|
|
17
16
|
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import get from 'lodash.get';
|
|
6
6
|
|
|
7
7
|
import { next as A } from '@dxos/automerge/automerge';
|
|
8
|
-
import { type DocAccessor } from '@dxos/echo-schema';
|
|
9
8
|
import { log } from '@dxos/log';
|
|
10
9
|
|
|
10
|
+
import { type DocAccessor } from './defs';
|
|
11
11
|
import { type CursorConverter } from '../cursor';
|
|
12
12
|
|
|
13
13
|
export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter => ({
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
//
|
|
6
6
|
|
|
7
7
|
import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
|
|
8
|
+
import get from 'lodash.get';
|
|
8
9
|
|
|
9
|
-
import { type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
10
|
+
import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
10
11
|
|
|
11
12
|
export type State = {
|
|
12
13
|
path: Prop[];
|
|
@@ -30,3 +31,23 @@ export const reconcileAnnotation = Annotation.define<boolean>();
|
|
|
30
31
|
export const isReconcile = (tr: Transaction): boolean => {
|
|
31
32
|
return !!tr.annotation(reconcileAnnotation);
|
|
32
33
|
};
|
|
34
|
+
|
|
35
|
+
// TODO(burdon): Reconcile with echo.
|
|
36
|
+
export interface DocAccessor<T = any> {
|
|
37
|
+
handle: IDocHandle<T>;
|
|
38
|
+
get path(): string[]; // TODO(burdon): Getter or prop?
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// TODO(burdon): Reconcile with echo.
|
|
42
|
+
export const DocAccessor = {
|
|
43
|
+
getValue: (accessor: DocAccessor) => get(accessor.handle, accessor.path),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// TODO(burdon): Reconcile with echo.
|
|
47
|
+
export interface IDocHandle<T = any> {
|
|
48
|
+
docSync(): Doc<T> | undefined;
|
|
49
|
+
change(callback: ChangeFn<T>, options?: ChangeOptions<T>): void;
|
|
50
|
+
changeAt(heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): string[] | undefined;
|
|
51
|
+
addListener(event: 'change', listener: () => void): void;
|
|
52
|
+
removeListener(event: 'change', listener: () => void): void;
|
|
53
|
+
}
|
|
@@ -8,9 +8,16 @@ import { type StateField } from '@codemirror/state';
|
|
|
8
8
|
import { type EditorView } from '@codemirror/view';
|
|
9
9
|
|
|
10
10
|
import { next as A } from '@dxos/automerge/automerge';
|
|
11
|
-
import { type IDocHandle } from '@dxos/echo-schema';
|
|
12
11
|
|
|
13
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getLastHeads,
|
|
14
|
+
getPath,
|
|
15
|
+
type IDocHandle,
|
|
16
|
+
type State,
|
|
17
|
+
reconcileAnnotation,
|
|
18
|
+
updateHeads,
|
|
19
|
+
isReconcile,
|
|
20
|
+
} from './defs';
|
|
14
21
|
import { updateAutomerge } from './update-automerge';
|
|
15
22
|
import { updateCodeMirror } from './update-codemirror';
|
|
16
23
|
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
import { type EditorState, type StateField, type Transaction, type Text } from '@codemirror/state';
|
|
8
8
|
|
|
9
9
|
import { next as A, type Heads } from '@dxos/automerge/automerge';
|
|
10
|
-
import { type IDocHandle } from '@dxos/echo-schema';
|
|
11
10
|
|
|
12
|
-
import { type State } from './defs';
|
|
11
|
+
import { type IDocHandle, type State } from './defs';
|
|
13
12
|
|
|
14
13
|
export const updateAutomerge = (
|
|
15
14
|
field: StateField<State>,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { Event } from '@dxos/async';
|
|
17
17
|
import { Context } from '@dxos/context';
|
|
18
18
|
|
|
19
|
-
import { Cursor, type CursorConverter } from '
|
|
19
|
+
import { Cursor, type CursorConverter } from './cursor';
|
|
20
20
|
|
|
21
21
|
export interface AwarenessProvider {
|
|
22
22
|
remoteStateChange: Event<void>;
|
|
@@ -74,7 +74,7 @@ export const commandState = StateField.define<CommandState>({
|
|
|
74
74
|
|
|
75
75
|
return state;
|
|
76
76
|
},
|
|
77
|
-
provide: (
|
|
77
|
+
provide: (f) => [showTooltip.from(f, (value) => value.tooltip ?? null)],
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
|
|
@@ -13,7 +13,7 @@ import { log } from '@dxos/log';
|
|
|
13
13
|
import { nonNullable } from '@dxos/util';
|
|
14
14
|
|
|
15
15
|
import { Cursor } from './cursor';
|
|
16
|
-
import { type Comment, type Range } from '
|
|
16
|
+
import { type Comment, type Range } from '../hooks';
|
|
17
17
|
import { getToken } from '../styles';
|
|
18
18
|
import { callbackWrapper } from '../util';
|
|
19
19
|
|
|
@@ -508,16 +508,12 @@ export const focusComment = (view: EditorView, id: string, center = true) => {
|
|
|
508
508
|
/**
|
|
509
509
|
* Update comments state field.
|
|
510
510
|
*/
|
|
511
|
-
export const useComments = (view: EditorView | null
|
|
511
|
+
export const useComments = (view: EditorView | null, id: string, comments: Comment[] = []) => {
|
|
512
512
|
useEffect(() => {
|
|
513
513
|
if (view) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
view.dispatch({
|
|
518
|
-
effects: setComments.of({ id, comments }),
|
|
519
|
-
});
|
|
520
|
-
}
|
|
514
|
+
view.dispatch({
|
|
515
|
+
effects: setComments.of({ id, comments }),
|
|
516
|
+
});
|
|
521
517
|
}
|
|
522
518
|
}, [id, view, comments]);
|
|
523
519
|
};
|