@dxos/react-ui-editor 0.4.8-main.ac78619 → 0.4.8-main.beadd4b
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 +2703 -2714
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +18 -34
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +80 -5
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +10 -4
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +3 -1
- package/dist/types/src/components/Toolbar/Toolbar.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 +2 -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 +1 -15
- 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/sync.d.ts +2 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/{hooks → extensions/awareness}/awareness-provider.d.ts +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/index.d.ts +3 -0
- package/dist/types/src/extensions/awareness/index.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 +6 -7
- 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/doc.d.ts +3 -0
- package/dist/types/src/extensions/doc.d.ts.map +1 -0
- package/dist/types/src/extensions/factories.d.ts +49 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +4 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +5 -4
- 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 +5 -2
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +6 -0
- package/dist/types/src/extensions/markdown/image.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/modes.d.ts +1 -0
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/extensions/state.d.ts +20 -0
- package/dist/types/src/extensions/state.d.ts.map +1 -0
- package/dist/types/src/extensions/types.d.ts +9 -0
- package/dist/types/src/extensions/types.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +1 -4
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useDocAccessor.d.ts +7 -0
- package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +3 -44
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts +7 -0
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts +0 -3
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +25 -27
- package/src/components/TextEditor/TextEditor.stories.tsx +520 -23
- package/src/components/TextEditor/TextEditor.tsx +44 -155
- package/src/components/Toolbar/Toolbar.stories.tsx +44 -43
- package/src/components/Toolbar/Toolbar.tsx +86 -32
- package/src/extensions/automerge/automerge.stories.tsx +16 -17
- package/src/extensions/automerge/automerge.ts +2 -1
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/automerge/defs.ts +1 -22
- package/src/extensions/automerge/index.ts +0 -1
- package/src/extensions/automerge/sync.ts +2 -9
- package/src/extensions/automerge/update-automerge.ts +2 -1
- package/src/{hooks → extensions/awareness}/awareness-provider.ts +1 -1
- package/src/extensions/{awareness.ts → awareness/awareness.ts} +1 -1
- package/src/extensions/awareness/index.ts +6 -0
- package/src/extensions/command/state.ts +1 -1
- package/src/extensions/comments.ts +9 -5
- package/src/extensions/cursor-line-margin.ts +17 -18
- package/src/extensions/cursor.ts +1 -1
- package/src/extensions/doc.ts +14 -0
- package/src/extensions/factories.ts +167 -0
- package/src/extensions/index.ts +4 -1
- package/src/extensions/markdown/bundle.ts +11 -28
- package/src/extensions/markdown/decorate.ts +2 -4
- package/src/extensions/markdown/formatting.test.ts +13 -13
- package/src/extensions/markdown/formatting.ts +95 -86
- package/src/extensions/markdown/image.ts +7 -0
- package/src/extensions/markdown/table.ts +11 -3
- package/src/extensions/modes.ts +3 -1
- package/src/extensions/state.ts +90 -0
- package/src/extensions/types.ts +16 -0
- package/src/hooks/index.ts +1 -5
- package/src/hooks/useActionHandler.ts +5 -1
- package/src/hooks/useDocAccessor.ts +21 -0
- package/src/hooks/useTextEditor.stories.tsx +40 -35
- package/src/hooks/useTextEditor.ts +11 -120
- package/src/index.ts +1 -1
- package/src/themes/default.ts +1 -28
- package/src/translations.ts +1 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +0 -103
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/awareness.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner.d.ts +0 -4
- package/dist/types/src/extensions/outliner.d.ts.map +0 -1
- package/dist/types/src/hooks/awareness-provider.d.ts.map +0 -1
- package/dist/types/src/hooks/defs.d.ts +0 -20
- package/dist/types/src/hooks/defs.d.ts.map +0 -1
- package/dist/types/src/hooks/useEditorView.d.ts +0 -20
- package/dist/types/src/hooks/useEditorView.d.ts.map +0 -1
- package/dist/types/src/hooks/useTextModel.d.ts +0 -33
- package/dist/types/src/hooks/useTextModel.d.ts.map +0 -1
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +0 -524
- package/src/extensions/outliner.ts +0 -12
- package/src/hooks/defs.ts +0 -30
- package/src/hooks/useEditorView.ts +0 -32
- package/src/hooks/useTextModel.ts +0 -121
- /package/dist/types/src/extensions/{awareness.d.ts → awareness/awareness.d.ts} +0 -0
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { EditorState, type
|
|
6
|
-
import { EditorView
|
|
5
|
+
import { EditorState, type EditorStateConfig, type StateEffect } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
7
|
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
|
-
import defaultsDeep from 'lodash.defaultsdeep';
|
|
9
8
|
import React, {
|
|
10
|
-
type ComponentProps,
|
|
11
9
|
type KeyboardEventHandler,
|
|
12
10
|
forwardRef,
|
|
13
11
|
useCallback,
|
|
@@ -15,19 +13,12 @@ import React, {
|
|
|
15
13
|
useImperativeHandle,
|
|
16
14
|
useRef,
|
|
17
15
|
useState,
|
|
18
|
-
useMemo,
|
|
19
16
|
} from 'react';
|
|
20
17
|
|
|
21
|
-
import { DocAccessor } from '@dxos/echo-schema';
|
|
22
18
|
import { log } from '@dxos/log';
|
|
23
|
-
import { useThemeContext } from '@dxos/react-ui';
|
|
24
|
-
import { focusRing } from '@dxos/react-ui-theme';
|
|
25
19
|
import { isNotFalsy } from '@dxos/util';
|
|
26
20
|
|
|
27
|
-
import {
|
|
28
|
-
import { createBasicExtensions, createThemeExtensions, type EditorModel } from '../../hooks';
|
|
29
|
-
import { type ThemeStyles } from '../../styles';
|
|
30
|
-
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
21
|
+
import { documentId, editorMode, focusEvent } from '../../extensions';
|
|
31
22
|
import { logChanges } from '../../util';
|
|
32
23
|
|
|
33
24
|
export type CursorInfo = {
|
|
@@ -39,61 +30,49 @@ export type CursorInfo = {
|
|
|
39
30
|
after?: string;
|
|
40
31
|
};
|
|
41
32
|
|
|
42
|
-
export type
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
33
|
+
export type TextEditorProps = Pick<EditorStateConfig, 'doc' | 'selection' | 'extensions'> & {
|
|
34
|
+
id?: string;
|
|
35
|
+
className?: string;
|
|
55
36
|
autoFocus?: boolean;
|
|
56
|
-
|
|
37
|
+
scrollTo?: StateEffect<unknown>;
|
|
57
38
|
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[];
|
|
65
39
|
debug?: boolean;
|
|
40
|
+
dataTestId?: string;
|
|
66
41
|
};
|
|
67
42
|
|
|
43
|
+
let instanceCount = 0;
|
|
44
|
+
|
|
68
45
|
/**
|
|
69
|
-
*
|
|
46
|
+
* Thin wrapper for text editor.
|
|
47
|
+
* Handles tabster and focus management.
|
|
70
48
|
*/
|
|
71
|
-
// TODO(burdon):
|
|
72
|
-
export const
|
|
49
|
+
// TODO(burdon): Use useTextEditor internally.
|
|
50
|
+
export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
73
51
|
(
|
|
74
52
|
{
|
|
75
|
-
|
|
76
|
-
|
|
53
|
+
id,
|
|
54
|
+
doc,
|
|
55
|
+
selection,
|
|
56
|
+
extensions,
|
|
57
|
+
className,
|
|
77
58
|
autoFocus,
|
|
78
|
-
scrollTo = EditorView.scrollIntoView(0),
|
|
59
|
+
scrollTo = EditorView.scrollIntoView(0, { yMargin: 0 }),
|
|
79
60
|
moveToEndOfLine,
|
|
80
|
-
selection,
|
|
81
|
-
theme,
|
|
82
|
-
slots = defaultSlots,
|
|
83
|
-
extensions = [],
|
|
84
|
-
scrollPastEnd: _scrollPastEnd,
|
|
85
61
|
debug,
|
|
62
|
+
dataTestId,
|
|
86
63
|
},
|
|
87
64
|
forwardedRef,
|
|
88
65
|
) => {
|
|
89
|
-
|
|
90
|
-
const
|
|
66
|
+
// NOTE: Increments by 2 in strict mode.
|
|
67
|
+
const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
|
|
91
68
|
|
|
69
|
+
// TODO(burdon): Make tabster optional.
|
|
70
|
+
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
92
71
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
93
72
|
const [view, setView] = useState<EditorView | null>(null);
|
|
94
73
|
|
|
95
74
|
// The view ref can be used to focus the editor.
|
|
96
|
-
// NOTE:
|
|
75
|
+
// NOTE: Ref updates do not cause the parent to re-render; also the ref is not available immediately.
|
|
97
76
|
useImperativeHandle<EditorView | null, EditorView | null>(forwardedRef, () => view, [view]);
|
|
98
77
|
|
|
99
78
|
// Set focus.
|
|
@@ -106,53 +85,32 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
106
85
|
// Create editor state and view.
|
|
107
86
|
// The view is recreated if the model or extensions are changed.
|
|
108
87
|
useEffect(() => {
|
|
109
|
-
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
88
|
+
log('updating', { id, instanceId });
|
|
112
89
|
|
|
113
90
|
//
|
|
114
91
|
// EditorState
|
|
115
92
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
116
93
|
//
|
|
117
94
|
const state = EditorState.create({
|
|
118
|
-
doc
|
|
95
|
+
doc,
|
|
119
96
|
selection,
|
|
120
97
|
extensions: [
|
|
121
|
-
|
|
98
|
+
id && documentId.of(id),
|
|
99
|
+
// TODO(burdon): NOTE: Doesn't catch errors in keymap functions.
|
|
122
100
|
EditorView.exceptionSink.of((err) => {
|
|
123
101
|
log.catch(err);
|
|
124
102
|
}),
|
|
125
103
|
|
|
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
|
-
|
|
137
104
|
// Focus.
|
|
138
105
|
EditorView.updateListener.of((update) => {
|
|
139
106
|
update.transactions.forEach((transaction) => {
|
|
140
|
-
if (transaction.isUserEvent(
|
|
107
|
+
if (transaction.isUserEvent(focusEvent)) {
|
|
141
108
|
rootRef.current?.focus();
|
|
142
109
|
}
|
|
143
110
|
});
|
|
144
111
|
}),
|
|
145
112
|
|
|
146
|
-
|
|
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,
|
|
113
|
+
extensions,
|
|
156
114
|
].filter(isNotFalsy),
|
|
157
115
|
});
|
|
158
116
|
|
|
@@ -162,8 +120,9 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
162
120
|
//
|
|
163
121
|
const view = new EditorView({
|
|
164
122
|
state,
|
|
165
|
-
parent: rootRef.current
|
|
123
|
+
parent: rootRef.current!,
|
|
166
124
|
scrollTo,
|
|
125
|
+
|
|
167
126
|
// NOTE: Uncomment to debug/monitor all transactions.
|
|
168
127
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
169
128
|
dispatchTransactions: (trs, view) => {
|
|
@@ -174,24 +133,24 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
174
133
|
},
|
|
175
134
|
});
|
|
176
135
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (moveToEndOfLine) {
|
|
136
|
+
// Position cursor at end of first line.
|
|
137
|
+
if (moveToEndOfLine && !(scrollTo || selection)) {
|
|
180
138
|
const { to } = view.state.doc.lineAt(0);
|
|
181
|
-
view
|
|
139
|
+
view.dispatch({ selection: { anchor: to } });
|
|
182
140
|
}
|
|
183
141
|
|
|
184
142
|
// Remove tabster attribute (rely on custom keymap).
|
|
185
143
|
if (state.facet(editorMode).noTabster) {
|
|
186
|
-
rootRef.current
|
|
144
|
+
rootRef.current?.removeAttribute('data-tabster');
|
|
187
145
|
}
|
|
188
146
|
|
|
147
|
+
setView(view);
|
|
148
|
+
|
|
189
149
|
return () => {
|
|
190
150
|
view?.destroy();
|
|
191
151
|
setView(null);
|
|
192
152
|
};
|
|
193
|
-
|
|
194
|
-
}, [rootRef, extensions, model, readonly, themeMode]);
|
|
153
|
+
}, [doc, selection, extensions]);
|
|
195
154
|
|
|
196
155
|
// Focus editor on Enter (e.g., when tabbing to this component).
|
|
197
156
|
const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
|
@@ -211,82 +170,12 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
211
170
|
<div
|
|
212
171
|
role='none'
|
|
213
172
|
ref={rootRef}
|
|
214
|
-
key={model.id} // TODO(burdon): Remove?
|
|
215
173
|
tabIndex={0}
|
|
216
|
-
|
|
217
|
-
{
|
|
174
|
+
className={className}
|
|
175
|
+
data-testid={dataTestId}
|
|
218
176
|
{...tabsterDOMAttribute}
|
|
177
|
+
onKeyUp={handleKeyUp}
|
|
219
178
|
/>
|
|
220
179
|
);
|
|
221
180
|
},
|
|
222
181
|
);
|
|
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,79 +4,80 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
|
-
import React, { type FC,
|
|
7
|
+
import React, { type FC, 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 } from '@dxos/react-ui';
|
|
13
|
-
import {
|
|
12
|
+
import { Tooltip, useThemeContext } from '@dxos/react-ui';
|
|
13
|
+
import { 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,
|
|
18
19
|
decorateMarkdown,
|
|
19
20
|
comments,
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
createBasicExtensions,
|
|
22
|
+
createDataExtensions,
|
|
23
|
+
createThemeExtensions,
|
|
24
|
+
formattingKeymap,
|
|
22
25
|
table,
|
|
23
26
|
useComments,
|
|
24
27
|
useFormattingState,
|
|
28
|
+
createMarkdownExtensions,
|
|
29
|
+
image,
|
|
25
30
|
} from '../../extensions';
|
|
26
|
-
import {
|
|
31
|
+
import { useActionHandler, useDocAccessor, useTextEditor } from '../../hooks';
|
|
27
32
|
import translations from '../../translations';
|
|
28
|
-
import { MarkdownEditor } from '../TextEditor';
|
|
29
33
|
|
|
30
34
|
faker.seed(101);
|
|
31
35
|
|
|
32
|
-
const Story: FC<{
|
|
36
|
+
const Story: FC<{ content: string }> = ({ content }) => {
|
|
37
|
+
const { themeMode } = useThemeContext();
|
|
33
38
|
const [item] = useState({ text: new TextObject(content) });
|
|
34
|
-
const
|
|
35
|
-
const [editorRef, viewInvalidated] = useEditorView(id);
|
|
36
|
-
const model = useTextModel({ text: item.text });
|
|
39
|
+
const { id, doc, accessor } = useDocAccessor(item.text);
|
|
37
40
|
const [formattingState, formattingObserver] = useFormattingState();
|
|
38
|
-
const
|
|
39
|
-
() =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
41
|
+
const { parentRef, view } = useTextEditor(
|
|
42
|
+
() => ({
|
|
43
|
+
id,
|
|
44
|
+
doc,
|
|
45
|
+
extensions: [
|
|
46
|
+
createBasicExtensions(),
|
|
47
|
+
createMarkdownExtensions({ themeMode }),
|
|
48
|
+
createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2' } } }),
|
|
49
|
+
createDataExtensions({ id, text: accessor }),
|
|
50
|
+
comments({
|
|
51
|
+
onCreate: ({ cursor }) => {
|
|
52
|
+
const id = PublicKey.random().toHex();
|
|
53
|
+
setComments((comments) => [...comments, { id, cursor }]);
|
|
54
|
+
return id;
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
decorateMarkdown(),
|
|
58
|
+
formattingKeymap(),
|
|
59
|
+
image(),
|
|
60
|
+
table(),
|
|
61
|
+
formattingObserver,
|
|
62
|
+
],
|
|
63
|
+
}),
|
|
64
|
+
[id, accessor, themeMode],
|
|
54
65
|
);
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
const [_comments, setComments] = useState<Comment[]>([]);
|
|
68
|
+
useComments(view, id, _comments);
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
70
|
+
const handleAction = useActionHandler(view);
|
|
62
71
|
|
|
63
72
|
return (
|
|
64
73
|
<div role='none' className='fixed inset-0 flex flex-col'>
|
|
65
74
|
<Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
|
|
66
75
|
<Toolbar.Markdown />
|
|
76
|
+
<Toolbar.Custom onUpload={async (file) => ({ url: file.name })} />
|
|
67
77
|
<Toolbar.Separator />
|
|
68
|
-
<Toolbar.
|
|
78
|
+
<Toolbar.Actions />
|
|
69
79
|
</Toolbar.Root>
|
|
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
|
+
<div ref={parentRef} className={textBlockWidth} />
|
|
80
81
|
</div>
|
|
81
82
|
);
|
|
82
83
|
};
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ChatText,
|
|
8
8
|
Code,
|
|
9
9
|
CodeBlock,
|
|
10
|
+
Image,
|
|
10
11
|
Link,
|
|
11
12
|
ListBullets,
|
|
12
13
|
ListChecks,
|
|
@@ -25,7 +26,8 @@ import {
|
|
|
25
26
|
Quotes,
|
|
26
27
|
} from '@phosphor-icons/react';
|
|
27
28
|
import { createContext } from '@radix-ui/react-context';
|
|
28
|
-
import React, { type PropsWithChildren, useRef, useState } from 'react';
|
|
29
|
+
import React, { type PropsWithChildren, useEffect, useRef, useState } from 'react';
|
|
30
|
+
import { useDropzone } from 'react-dropzone';
|
|
29
31
|
|
|
30
32
|
import {
|
|
31
33
|
DensityProvider,
|
|
@@ -34,7 +36,8 @@ import {
|
|
|
34
36
|
type ThemedClassName,
|
|
35
37
|
Toolbar as NaturalToolbar,
|
|
36
38
|
Tooltip,
|
|
37
|
-
type ToolbarToggleGroupItemProps,
|
|
39
|
+
type ToolbarToggleGroupItemProps as NaturalToolbarToggleGroupItemProps,
|
|
40
|
+
type ToolbarButtonProps as NaturalToolbarButtonProps,
|
|
38
41
|
useTranslation,
|
|
39
42
|
} from '@dxos/react-ui';
|
|
40
43
|
import { getSize } from '@dxos/react-ui-theme';
|
|
@@ -111,9 +114,9 @@ type ButtonProps = {
|
|
|
111
114
|
disabled?: (state: Formatting) => boolean;
|
|
112
115
|
};
|
|
113
116
|
|
|
114
|
-
type
|
|
117
|
+
type ToolbarToggleButtonProps = NaturalToolbarToggleGroupItemProps & { Icon: Icon };
|
|
115
118
|
|
|
116
|
-
const
|
|
119
|
+
const ToolbarToggleButton = ({ Icon, children, ...props }: ToolbarToggleButtonProps) => {
|
|
117
120
|
return (
|
|
118
121
|
<Tooltip.Root>
|
|
119
122
|
<Tooltip.Trigger asChild>
|
|
@@ -132,6 +135,27 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
|
132
135
|
);
|
|
133
136
|
};
|
|
134
137
|
|
|
138
|
+
type ToolbarButtonProps = NaturalToolbarButtonProps & { Icon: Icon };
|
|
139
|
+
|
|
140
|
+
const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
141
|
+
return (
|
|
142
|
+
<Tooltip.Root>
|
|
143
|
+
<Tooltip.Trigger asChild>
|
|
144
|
+
<NaturalToolbar.Button variant='ghost' {...props} classNames={buttonStyles}>
|
|
145
|
+
<Icon className={iconStyles} />
|
|
146
|
+
<span className='sr-only'>{children}</span>
|
|
147
|
+
</NaturalToolbar.Button>
|
|
148
|
+
</Tooltip.Trigger>
|
|
149
|
+
<Tooltip.Portal>
|
|
150
|
+
<Tooltip.Content {...tooltipProps}>
|
|
151
|
+
{children}
|
|
152
|
+
<Tooltip.Arrow />
|
|
153
|
+
</Tooltip.Content>
|
|
154
|
+
</Tooltip.Portal>
|
|
155
|
+
</Tooltip.Root>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
135
159
|
const ToolbarSeparator = () => <div role='separator' className='grow' />;
|
|
136
160
|
|
|
137
161
|
const MarkdownHeading = () => {
|
|
@@ -227,7 +251,7 @@ const MarkdownStyles = () => {
|
|
|
227
251
|
value={markdownStyles.filter(({ getState }) => state && getState(state)).map(({ type }) => type)}
|
|
228
252
|
>
|
|
229
253
|
{markdownStyles.map(({ type, getState, Icon }) => (
|
|
230
|
-
<
|
|
254
|
+
<ToolbarToggleButton
|
|
231
255
|
key={type}
|
|
232
256
|
value={type}
|
|
233
257
|
Icon={Icon}
|
|
@@ -235,7 +259,7 @@ const MarkdownStyles = () => {
|
|
|
235
259
|
onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
236
260
|
>
|
|
237
261
|
{t(`${type} label`)}
|
|
238
|
-
</
|
|
262
|
+
</ToolbarToggleButton>
|
|
239
263
|
))}
|
|
240
264
|
</NaturalToolbar.ToggleGroup>
|
|
241
265
|
);
|
|
@@ -253,14 +277,14 @@ const MarkdownLists = () => {
|
|
|
253
277
|
return (
|
|
254
278
|
<NaturalToolbar.ToggleGroup type='single' value={state?.listStyle ? `list-${state.listStyle}` : ''}>
|
|
255
279
|
{markdownLists.map(({ type, getState, Icon }) => (
|
|
256
|
-
<
|
|
280
|
+
<ToolbarToggleButton
|
|
257
281
|
key={type}
|
|
258
282
|
value={type}
|
|
259
283
|
Icon={Icon}
|
|
260
284
|
onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
261
285
|
>
|
|
262
286
|
{t(`${type} label`)}
|
|
263
|
-
</
|
|
287
|
+
</ToolbarToggleButton>
|
|
264
288
|
))}
|
|
265
289
|
</NaturalToolbar.ToggleGroup>
|
|
266
290
|
);
|
|
@@ -292,7 +316,7 @@ const MarkdownBlocks = () => {
|
|
|
292
316
|
return (
|
|
293
317
|
<NaturalToolbar.ToggleGroup type='single' value={value?.type ?? ''}>
|
|
294
318
|
{markdownBlocks.map(({ type, disabled, getState, Icon }) => (
|
|
295
|
-
<
|
|
319
|
+
<ToolbarToggleButton
|
|
296
320
|
key={type}
|
|
297
321
|
value={type}
|
|
298
322
|
Icon={Icon}
|
|
@@ -300,7 +324,7 @@ const MarkdownBlocks = () => {
|
|
|
300
324
|
onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
301
325
|
>
|
|
302
326
|
{t(`${type} label`)}
|
|
303
|
-
</
|
|
327
|
+
</ToolbarToggleButton>
|
|
304
328
|
))}
|
|
305
329
|
</NaturalToolbar.ToggleGroup>
|
|
306
330
|
);
|
|
@@ -315,38 +339,68 @@ const MarkdownStandard = () => (
|
|
|
315
339
|
</>
|
|
316
340
|
);
|
|
317
341
|
|
|
318
|
-
|
|
342
|
+
// TODO(burdon): Make extensible.
|
|
343
|
+
export type MarkdownCustomOptions = {
|
|
344
|
+
onUpload?: (file: File) => Promise<{ url?: string }>;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
|
|
319
348
|
const { onAction } = useToolbarContext('MarkdownStyles');
|
|
320
349
|
const { t } = useTranslation(translationKey);
|
|
350
|
+
// https://react-dropzone.js.org/#src
|
|
351
|
+
const { acceptedFiles, getInputProps, open } = useDropzone({
|
|
352
|
+
multiple: false,
|
|
353
|
+
noDrag: true,
|
|
354
|
+
accept: {
|
|
355
|
+
'image/*': ['.jpg', '.jpeg', '.png', '.gif'],
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
if (onUpload && acceptedFiles.length) {
|
|
360
|
+
setTimeout(async () => {
|
|
361
|
+
// NOTE: Clone file since react-dropzone patches in a non-standard `path` property, which confuses IPFS.
|
|
362
|
+
const f = acceptedFiles[0];
|
|
363
|
+
const file = new File([f], f.name, {
|
|
364
|
+
type: f.type,
|
|
365
|
+
lastModified: f.lastModified,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const { url } = await onUpload(file);
|
|
369
|
+
if (url) {
|
|
370
|
+
onAction?.({ type: 'image', data: url });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}, [acceptedFiles]);
|
|
375
|
+
|
|
321
376
|
return (
|
|
322
|
-
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
</Tooltip.Portal>
|
|
340
|
-
</Tooltip.Root>
|
|
377
|
+
<>
|
|
378
|
+
<input {...getInputProps()} />
|
|
379
|
+
<ToolbarButton value='image' Icon={Image} onClick={() => open()}>
|
|
380
|
+
{t('image label')}
|
|
381
|
+
</ToolbarButton>
|
|
382
|
+
</>
|
|
383
|
+
);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// TODO(burdon): Make extensible.
|
|
387
|
+
const MarkdownActions = () => {
|
|
388
|
+
const { onAction } = useToolbarContext('MarkdownStyles');
|
|
389
|
+
const { t } = useTranslation(translationKey);
|
|
390
|
+
return (
|
|
391
|
+
<ToolbarButton value='comment' Icon={ChatText} onClick={() => onAction?.({ type: 'comment' })}>
|
|
392
|
+
{t('comment label')}
|
|
393
|
+
</ToolbarButton>
|
|
341
394
|
);
|
|
342
395
|
};
|
|
343
396
|
|
|
344
397
|
export const Toolbar = {
|
|
345
398
|
Root: ToolbarRoot,
|
|
346
|
-
Button:
|
|
399
|
+
Button: ToolbarToggleButton,
|
|
347
400
|
Separator: ToolbarSeparator,
|
|
348
401
|
Markdown: MarkdownStandard,
|
|
349
|
-
|
|
402
|
+
Custom: MarkdownCustom,
|
|
403
|
+
Actions: MarkdownActions,
|
|
350
404
|
};
|
|
351
405
|
|
|
352
406
|
export { useToolbarContext };
|