@dxos/react-ui-editor 0.3.11-main.7ff9773 → 0.3.11-main.80db41b
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 +1402 -934
- 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 +1 -2
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/{yjs.stories.d.ts → hooks.stories.d.ts} +13 -2
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +2 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +5 -5
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +2 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts +2 -4
- package/dist/types/src/extensions/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/basic.d.ts +2 -2
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +9 -5
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +0 -7
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +2 -2
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/code.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
- package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +8 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
- package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +1 -4
- package/dist/types/src/hooks/defs.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +3 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useEditorView.d.ts +17 -0
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +12 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +39 -29
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +15 -19
- package/src/components/TextEditor/TextEditor.tsx +46 -27
- package/src/components/TextEditor/automerge.stories.tsx +1 -2
- package/src/components/TextEditor/hooks.stories.tsx +106 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
- package/src/components/Toolbar/Toolbar.tsx +166 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +1 -0
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +44 -10
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +9 -9
- package/src/extensions/automerge/semaphore.ts +17 -19
- package/src/extensions/automerge/update-automerge.ts +12 -6
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +21 -35
- package/src/extensions/basic.ts +17 -15
- package/src/extensions/blast.ts +4 -1
- package/src/extensions/comments.ts +137 -79
- package/src/extensions/index.ts +0 -7
- package/src/extensions/markdown/bundle.ts +9 -8
- package/src/extensions/markdown/code.ts +150 -0
- package/src/extensions/markdown/formatting.ts +188 -0
- package/src/extensions/markdown/heading.ts +59 -0
- package/src/extensions/markdown/highlight.ts +9 -15
- package/src/extensions/{hr.ts → markdown/hr.ts} +3 -4
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +13 -4
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +2 -3
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +1 -6
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useActionHandler.ts +62 -0
- package/src/hooks/useEditorView.ts +29 -0
- package/src/hooks/useTextEditor.ts +153 -0
- package/src/hooks/useTextModel.ts +65 -12
- package/src/index.ts +1 -1
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +11 -8
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +0 -26
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -4
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -28
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.d.ts.map +0 -1
- package/src/components/TextEditor/codemirror.stories.ts +0 -115
- package/src/components/TextEditor/comments.stories.tsx +0 -357
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/code.ts +0 -104
- package/src/extensions/yjs/cursor.ts +0 -22
- package/src/extensions/yjs/index.ts +0 -5
- package/src/extensions/yjs/yjs.test.ts +0 -35
- package/src/extensions/yjs/yjs.ts +0 -16
- package/src/hooks/automerge.ts +0 -53
- package/src/hooks/yjs.ts +0 -157
- package/src/testing/index.ts +0 -6
- package/src/testing/proto/gen/schema.ts +0 -49
- package/src/testing/proto/index.ts +0 -5
- package/src/testing/proto/schema.proto +0 -15
- package/src/testing/replicator.ts +0 -184
- /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
- /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
- /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
- /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
- /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
- /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
- /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
- /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
|
@@ -19,11 +19,14 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
19
19
|
|
|
20
20
|
import { MarkdownEditor, type TextEditorProps } from './TextEditor';
|
|
21
21
|
import {
|
|
22
|
+
type CommentsOptions,
|
|
23
|
+
type LinkOptions,
|
|
22
24
|
autocomplete,
|
|
23
25
|
blast,
|
|
24
26
|
code,
|
|
25
27
|
comments,
|
|
26
28
|
defaultOptions,
|
|
29
|
+
heading,
|
|
27
30
|
hr,
|
|
28
31
|
image,
|
|
29
32
|
link,
|
|
@@ -31,16 +34,11 @@ import {
|
|
|
31
34
|
table,
|
|
32
35
|
tasklist,
|
|
33
36
|
typewriter,
|
|
34
|
-
type CommentsOptions,
|
|
35
|
-
type LinkOptions,
|
|
36
37
|
useComments,
|
|
38
|
+
formatting,
|
|
37
39
|
} from '../../extensions';
|
|
38
40
|
import { type Comment, useTextModel } from '../../hooks';
|
|
39
41
|
|
|
40
|
-
// Extensions:
|
|
41
|
-
// TODO(burdon): Table of contents.
|
|
42
|
-
// TODO(burdon): Front-matter
|
|
43
|
-
|
|
44
42
|
faker.seed(101);
|
|
45
43
|
|
|
46
44
|
const str = (...lines: string[]) => lines.join('\n');
|
|
@@ -82,7 +80,7 @@ const text = {
|
|
|
82
80
|
'## Code',
|
|
83
81
|
'',
|
|
84
82
|
'```',
|
|
85
|
-
'
|
|
83
|
+
'$ ls -las',
|
|
86
84
|
'```',
|
|
87
85
|
'',
|
|
88
86
|
'```tsx',
|
|
@@ -213,11 +211,10 @@ const onRenderLink: LinkOptions['onRender'] = (el, url) => {
|
|
|
213
211
|
type StoryProps = {
|
|
214
212
|
text?: string;
|
|
215
213
|
comments?: Comment[];
|
|
216
|
-
automerge?: boolean;
|
|
217
214
|
} & Pick<TextEditorProps, 'readonly' | 'placeholder' | 'slots' | 'extensions'>;
|
|
218
215
|
|
|
219
|
-
const Story = ({ text, comments,
|
|
220
|
-
const [item] = useState({ text: new TextObject(text
|
|
216
|
+
const Story = ({ text, comments, placeholder = 'New document.', ...props }: StoryProps) => {
|
|
217
|
+
const [item] = useState({ text: new TextObject(text) });
|
|
221
218
|
const view = useRef<EditorView>(null);
|
|
222
219
|
const model = useTextModel({ text: item.text });
|
|
223
220
|
useComments(view.current, comments);
|
|
@@ -248,12 +245,11 @@ const defaults = [
|
|
|
248
245
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
249
246
|
}),
|
|
250
247
|
code(),
|
|
248
|
+
formatting(),
|
|
249
|
+
heading(),
|
|
251
250
|
hr(),
|
|
252
251
|
image(),
|
|
253
252
|
link({ onRender: onRenderLink, onHover: onHoverLinkTooltip }),
|
|
254
|
-
// mention({
|
|
255
|
-
// onSearch: (text) => names.filter((name) => name.toLowerCase().startsWith(text.toLowerCase())),
|
|
256
|
-
// }),
|
|
257
253
|
table(),
|
|
258
254
|
tasklist(),
|
|
259
255
|
];
|
|
@@ -290,11 +286,11 @@ export const Links = {
|
|
|
290
286
|
};
|
|
291
287
|
|
|
292
288
|
export const Code = {
|
|
293
|
-
render: () => <Story text={str(text.code, text.footer)} extensions={[code()]}
|
|
289
|
+
render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} />,
|
|
294
290
|
};
|
|
295
291
|
|
|
296
292
|
export const Image = {
|
|
297
|
-
render: () => <Story text={str(text.image, text.footer)}
|
|
293
|
+
render: () => <Story text={str(text.image, text.footer)} extensions={[image()]} />,
|
|
298
294
|
};
|
|
299
295
|
|
|
300
296
|
export const Lists = {
|
|
@@ -345,18 +341,18 @@ export const Mention = {
|
|
|
345
341
|
|
|
346
342
|
export const Comments = {
|
|
347
343
|
render: () => {
|
|
348
|
-
const [
|
|
344
|
+
const [_comments, setComments] = useState<Comment[]>([]);
|
|
349
345
|
|
|
350
346
|
return (
|
|
351
347
|
<Story
|
|
352
348
|
text={str('# Comments', '', text.paragraphs, text.footer)}
|
|
353
|
-
comments={
|
|
349
|
+
comments={_comments}
|
|
354
350
|
extensions={[
|
|
355
351
|
comments({
|
|
356
352
|
onHover: onCommentsHover,
|
|
357
353
|
onCreate: (range) => {
|
|
358
354
|
const id = PublicKey.random().toHex();
|
|
359
|
-
|
|
355
|
+
setComments((commentRanges) => [...commentRanges, { id, cursor: range }]);
|
|
360
356
|
return id;
|
|
361
357
|
},
|
|
362
358
|
onSelect: (state) => {
|
|
@@ -366,7 +362,7 @@ export const Comments = {
|
|
|
366
362
|
'update',
|
|
367
363
|
JSON.stringify({
|
|
368
364
|
comments: state.comments.length,
|
|
369
|
-
active: state.selection.
|
|
365
|
+
active: state.selection.current?.slice(0, 8),
|
|
370
366
|
closest: state.selection.closest?.slice(0, 8),
|
|
371
367
|
}),
|
|
372
368
|
);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
|
|
6
6
|
import { EditorView } from '@codemirror/view';
|
|
7
|
-
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
7
|
+
// import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
8
|
import { vim } from '@replit/codemirror-vim';
|
|
9
9
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
10
10
|
import React, {
|
|
@@ -20,9 +20,10 @@ import React, {
|
|
|
20
20
|
|
|
21
21
|
import { log } from '@dxos/log';
|
|
22
22
|
import { useThemeContext } from '@dxos/react-ui';
|
|
23
|
-
import {
|
|
23
|
+
import { focusRing, mx } from '@dxos/react-ui-theme';
|
|
24
|
+
import { isNotFalsy } from '@dxos/util';
|
|
24
25
|
|
|
25
|
-
import {
|
|
26
|
+
import { createBasicBundle, createMarkdownExtensions } from '../../extensions';
|
|
26
27
|
import { type EditorModel } from '../../hooks';
|
|
27
28
|
import { type ThemeStyles } from '../../styles';
|
|
28
29
|
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
@@ -53,10 +54,10 @@ export type TextEditorSlots = {
|
|
|
53
54
|
|
|
54
55
|
// TODO(burdon): Spellcheck?
|
|
55
56
|
export type TextEditorProps = {
|
|
56
|
-
model: EditorModel;
|
|
57
|
+
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
57
58
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
autoFocus?: boolean;
|
|
60
|
+
lineWrapping?: boolean;
|
|
60
61
|
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
61
62
|
selection?: { anchor: number; head?: number };
|
|
62
63
|
editorMode?: EditorMode; // TODO(burdon): Factor out.
|
|
@@ -64,6 +65,7 @@ export type TextEditorProps = {
|
|
|
64
65
|
theme?: ThemeStyles;
|
|
65
66
|
slots?: TextEditorSlots;
|
|
66
67
|
extensions?: Extension[];
|
|
68
|
+
debug?: boolean;
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
/**
|
|
@@ -71,10 +73,26 @@ export type TextEditorProps = {
|
|
|
71
73
|
*/
|
|
72
74
|
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
73
75
|
(
|
|
74
|
-
{
|
|
76
|
+
{
|
|
77
|
+
model,
|
|
78
|
+
readonly,
|
|
79
|
+
autoFocus,
|
|
80
|
+
scrollTo,
|
|
81
|
+
selection,
|
|
82
|
+
editorMode,
|
|
83
|
+
theme,
|
|
84
|
+
slots = defaultSlots,
|
|
85
|
+
extensions = [],
|
|
86
|
+
debug,
|
|
87
|
+
},
|
|
75
88
|
forwardedRef,
|
|
76
89
|
) => {
|
|
77
|
-
|
|
90
|
+
// TODO(burdon): Hook causes error even if properties are not spread into div.
|
|
91
|
+
// Uncaught TypeError: Cannot read properties of undefined (reading 'relatedTarget')
|
|
92
|
+
// Uses event.detail, which is deprecated (not event.details). At runtime the event has a property `details`.
|
|
93
|
+
// https://github.com/microsoft/tabster/blob/master/src/State/FocusedElement.ts#L348 (e.detail.relatedTarget)
|
|
94
|
+
// https://github.com/microsoft/keyborg/blob/49e49b2c3ba0a5f6cc518ac46825d7551def8109/src/FocusEvent.ts#L58
|
|
95
|
+
// const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
78
96
|
const { themeMode } = useThemeContext();
|
|
79
97
|
|
|
80
98
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
@@ -86,13 +104,15 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
86
104
|
|
|
87
105
|
// Set focus.
|
|
88
106
|
useEffect(() => {
|
|
89
|
-
if (
|
|
107
|
+
if (autoFocus && !view?.hasFocus) {
|
|
108
|
+
if (view?.state.selection.main?.from === 0) {
|
|
109
|
+
// Start at end of line.
|
|
110
|
+
const { to } = view.state.doc.lineAt(0);
|
|
111
|
+
view?.dispatch({ selection: { anchor: to } });
|
|
112
|
+
}
|
|
90
113
|
view?.focus();
|
|
91
114
|
}
|
|
92
|
-
}, [view,
|
|
93
|
-
|
|
94
|
-
// Monitor awareness.
|
|
95
|
-
useAwareness(model);
|
|
115
|
+
}, [view, autoFocus]);
|
|
96
116
|
|
|
97
117
|
// Create editor state and view.
|
|
98
118
|
// The view is recreated if the model or extensions are changed.
|
|
@@ -123,18 +143,19 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
123
143
|
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
124
144
|
|
|
125
145
|
// State.
|
|
146
|
+
EditorView.editable.of(!readonly),
|
|
126
147
|
EditorState.readOnly.of(!!readonly),
|
|
127
148
|
|
|
128
149
|
// Storage and replication.
|
|
129
150
|
// NOTE: This must come before user extensions.
|
|
130
151
|
model.extension,
|
|
131
152
|
|
|
132
|
-
// TODO(burdon): Factor out (
|
|
153
|
+
// TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
|
|
133
154
|
editorMode === 'vim' && vim(),
|
|
134
155
|
|
|
135
156
|
// Custom.
|
|
136
157
|
...extensions,
|
|
137
|
-
].filter(
|
|
158
|
+
].filter(isNotFalsy),
|
|
138
159
|
});
|
|
139
160
|
|
|
140
161
|
//
|
|
@@ -148,11 +169,10 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
148
169
|
// NOTE: Uncomment to debug/monitor all transactions.
|
|
149
170
|
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
150
171
|
dispatchTransactions: (trs, view) => {
|
|
151
|
-
view.update(trs);
|
|
152
|
-
const debug = false;
|
|
153
172
|
if (debug) {
|
|
154
173
|
logChanges(trs);
|
|
155
174
|
}
|
|
175
|
+
view.update(trs);
|
|
156
176
|
},
|
|
157
177
|
});
|
|
158
178
|
|
|
@@ -167,6 +187,7 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
167
187
|
|
|
168
188
|
// Handles tab/focus.
|
|
169
189
|
// Pressing Escape focuses the outer div (to support tab navigation); pressing Enter refocuses the editor.
|
|
190
|
+
// TODO(burdon): Convert to keymap?
|
|
170
191
|
const handleKeyUp = useCallback(
|
|
171
192
|
(event: KeyboardEvent) => {
|
|
172
193
|
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
@@ -178,7 +199,7 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
178
199
|
|
|
179
200
|
case 'Escape': {
|
|
180
201
|
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
181
|
-
|
|
202
|
+
view?.focus();
|
|
182
203
|
}
|
|
183
204
|
break;
|
|
184
205
|
}
|
|
@@ -192,25 +213,24 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
192
213
|
key={model.id}
|
|
193
214
|
role='none'
|
|
194
215
|
tabIndex={0}
|
|
195
|
-
onKeyUp={handleKeyUp}
|
|
196
216
|
{...slots.root}
|
|
197
|
-
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
217
|
+
// {...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
218
|
+
onKeyUp={handleKeyUp}
|
|
198
219
|
ref={rootRef}
|
|
199
220
|
/>
|
|
200
221
|
);
|
|
201
222
|
},
|
|
202
223
|
);
|
|
203
224
|
|
|
204
|
-
// TODO(burdon): Single-line/scroll.
|
|
205
225
|
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
206
|
-
({ readonly, placeholder,
|
|
226
|
+
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
207
227
|
const { themeMode } = useThemeContext();
|
|
208
228
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
209
229
|
return (
|
|
210
230
|
<BaseTextEditor
|
|
211
231
|
ref={forwardedRef}
|
|
212
232
|
readonly={readonly}
|
|
213
|
-
extensions={[
|
|
233
|
+
extensions={[createBasicBundle({ themeMode, placeholder, lineWrapping }), ...extensions]}
|
|
214
234
|
theme={theme}
|
|
215
235
|
slots={updatedSlots}
|
|
216
236
|
{...props}
|
|
@@ -227,7 +247,7 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
227
247
|
<BaseTextEditor
|
|
228
248
|
ref={forwardedRef}
|
|
229
249
|
readonly={readonly}
|
|
230
|
-
extensions={[
|
|
250
|
+
extensions={[createMarkdownExtensions({ themeMode, placeholder }), ...extensions]}
|
|
231
251
|
theme={theme}
|
|
232
252
|
slots={updatedSlots}
|
|
233
253
|
{...props}
|
|
@@ -238,11 +258,10 @@ export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
238
258
|
|
|
239
259
|
export const defaultSlots: TextEditorSlots = {
|
|
240
260
|
root: {
|
|
241
|
-
|
|
242
|
-
className: mx('flex flex-col grow overflow-y-auto', inputSurface),
|
|
261
|
+
className: mx('grow', focusRing),
|
|
243
262
|
},
|
|
244
263
|
editor: {
|
|
245
|
-
className: '
|
|
264
|
+
className: 'bs-full',
|
|
246
265
|
},
|
|
247
266
|
};
|
|
248
267
|
|
|
@@ -13,7 +13,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
13
13
|
|
|
14
14
|
import { type Prop } from '@dxos/automerge/automerge';
|
|
15
15
|
import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
|
|
16
|
-
import { Filter
|
|
16
|
+
import { Filter } from '@dxos/echo-schema';
|
|
17
17
|
import { type PublicKey } from '@dxos/keys';
|
|
18
18
|
import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
|
|
19
19
|
import { useIdentity } from '@dxos/react-client/halo';
|
|
@@ -130,7 +130,6 @@ const EchoStory = ({ id, spaceKey }: { id: number; spaceKey: PublicKey }) => {
|
|
|
130
130
|
|
|
131
131
|
export const WithEcho = {
|
|
132
132
|
render: () => {
|
|
133
|
-
setGlobalAutomergePreference(true);
|
|
134
133
|
return (
|
|
135
134
|
<ClientRepeater
|
|
136
135
|
count={2}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
import { useThemeContext } from '@dxos/react-ui';
|
|
10
|
+
import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
11
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
12
|
+
|
|
13
|
+
import { TextEditor } from './TextEditor';
|
|
14
|
+
import {
|
|
15
|
+
code,
|
|
16
|
+
createMarkdownExtensions,
|
|
17
|
+
formatting,
|
|
18
|
+
heading,
|
|
19
|
+
hr,
|
|
20
|
+
image,
|
|
21
|
+
link,
|
|
22
|
+
table,
|
|
23
|
+
tasklist,
|
|
24
|
+
} from '../../extensions';
|
|
25
|
+
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
26
|
+
import { markdownTheme } from '../../themes';
|
|
27
|
+
import { Toolbar } from '../Toolbar';
|
|
28
|
+
|
|
29
|
+
// TODO(burdon): Demo toolbar with hooks.
|
|
30
|
+
// TODO(burdon): Build components from hooks and adapters for model/extensions, etc.
|
|
31
|
+
// TODO(burdon): Remove BaseTextEditor.
|
|
32
|
+
// TODO(burdon): Move scrolling container layout/logic into TextEditor/MarkdownEditor components.
|
|
33
|
+
// TODO(burdon): Define keymap in composer framework format.
|
|
34
|
+
|
|
35
|
+
type StoryProps = {
|
|
36
|
+
autoFocus?: boolean;
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
doc?: string;
|
|
39
|
+
readonly?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
43
|
+
const { themeMode } = useThemeContext();
|
|
44
|
+
const { parentRef, view } = useTextEditor({
|
|
45
|
+
autoFocus,
|
|
46
|
+
doc,
|
|
47
|
+
extensions: [
|
|
48
|
+
//
|
|
49
|
+
createDataExtensions({ readonly }),
|
|
50
|
+
createThemeExtensions({
|
|
51
|
+
themeMode,
|
|
52
|
+
theme: markdownTheme,
|
|
53
|
+
slots: {
|
|
54
|
+
// TODO(burdon): Document classes re base theme.
|
|
55
|
+
// Semantic tokens (e.g., replacement of input surface).
|
|
56
|
+
editor: {
|
|
57
|
+
className: 'h-full p-4 bg-white text-black dark:bg-black dark:text-white',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
// TODO(burdon): Move lineWrapping.
|
|
62
|
+
createMarkdownExtensions({ placeholder, lineWrapping: true }),
|
|
63
|
+
// TODO(burdon): Move into markdown bundle (with React callbacks).
|
|
64
|
+
code(),
|
|
65
|
+
formatting(),
|
|
66
|
+
heading(),
|
|
67
|
+
hr(),
|
|
68
|
+
image(),
|
|
69
|
+
link(),
|
|
70
|
+
table(),
|
|
71
|
+
tasklist(),
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const handleAction = useActionHandler(view);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
79
|
+
<div className='flex h-full justify-center'>
|
|
80
|
+
<div className='flex flex-col h-full w-[800px]'>
|
|
81
|
+
<Toolbar.Root onAction={handleAction}>
|
|
82
|
+
<Toolbar.Markdown />
|
|
83
|
+
</Toolbar.Root>
|
|
84
|
+
|
|
85
|
+
{/* TODO(burdon): Handle scrolling in component wrapper (like this). */}
|
|
86
|
+
<div role='none' className='h-full overflow-y-auto' ref={parentRef} />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default {
|
|
94
|
+
title: 'react-ui-editor/useTextEditor',
|
|
95
|
+
component: TextEditor,
|
|
96
|
+
decorators: [withTheme],
|
|
97
|
+
render: (args: StoryProps) => <Story {...args} />,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Default = {
|
|
101
|
+
args: {
|
|
102
|
+
autoFocus: true,
|
|
103
|
+
placeholder: 'Text...',
|
|
104
|
+
doc: '# Demo\n\nThis is a document.\n\n',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import { faker } from '@faker-js/faker';
|
|
8
|
+
import React, { type FC, useMemo, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import { TextObject } from '@dxos/echo-schema';
|
|
11
|
+
import { PublicKey } from '@dxos/keys';
|
|
12
|
+
import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
13
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
14
|
+
|
|
15
|
+
import { Toolbar } from './Toolbar';
|
|
16
|
+
import { code, comments, formatting, heading, image, table, tasklist, useComments } from '../../extensions';
|
|
17
|
+
import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
|
|
18
|
+
import { MarkdownEditor } from '../TextEditor';
|
|
19
|
+
|
|
20
|
+
faker.seed(101);
|
|
21
|
+
|
|
22
|
+
const Story: FC<{ content: string }> = ({ content }) => {
|
|
23
|
+
const [item] = useState({ text: new TextObject(content) });
|
|
24
|
+
const [_comments, setComments] = useState<Comment[]>([]);
|
|
25
|
+
const [editorRef, editorView] = useEditorView();
|
|
26
|
+
const model = useTextModel({ text: item.text });
|
|
27
|
+
const extensions = useMemo(
|
|
28
|
+
() => [
|
|
29
|
+
code(),
|
|
30
|
+
comments({
|
|
31
|
+
onCreate: (cursor) => {
|
|
32
|
+
const id = PublicKey.random().toHex();
|
|
33
|
+
setComments((comments) => [...comments, { id, cursor }]);
|
|
34
|
+
return id;
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
formatting(),
|
|
38
|
+
heading(),
|
|
39
|
+
image(),
|
|
40
|
+
table(),
|
|
41
|
+
tasklist(),
|
|
42
|
+
],
|
|
43
|
+
[],
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
useComments(editorView, _comments);
|
|
47
|
+
const handleAction = useActionHandler(editorView);
|
|
48
|
+
|
|
49
|
+
if (!model) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
55
|
+
<div className='flex h-full justify-center'>
|
|
56
|
+
<div className='flex flex-col h-full w-[800px]'>
|
|
57
|
+
<Toolbar.Root onAction={handleAction}>
|
|
58
|
+
<Toolbar.Markdown />
|
|
59
|
+
<Toolbar.Separator />
|
|
60
|
+
<Toolbar.Extended />
|
|
61
|
+
</Toolbar.Root>
|
|
62
|
+
<MarkdownEditor ref={editorRef} model={model} extensions={extensions} />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default {
|
|
70
|
+
title: 'react-ui-editor/Toolbar',
|
|
71
|
+
component: Toolbar,
|
|
72
|
+
render: (args: any) => <Story {...args} />,
|
|
73
|
+
decorators: [withTheme],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const content = [
|
|
77
|
+
'Demo',
|
|
78
|
+
'',
|
|
79
|
+
'The editor supports **Markdown** styles.',
|
|
80
|
+
'',
|
|
81
|
+
faker.lorem.paragraph({ min: 5, max: 8 }),
|
|
82
|
+
'',
|
|
83
|
+
'',
|
|
84
|
+
].join('\n');
|
|
85
|
+
|
|
86
|
+
export const Default = {
|
|
87
|
+
args: {
|
|
88
|
+
content,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type Icon,
|
|
7
|
+
At,
|
|
8
|
+
CaretRight,
|
|
9
|
+
ChatText,
|
|
10
|
+
Code,
|
|
11
|
+
Image,
|
|
12
|
+
Link,
|
|
13
|
+
ListBullets,
|
|
14
|
+
ListChecks,
|
|
15
|
+
ListNumbers,
|
|
16
|
+
TextStrikethrough,
|
|
17
|
+
Table,
|
|
18
|
+
TextB,
|
|
19
|
+
TextItalic,
|
|
20
|
+
} from '@phosphor-icons/react';
|
|
21
|
+
import { createContext } from '@radix-ui/react-context';
|
|
22
|
+
import React, { type PropsWithChildren } from 'react';
|
|
23
|
+
|
|
24
|
+
import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
|
|
25
|
+
import { getSize } from '@dxos/react-ui-theme';
|
|
26
|
+
|
|
27
|
+
// TODO(burdon): Revert to string to make extensible?
|
|
28
|
+
export type ActionType =
|
|
29
|
+
| 'blockquote'
|
|
30
|
+
| 'bold'
|
|
31
|
+
| 'codeblock'
|
|
32
|
+
| 'comment'
|
|
33
|
+
| 'heading'
|
|
34
|
+
| 'image'
|
|
35
|
+
| 'italic'
|
|
36
|
+
| 'link'
|
|
37
|
+
| 'list'
|
|
38
|
+
| 'list-ordered'
|
|
39
|
+
| 'list-tasks'
|
|
40
|
+
| 'mention'
|
|
41
|
+
| 'prompt'
|
|
42
|
+
| 'strikethrough'
|
|
43
|
+
| 'table';
|
|
44
|
+
|
|
45
|
+
export type Action = {
|
|
46
|
+
type: ActionType;
|
|
47
|
+
data?: any;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ToolbarProps = PropsWithChildren<{
|
|
51
|
+
onAction?: (action: Action) => void;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
|
|
55
|
+
|
|
56
|
+
const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
|
|
57
|
+
return (
|
|
58
|
+
<ToolbarContextProvider onAction={onAction}>
|
|
59
|
+
<DensityProvider density='fine'>
|
|
60
|
+
<div role='toolbar' className='flex w-full shrink-0 p-2 gap-4 items-center whitespace-nowrap overflow-hidden'>
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
</DensityProvider>
|
|
64
|
+
</ToolbarContextProvider>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ToolbarButtonProps = {
|
|
69
|
+
Icon: Icon;
|
|
70
|
+
onClick: () => Action | undefined;
|
|
71
|
+
} & NonNullable<Pick<ButtonProps, 'title'>>;
|
|
72
|
+
|
|
73
|
+
const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
|
|
74
|
+
const { onAction } = useToolbarContext('ToolbarButton');
|
|
75
|
+
const handleClick = () => {
|
|
76
|
+
const action = onClick();
|
|
77
|
+
if (action) {
|
|
78
|
+
onAction?.(action);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Button variant='ghost' classNames='p-2' onClick={handleClick} title={title}>
|
|
84
|
+
<Icon className={getSize(5)} />
|
|
85
|
+
</Button>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const ToolbarSeparator = () => <div className='grow' />;
|
|
90
|
+
|
|
91
|
+
const MarkdownHeading = () => {
|
|
92
|
+
const { onAction } = useToolbarContext('MarkdownFormatting');
|
|
93
|
+
return (
|
|
94
|
+
<Select.Root defaultValue='0' onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}>
|
|
95
|
+
<Select.TriggerButton classNames='w-[8rem]' />
|
|
96
|
+
<Select.Portal>
|
|
97
|
+
<Select.Content>
|
|
98
|
+
<Select.Viewport>
|
|
99
|
+
<Select.Option value='0'>Paragraph</Select.Option>
|
|
100
|
+
{[1, 2, 3, 4, 5, 6].map((level) => (
|
|
101
|
+
<Select.Option key={level} value={String(level)}>
|
|
102
|
+
Heading {level}
|
|
103
|
+
</Select.Option>
|
|
104
|
+
))}
|
|
105
|
+
</Select.Viewport>
|
|
106
|
+
</Select.Content>
|
|
107
|
+
</Select.Portal>
|
|
108
|
+
</Select.Root>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const MarkdownStyles = () => (
|
|
113
|
+
<div role='none'>
|
|
114
|
+
<ToolbarButton Icon={TextB} title='Bold' onClick={() => ({ type: 'bold' })} />
|
|
115
|
+
<ToolbarButton Icon={TextItalic} title='Italic' onClick={() => ({ type: 'italic' })} />
|
|
116
|
+
<ToolbarButton Icon={TextStrikethrough} title='Strike-through' onClick={() => ({ type: 'strikethrough' })} />
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const MarkdownLists = () => (
|
|
121
|
+
<div role='none'>
|
|
122
|
+
<ToolbarButton Icon={ListBullets} title='Bullet list' onClick={() => ({ type: 'list' })} />
|
|
123
|
+
<ToolbarButton Icon={ListNumbers} title='Numbered list' onClick={() => ({ type: 'list-ordered' })} />
|
|
124
|
+
<ToolbarButton Icon={ListChecks} title='Task list' onClick={() => ({ type: 'list-tasks' })} />
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const MarkdownBlocks = () => (
|
|
129
|
+
<div role='none'>
|
|
130
|
+
<ToolbarButton Icon={CaretRight} title='Block quite' onClick={() => ({ type: 'blockquote' })} />
|
|
131
|
+
<ToolbarButton Icon={Code} title='Code block' onClick={() => ({ type: 'codeblock' })} />
|
|
132
|
+
<ToolbarButton Icon={Table} title='Table' onClick={() => ({ type: 'table' })} />
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const MarkdownLinks = () => (
|
|
137
|
+
<div role='none'>
|
|
138
|
+
<ToolbarButton Icon={Link} title='Link' onClick={() => ({ type: 'link' })} />
|
|
139
|
+
<ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
|
|
140
|
+
<ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const MarkdownStandard = () => (
|
|
145
|
+
<>
|
|
146
|
+
<MarkdownHeading />
|
|
147
|
+
<MarkdownStyles />
|
|
148
|
+
<MarkdownLists />
|
|
149
|
+
<MarkdownBlocks />
|
|
150
|
+
<MarkdownLinks />
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const MarkdownExtended = () => (
|
|
155
|
+
<Toolbar.Button Icon={ChatText} title='Create comment' onClick={() => ({ type: 'comment' })} />
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
export const Toolbar = {
|
|
159
|
+
Root: ToolbarRoot,
|
|
160
|
+
Button: ToolbarButton,
|
|
161
|
+
Separator: ToolbarSeparator,
|
|
162
|
+
Markdown: MarkdownStandard,
|
|
163
|
+
Extended: MarkdownExtended,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export { useToolbarContext };
|