@dxos/react-ui-editor 0.4.5 → 0.4.6-main.20d1ec4
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 +156 -137
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -0
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts +3 -5
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +2 -7
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +2 -3
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +9 -2
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +1 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -1
- package/dist/types/src/hooks/useEditorView.d.ts +1 -0
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts +3 -3
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +12 -4
- 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/layout.d.ts +2 -3
- package/dist/types/src/styles/layout.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +26 -25
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +1 -1
- package/src/components/TextEditor/TextEditor.tsx +36 -18
- package/src/components/TextEditor/automerge.stories.tsx +15 -16
- package/src/components/TextEditor/hooks.stories.tsx +11 -8
- package/src/components/Toolbar/Toolbar.stories.tsx +9 -6
- package/src/components/Toolbar/Toolbar.tsx +22 -2
- package/src/extensions/automerge/automerge.ts +10 -15
- package/src/extensions/automerge/cursor.ts +2 -3
- package/src/extensions/automerge/defs.ts +15 -2
- package/src/extensions/markdown/decorate.ts +13 -49
- package/src/hooks/defs.ts +2 -0
- package/src/hooks/useEditorView.ts +2 -0
- package/src/hooks/useTextEditor.ts +8 -12
- package/src/hooks/useTextModel.ts +34 -5
- package/src/index.ts +1 -7
- package/src/styles/layout.ts +2 -5
- package/src/themes/default.ts +14 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-editor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6-main.20d1ec4",
|
|
4
4
|
"description": "Document editing experience within a DXOS shell.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@codemirror/autocomplete": "^6.12.0",
|
|
17
17
|
"@codemirror/commands": "^6.3.3",
|
|
18
|
+
"@codemirror/lang-javascript": "^6.2.2",
|
|
18
19
|
"@codemirror/lang-markdown": "^6.2.4",
|
|
19
20
|
"@codemirror/language": "^6.10.1",
|
|
20
21
|
"@codemirror/language-data": "^6.3.1",
|
|
@@ -29,27 +30,27 @@
|
|
|
29
30
|
"@lezer/markdown": "^1.2.0",
|
|
30
31
|
"@radix-ui/react-context": "^1.0.0",
|
|
31
32
|
"@replit/codemirror-vim": "^6.0.14",
|
|
32
|
-
"codemirror": "6.0.1",
|
|
33
|
+
"codemirror": "^6.0.1",
|
|
33
34
|
"lib0": "^0.2.65",
|
|
34
35
|
"lodash.defaultsdeep": "^4.6.1",
|
|
35
36
|
"lodash.get": "^4.4.2",
|
|
36
37
|
"lodash.pick": "^4.4.0",
|
|
37
38
|
"lodash.sortby": "^4.7.0",
|
|
38
39
|
"style-mod": "^4.1.0",
|
|
39
|
-
"@dxos/async": "0.4.
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/react-
|
|
49
|
-
"@dxos/react-
|
|
50
|
-
"@dxos/react-ui
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
40
|
+
"@dxos/async": "0.4.6-main.20d1ec4",
|
|
41
|
+
"@dxos/automerge": "0.4.6-main.20d1ec4",
|
|
42
|
+
"@dxos/context": "0.4.6-main.20d1ec4",
|
|
43
|
+
"@dxos/debug": "0.4.6-main.20d1ec4",
|
|
44
|
+
"@dxos/display-name": "0.4.6-main.20d1ec4",
|
|
45
|
+
"@dxos/invariant": "0.4.6-main.20d1ec4",
|
|
46
|
+
"@dxos/echo-schema": "0.4.6-main.20d1ec4",
|
|
47
|
+
"@dxos/log": "0.4.6-main.20d1ec4",
|
|
48
|
+
"@dxos/protocols": "0.4.6-main.20d1ec4",
|
|
49
|
+
"@dxos/react-async": "0.4.6-main.20d1ec4",
|
|
50
|
+
"@dxos/react-ui-theme": "0.4.6-main.20d1ec4",
|
|
51
|
+
"@dxos/react-ui": "0.4.6-main.20d1ec4",
|
|
52
|
+
"@dxos/util": "0.4.6-main.20d1ec4",
|
|
53
|
+
"@dxos/text-model": "0.4.6-main.20d1ec4"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@automerge/automerge-repo-network-broadcastchannel": "^1.1.1",
|
|
@@ -77,14 +78,14 @@
|
|
|
77
78
|
"vite-plugin-top-level-await": "^1.4.1",
|
|
78
79
|
"vite-plugin-wasm": "^3.3.0",
|
|
79
80
|
"vitest": "^1.2.2",
|
|
80
|
-
"@dxos/
|
|
81
|
-
"@dxos/
|
|
82
|
-
"@dxos/echo-
|
|
83
|
-
"@dxos/keyboard": "0.4.
|
|
84
|
-
"@dxos/
|
|
85
|
-
"@dxos/
|
|
86
|
-
"@dxos/storybook-utils": "0.4.
|
|
87
|
-
"@dxos/react-
|
|
81
|
+
"@dxos/echo-typegen": "0.4.6-main.20d1ec4",
|
|
82
|
+
"@dxos/config": "0.4.6-main.20d1ec4",
|
|
83
|
+
"@dxos/echo-signals": "0.4.6-main.20d1ec4",
|
|
84
|
+
"@dxos/keyboard": "0.4.6-main.20d1ec4",
|
|
85
|
+
"@dxos/random": "0.4.6-main.20d1ec4",
|
|
86
|
+
"@dxos/react-ui": "0.4.6-main.20d1ec4",
|
|
87
|
+
"@dxos/storybook-utils": "0.4.6-main.20d1ec4",
|
|
88
|
+
"@dxos/react-client": "0.4.6-main.20d1ec4"
|
|
88
89
|
},
|
|
89
90
|
"peerDependencies": {
|
|
90
91
|
"@phosphor-icons/react": "^2.0.5",
|
|
@@ -92,7 +93,7 @@
|
|
|
92
93
|
"@vitest/ui": "^1.2.2",
|
|
93
94
|
"react": "^18.2.0",
|
|
94
95
|
"react-dom": "^18.2.0",
|
|
95
|
-
"@dxos/react-client": "0.4.
|
|
96
|
+
"@dxos/react-client": "0.4.6-main.20d1ec4"
|
|
96
97
|
},
|
|
97
98
|
"publishConfig": {
|
|
98
99
|
"access": "public"
|
|
@@ -288,7 +288,7 @@ export const Empty = {
|
|
|
288
288
|
};
|
|
289
289
|
|
|
290
290
|
export const Scrolling = {
|
|
291
|
-
render: () => <Story text={str('# Large Document', '', large)}
|
|
291
|
+
render: () => <Story text={str('# Large Document', '', large)} />,
|
|
292
292
|
};
|
|
293
293
|
|
|
294
294
|
export const ScrollingWithImages = {
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
|
|
6
|
-
import { EditorView } from '@codemirror/view';
|
|
6
|
+
import { EditorView, scrollPastEnd } from '@codemirror/view';
|
|
7
7
|
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
8
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
9
9
|
import React, {
|
|
10
10
|
type ComponentProps,
|
|
11
|
-
forwardRef,
|
|
12
11
|
type KeyboardEventHandler,
|
|
12
|
+
forwardRef,
|
|
13
13
|
useCallback,
|
|
14
14
|
useEffect,
|
|
15
15
|
useImperativeHandle,
|
|
@@ -52,6 +52,8 @@ export type TextEditorProps = {
|
|
|
52
52
|
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
53
53
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
54
54
|
autoFocus?: boolean;
|
|
55
|
+
scrollPastEnd?: boolean;
|
|
56
|
+
moveToEndOfLine?: boolean;
|
|
55
57
|
lineWrapping?: boolean;
|
|
56
58
|
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
57
59
|
selection?: { anchor: number; head?: number };
|
|
@@ -68,7 +70,19 @@ export type TextEditorProps = {
|
|
|
68
70
|
// TODO(burdon): Replace with useTextEditor.
|
|
69
71
|
export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
70
72
|
(
|
|
71
|
-
{
|
|
73
|
+
{
|
|
74
|
+
model,
|
|
75
|
+
readonly,
|
|
76
|
+
autoFocus,
|
|
77
|
+
scrollTo = EditorView.scrollIntoView(0),
|
|
78
|
+
moveToEndOfLine,
|
|
79
|
+
selection,
|
|
80
|
+
theme,
|
|
81
|
+
slots = defaultSlots,
|
|
82
|
+
extensions = [],
|
|
83
|
+
scrollPastEnd: _scrollPastEnd,
|
|
84
|
+
debug,
|
|
85
|
+
},
|
|
72
86
|
forwardedRef,
|
|
73
87
|
) => {
|
|
74
88
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
@@ -83,12 +97,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
83
97
|
|
|
84
98
|
// Set focus.
|
|
85
99
|
useEffect(() => {
|
|
86
|
-
if (autoFocus
|
|
87
|
-
if (view?.state.selection.main?.from === 0) {
|
|
88
|
-
// Start at end of line.
|
|
89
|
-
const { to } = view.state.doc.lineAt(0);
|
|
90
|
-
view?.dispatch({ selection: { anchor: to } });
|
|
91
|
-
}
|
|
100
|
+
if (autoFocus) {
|
|
92
101
|
view?.focus();
|
|
93
102
|
}
|
|
94
103
|
}, [view, autoFocus]);
|
|
@@ -114,12 +123,15 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
114
123
|
}),
|
|
115
124
|
|
|
116
125
|
// Theme.
|
|
117
|
-
// TODO(burdon): Make configurable.
|
|
126
|
+
// TODO(burdon): Make base theme configurable.
|
|
118
127
|
EditorView.baseTheme(defaultTheme),
|
|
119
|
-
EditorView.theme(theme
|
|
128
|
+
theme && EditorView.theme(theme),
|
|
120
129
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
121
|
-
EditorView.editorAttributes.of({ class: slots.editor
|
|
122
|
-
EditorView.contentAttributes.of({ class: slots.content
|
|
130
|
+
slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
|
|
131
|
+
slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
|
|
132
|
+
|
|
133
|
+
// NOTE: Assumes default line height.
|
|
134
|
+
_scrollPastEnd && scrollPastEnd(),
|
|
123
135
|
|
|
124
136
|
// Focus.
|
|
125
137
|
EditorView.updateListener.of((update) => {
|
|
@@ -147,7 +159,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
147
159
|
// EditorView
|
|
148
160
|
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
149
161
|
//
|
|
150
|
-
const
|
|
162
|
+
const view = new EditorView({
|
|
151
163
|
state,
|
|
152
164
|
parent: rootRef.current,
|
|
153
165
|
scrollTo,
|
|
@@ -161,8 +173,12 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
161
173
|
},
|
|
162
174
|
});
|
|
163
175
|
|
|
164
|
-
view
|
|
165
|
-
|
|
176
|
+
setView(view);
|
|
177
|
+
|
|
178
|
+
if (moveToEndOfLine) {
|
|
179
|
+
const { to } = view.state.doc.lineAt(0);
|
|
180
|
+
view?.dispatch({ selection: { anchor: to } });
|
|
181
|
+
}
|
|
166
182
|
|
|
167
183
|
// Remove tabster attribute (rely on custom keymap).
|
|
168
184
|
if (state.facet(editorMode).noTabster) {
|
|
@@ -170,7 +186,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
170
186
|
}
|
|
171
187
|
|
|
172
188
|
return () => {
|
|
173
|
-
|
|
189
|
+
view?.destroy();
|
|
174
190
|
setView(null);
|
|
175
191
|
};
|
|
176
192
|
// TODO(wittjosiah): Does `rootRef` ever change? Only `.current` changes?
|
|
@@ -215,6 +231,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
215
231
|
() => [createBasicBundle({ themeMode, placeholder, lineWrapping }), ...(_extensions ?? [])],
|
|
216
232
|
[themeMode, placeholder, lineWrapping, _extensions],
|
|
217
233
|
);
|
|
234
|
+
|
|
218
235
|
return (
|
|
219
236
|
<BaseTextEditor
|
|
220
237
|
ref={forwardedRef}
|
|
@@ -236,6 +253,7 @@ export const MarkdownEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
236
253
|
() => [createMarkdownExtensions({ themeMode, placeholder }), ...(_extensions ?? [])],
|
|
237
254
|
[themeMode, placeholder, _extensions],
|
|
238
255
|
);
|
|
256
|
+
|
|
239
257
|
return (
|
|
240
258
|
<BaseTextEditor
|
|
241
259
|
ref={forwardedRef}
|
|
@@ -254,7 +272,7 @@ export const defaultSlots: TextEditorSlots = {
|
|
|
254
272
|
className: focusRing,
|
|
255
273
|
},
|
|
256
274
|
editor: {
|
|
257
|
-
className: '
|
|
275
|
+
className: 'bs-full',
|
|
258
276
|
},
|
|
259
277
|
};
|
|
260
278
|
|
|
@@ -7,11 +7,9 @@ import '@dxosTheme';
|
|
|
7
7
|
import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-network-broadcastchannel';
|
|
8
8
|
import '@preact/signals-react';
|
|
9
9
|
import { EditorView } from '@codemirror/view'; // Register react integration.
|
|
10
|
-
import get from 'lodash.get';
|
|
11
10
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
12
11
|
|
|
13
|
-
import {
|
|
14
|
-
import { Repo, type DocHandle } from '@dxos/automerge/automerge-repo';
|
|
12
|
+
import { Repo } from '@dxos/automerge/automerge-repo';
|
|
15
13
|
import { Filter } from '@dxos/echo-schema';
|
|
16
14
|
import { type PublicKey } from '@dxos/keys';
|
|
17
15
|
import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
|
|
@@ -20,7 +18,8 @@ import { ClientRepeater } from '@dxos/react-client/testing';
|
|
|
20
18
|
import { withTheme } from '@dxos/storybook-utils';
|
|
21
19
|
|
|
22
20
|
import { MarkdownEditor } from './TextEditor';
|
|
23
|
-
import {
|
|
21
|
+
import { automerge, awareness, createBasicBundle } from '../../extensions';
|
|
22
|
+
import { DocAccessor } from '../../extensions/automerge/defs';
|
|
24
23
|
import { useTextEditor, useTextModel } from '../../hooks';
|
|
25
24
|
import { defaultTheme } from '../../themes';
|
|
26
25
|
import translations from '../../translations';
|
|
@@ -32,24 +31,24 @@ type TestObject = {
|
|
|
32
31
|
};
|
|
33
32
|
|
|
34
33
|
type EditorProps = {
|
|
35
|
-
|
|
36
|
-
path?: Prop[];
|
|
34
|
+
source: DocAccessor;
|
|
37
35
|
};
|
|
38
36
|
|
|
39
|
-
const Editor = ({
|
|
37
|
+
const Editor = ({ source }: EditorProps) => {
|
|
40
38
|
const extensions = useMemo(
|
|
41
39
|
() => [
|
|
42
40
|
EditorView.baseTheme(defaultTheme),
|
|
43
41
|
EditorView.editorAttributes.of({ class: 'p-2 bg-white' }),
|
|
44
42
|
createBasicBundle({ placeholder: 'Type here...' }),
|
|
45
|
-
automerge(
|
|
43
|
+
automerge(source),
|
|
46
44
|
awareness(),
|
|
47
45
|
],
|
|
48
|
-
[
|
|
46
|
+
[source],
|
|
49
47
|
);
|
|
48
|
+
|
|
50
49
|
const { parentRef } = useTextEditor({
|
|
51
50
|
autoFocus: true,
|
|
52
|
-
doc:
|
|
51
|
+
doc: DocAccessor.getValue(source),
|
|
53
52
|
extensions,
|
|
54
53
|
});
|
|
55
54
|
|
|
@@ -57,8 +56,8 @@ const Editor = ({ handle, path = ['text'] }: EditorProps) => {
|
|
|
57
56
|
};
|
|
58
57
|
|
|
59
58
|
const Story = () => {
|
|
60
|
-
const [object1, setObject1] = useState<
|
|
61
|
-
const [object2, setObject2] = useState<
|
|
59
|
+
const [object1, setObject1] = useState<DocAccessor<TestObject>>();
|
|
60
|
+
const [object2, setObject2] = useState<DocAccessor<TestObject>>();
|
|
62
61
|
|
|
63
62
|
useEffect(() => {
|
|
64
63
|
queueMicrotask(async () => {
|
|
@@ -77,8 +76,8 @@ const Story = () => {
|
|
|
77
76
|
const object2 = repo2.find(object1.url);
|
|
78
77
|
await object2.whenReady();
|
|
79
78
|
|
|
80
|
-
setObject1(object1);
|
|
81
|
-
setObject2(object2);
|
|
79
|
+
setObject1({ handle: object1, path: ['text'] });
|
|
80
|
+
setObject2({ handle: object2, path: ['text'] });
|
|
82
81
|
});
|
|
83
82
|
}, []);
|
|
84
83
|
|
|
@@ -88,8 +87,8 @@ const Story = () => {
|
|
|
88
87
|
|
|
89
88
|
return (
|
|
90
89
|
<div role='none' className='grid grid-cols-2 bs-full is-full gap-2'>
|
|
91
|
-
<Editor
|
|
92
|
-
<Editor
|
|
90
|
+
<Editor source={object1} />
|
|
91
|
+
<Editor source={object2} />
|
|
93
92
|
</div>
|
|
94
93
|
);
|
|
95
94
|
};
|
|
@@ -6,7 +6,7 @@ import '@dxosTheme';
|
|
|
6
6
|
|
|
7
7
|
import React, { useMemo, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import { Toolbar as NaturalToolbar, Select, useThemeContext } from '@dxos/react-ui';
|
|
9
|
+
import { Toolbar as NaturalToolbar, Select, useThemeContext, Tooltip } from '@dxos/react-ui';
|
|
10
10
|
import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
11
|
import { withTheme } from '@dxos/storybook-utils';
|
|
12
12
|
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
useFormattingState,
|
|
23
23
|
} from '../../extensions';
|
|
24
24
|
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
25
|
-
import { editorFillLayoutEditor, editorWithToolbarLayout } from '../../styles';
|
|
26
25
|
import { markdownTheme } from '../../themes';
|
|
27
26
|
import translations from '../../translations';
|
|
28
27
|
import { Toolbar } from '../Toolbar';
|
|
@@ -52,7 +51,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
52
51
|
themeMode,
|
|
53
52
|
theme: markdownTheme,
|
|
54
53
|
slots: {
|
|
55
|
-
|
|
54
|
+
editor: { className: 'p-2' },
|
|
56
55
|
},
|
|
57
56
|
}),
|
|
58
57
|
// TODO(burdon): Move lineWrapping.
|
|
@@ -70,16 +69,16 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
70
69
|
|
|
71
70
|
const handleAction = useActionHandler(view);
|
|
72
71
|
|
|
73
|
-
// TODO(marijn) This doesn't update the state on view changes.
|
|
72
|
+
// TODO(marijn): This doesn't update the state on view changes.
|
|
74
73
|
// Also not sure if view is even guaranteed to exist at this point.
|
|
75
74
|
return (
|
|
76
|
-
<div role='none' className={mx('fixed inset-0'
|
|
75
|
+
<div role='none' className={mx('fixed inset-0 flex flex-col')}>
|
|
77
76
|
<Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
|
|
78
77
|
<Toolbar.Markdown />
|
|
79
78
|
<EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
|
|
80
79
|
</Toolbar.Root>
|
|
81
|
-
<div role='none' className='overflow-
|
|
82
|
-
<div role='textbox' className={mx(textBlockWidth, attentionSurface
|
|
80
|
+
<div role='none' className='grow overflow-hidden'>
|
|
81
|
+
<div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
|
|
83
82
|
</div>
|
|
84
83
|
</div>
|
|
85
84
|
);
|
|
@@ -119,7 +118,11 @@ export default {
|
|
|
119
118
|
title: 'react-ui-editor/useTextEditor',
|
|
120
119
|
component: TextEditor,
|
|
121
120
|
decorators: [withTheme],
|
|
122
|
-
render: (args: StoryProps) =>
|
|
121
|
+
render: (args: StoryProps) => (
|
|
122
|
+
<Tooltip.Provider>
|
|
123
|
+
<Story {...args} />
|
|
124
|
+
</Tooltip.Provider>
|
|
125
|
+
),
|
|
123
126
|
parameters: { translations, layout: 'fullscreen' },
|
|
124
127
|
};
|
|
125
128
|
|
|
@@ -9,6 +9,7 @@ import React, { type FC, useMemo, useState } from 'react';
|
|
|
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';
|
|
12
13
|
import { mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
13
14
|
import { withTheme } from '@dxos/storybook-utils';
|
|
14
15
|
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
useFormattingState,
|
|
24
25
|
} from '../../extensions';
|
|
25
26
|
import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
|
|
26
|
-
import { editorFillLayoutEditor, editorFillLayoutRoot, editorWithToolbarLayout } from '../../styles';
|
|
27
27
|
import translations from '../../translations';
|
|
28
28
|
import { MarkdownEditor } from '../TextEditor';
|
|
29
29
|
|
|
@@ -61,7 +61,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
return (
|
|
64
|
-
<div role='none' className={mx('fixed inset-0'
|
|
64
|
+
<div role='none' className={mx('fixed inset-0 flex flex-col')}>
|
|
65
65
|
<Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
|
|
66
66
|
<Toolbar.Markdown />
|
|
67
67
|
<Toolbar.Separator />
|
|
@@ -72,9 +72,8 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
72
72
|
model={model}
|
|
73
73
|
extensions={extensions}
|
|
74
74
|
slots={{
|
|
75
|
-
root: { className: mx(textBlockWidth
|
|
76
|
-
editor: { className:
|
|
77
|
-
content: { className: '!pli-2' },
|
|
75
|
+
root: { className: mx(textBlockWidth) },
|
|
76
|
+
editor: { className: 'p-2' },
|
|
78
77
|
}}
|
|
79
78
|
/>
|
|
80
79
|
</div>
|
|
@@ -84,7 +83,11 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
84
83
|
export default {
|
|
85
84
|
title: 'react-ui-editor/Toolbar',
|
|
86
85
|
component: Toolbar,
|
|
87
|
-
render: (args: any) =>
|
|
86
|
+
render: (args: any) => (
|
|
87
|
+
<Tooltip.Provider>
|
|
88
|
+
<Story {...args} />
|
|
89
|
+
</Tooltip.Provider>
|
|
90
|
+
),
|
|
88
91
|
decorators: [withTheme],
|
|
89
92
|
parameters: { translations, layout: 'fullscreen' },
|
|
90
93
|
};
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
Quotes,
|
|
26
26
|
} from '@phosphor-icons/react';
|
|
27
27
|
import { createContext } from '@radix-ui/react-context';
|
|
28
|
-
import React, { type PropsWithChildren } from 'react';
|
|
28
|
+
import React, { type PropsWithChildren, useRef, useState } from 'react';
|
|
29
29
|
|
|
30
30
|
import {
|
|
31
31
|
DensityProvider,
|
|
@@ -141,11 +141,31 @@ const MarkdownHeading = () => {
|
|
|
141
141
|
const header = blockType && /heading(\d)/.exec(blockType);
|
|
142
142
|
const value = header ? header[1] : blockType === 'paragraph' || !blockType ? '0' : undefined;
|
|
143
143
|
const HeadingIcon = HeadingIcons[value ?? '0'];
|
|
144
|
+
const suppressNextTooltip = useRef<boolean>(false);
|
|
145
|
+
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
146
|
+
const [selectOpen, setSelectOpen] = useState<boolean>(false);
|
|
144
147
|
return (
|
|
145
|
-
<Tooltip.Root
|
|
148
|
+
<Tooltip.Root
|
|
149
|
+
open={tooltipOpen}
|
|
150
|
+
onOpenChange={(nextOpen) => {
|
|
151
|
+
if (nextOpen && suppressNextTooltip.current) {
|
|
152
|
+
suppressNextTooltip.current = false;
|
|
153
|
+
return setTooltipOpen(false);
|
|
154
|
+
} else {
|
|
155
|
+
return setTooltipOpen(nextOpen);
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
146
159
|
<Select.Root
|
|
147
160
|
disabled={value === null}
|
|
148
161
|
value={value ?? '0'}
|
|
162
|
+
open={selectOpen}
|
|
163
|
+
onOpenChange={(nextOpen: boolean) => {
|
|
164
|
+
if (!nextOpen) {
|
|
165
|
+
suppressNextTooltip.current = true;
|
|
166
|
+
}
|
|
167
|
+
return setSelectOpen(nextOpen);
|
|
168
|
+
}}
|
|
149
169
|
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
150
170
|
>
|
|
151
171
|
<Tooltip.Trigger asChild>
|
|
@@ -7,29 +7,24 @@
|
|
|
7
7
|
import { StateField, type Extension } from '@codemirror/state';
|
|
8
8
|
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
9
9
|
|
|
10
|
-
import { next as A
|
|
10
|
+
import { next as A } from '@dxos/automerge/automerge';
|
|
11
11
|
|
|
12
12
|
import { cursorConverter } from './cursor';
|
|
13
|
-
import { updateHeadsEffect,
|
|
13
|
+
import { updateHeadsEffect, isReconcile, type State, type DocAccessor } from './defs';
|
|
14
14
|
import { Syncer } from './sync';
|
|
15
15
|
import { Cursor } from '../cursor';
|
|
16
16
|
|
|
17
|
-
export
|
|
18
|
-
handle: IDocHandle;
|
|
19
|
-
path: Prop[];
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
17
|
+
export const automerge = (accessor: DocAccessor): Extension => {
|
|
23
18
|
const syncState = StateField.define<State>({
|
|
24
19
|
create: () => ({
|
|
25
|
-
path: path.slice(),
|
|
26
|
-
lastHeads: A.getHeads(handle.docSync()!),
|
|
20
|
+
path: accessor.path.slice(),
|
|
21
|
+
lastHeads: A.getHeads(accessor.handle.docSync()!),
|
|
27
22
|
unreconciledTransactions: [],
|
|
28
23
|
}),
|
|
29
24
|
|
|
30
25
|
update: (value, tr) => {
|
|
31
26
|
const result: State = {
|
|
32
|
-
path: path.slice(),
|
|
27
|
+
path: accessor.path.slice(),
|
|
33
28
|
lastHeads: value.lastHeads,
|
|
34
29
|
unreconciledTransactions: value.unreconciledTransactions.slice(),
|
|
35
30
|
};
|
|
@@ -54,21 +49,21 @@ export const automerge = ({ handle, path }: AutomergeOptions): Extension => {
|
|
|
54
49
|
},
|
|
55
50
|
});
|
|
56
51
|
|
|
57
|
-
const syncer = new Syncer(handle, syncState);
|
|
52
|
+
const syncer = new Syncer(accessor.handle, syncState);
|
|
58
53
|
|
|
59
54
|
return [
|
|
60
|
-
Cursor.converter.of(cursorConverter(
|
|
55
|
+
Cursor.converter.of(cursorConverter(accessor)),
|
|
61
56
|
syncState,
|
|
62
57
|
|
|
63
58
|
// Reconcile external updates.
|
|
64
59
|
ViewPlugin.fromClass(
|
|
65
60
|
class {
|
|
66
61
|
constructor(private readonly _view: EditorView) {
|
|
67
|
-
handle.addListener('change', this._handleChange.bind(this));
|
|
62
|
+
accessor.handle.addListener('change', this._handleChange.bind(this));
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
destroy() {
|
|
71
|
-
handle.removeListener('change', this._handleChange.bind(this));
|
|
66
|
+
accessor.handle.removeListener('change', this._handleChange.bind(this));
|
|
72
67
|
}
|
|
73
68
|
|
|
74
69
|
_handleChange() {
|
|
@@ -4,14 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import get from 'lodash.get';
|
|
6
6
|
|
|
7
|
-
import type { Prop } from '@dxos/automerge/automerge';
|
|
8
7
|
import { next as A } from '@dxos/automerge/automerge';
|
|
9
8
|
import { log } from '@dxos/log';
|
|
10
9
|
|
|
11
|
-
import { type
|
|
10
|
+
import { type DocAccessor } from './defs';
|
|
12
11
|
import { type CursorConverter } from '../cursor';
|
|
13
12
|
|
|
14
|
-
export const cursorConverter = (handle
|
|
13
|
+
export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter => ({
|
|
15
14
|
// TODO(burdon): Handle assoc to associate with a previous character.
|
|
16
15
|
toCursor: (pos) => {
|
|
17
16
|
const doc = handle.docSync();
|
|
@@ -5,6 +5,7 @@
|
|
|
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
10
|
import { type ChangeFn, type ChangeOptions, type Doc, type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
10
11
|
|
|
@@ -31,10 +32,22 @@ export const isReconcile = (tr: Transaction): boolean => {
|
|
|
31
32
|
return !!tr.annotation(reconcileAnnotation);
|
|
32
33
|
};
|
|
33
34
|
|
|
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> {
|
|
35
48
|
docSync(): Doc<T> | undefined;
|
|
36
49
|
change(callback: ChangeFn<T>, options?: ChangeOptions<T>): void;
|
|
37
50
|
changeAt(heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): string[] | undefined;
|
|
38
51
|
addListener(event: 'change', listener: () => void): void;
|
|
39
52
|
removeListener(event: 'change', listener: () => void): void;
|
|
40
|
-
}
|
|
53
|
+
}
|