@dxos/react-ui-editor 0.4.5 → 0.4.6-main.268e405
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 +68 -45
- 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/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/hooks/useTextEditor.d.ts +1 -1
- package/dist/types/src/hooks/useTextEditor.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/hooks.stories.tsx +11 -8
- package/src/components/Toolbar/Toolbar.stories.tsx +9 -6
- package/src/components/Toolbar/Toolbar.tsx +22 -2
- package/src/hooks/useTextEditor.ts +3 -3
- package/src/index.ts +1 -7
- package/src/styles/layout.ts +2 -5
- package/src/themes/default.ts +7 -3
|
@@ -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
|
|
|
@@ -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>
|
|
@@ -16,7 +16,7 @@ import { log } from '@dxos/log';
|
|
|
16
16
|
import { type ThemeMode } from '@dxos/react-ui';
|
|
17
17
|
import { isNotFalsy } from '@dxos/util';
|
|
18
18
|
|
|
19
|
-
import { type ThemeStyles } from '
|
|
19
|
+
import { type ThemeStyles } from '../styles';
|
|
20
20
|
import { defaultTheme } from '../themes';
|
|
21
21
|
import { logChanges } from '../util';
|
|
22
22
|
|
|
@@ -26,6 +26,7 @@ export type UseTextEditorOptions = {
|
|
|
26
26
|
debug?: boolean;
|
|
27
27
|
} & EditorStateConfig;
|
|
28
28
|
|
|
29
|
+
// TODO(burdon): Return tuple?
|
|
29
30
|
export type UseTextEditor = {
|
|
30
31
|
parentRef: RefObject<HTMLDivElement>;
|
|
31
32
|
view?: EditorView;
|
|
@@ -87,8 +88,7 @@ export const useTextEditor = ({
|
|
|
87
88
|
return () => {
|
|
88
89
|
view?.destroy();
|
|
89
90
|
};
|
|
90
|
-
|
|
91
|
-
}, [parentRef, extensions]);
|
|
91
|
+
}, [doc, extensions]);
|
|
92
92
|
|
|
93
93
|
useEffect(() => {
|
|
94
94
|
if (view) {
|
package/src/index.ts
CHANGED
|
@@ -12,13 +12,7 @@ export { Doc, YText, YXmlFragment } from '@dxos/text-model';
|
|
|
12
12
|
export * from './components';
|
|
13
13
|
export * from './extensions';
|
|
14
14
|
export * from './hooks';
|
|
15
|
-
export {
|
|
16
|
-
getToken,
|
|
17
|
-
editorWithToolbarLayout,
|
|
18
|
-
editorFillLayoutEditor,
|
|
19
|
-
editorFillLayoutRoot,
|
|
20
|
-
editorHalfViewportOverscrollContent,
|
|
21
|
-
} from './styles';
|
|
15
|
+
export { getToken, editorWithToolbarLayout, editorFillLayoutRoot, editorFillLayoutEditor } from './styles';
|
|
22
16
|
export * from './themes';
|
|
23
17
|
export * from './util';
|
|
24
18
|
export { translations };
|
package/src/styles/layout.ts
CHANGED
|
@@ -5,8 +5,5 @@
|
|
|
5
5
|
export const editorWithToolbarLayout =
|
|
6
6
|
'grid grid-cols-1 grid-rows-[min-content_1fr] data-[toolbar=disabled]:grid-rows-[1fr] justify-center content-start overflow-hidden';
|
|
7
7
|
|
|
8
|
-
export const editorFillLayoutRoot = 'bs-full
|
|
9
|
-
export const editorFillLayoutEditor =
|
|
10
|
-
'bs-full overflow-hidden [&>.cm-scroller]:bs-full [&>.cm-scroller]:overflow-y-auto';
|
|
11
|
-
|
|
12
|
-
export const editorHalfViewportOverscrollContent = 'after:block after:min-bs-[50dvh] after:pointer-events-none';
|
|
8
|
+
export const editorFillLayoutRoot = 'bs-full relative';
|
|
9
|
+
export const editorFillLayoutEditor = '!absolute inset-0';
|
package/src/themes/default.ts
CHANGED
|
@@ -50,14 +50,18 @@ export const defaultTheme: ThemeStyles = {
|
|
|
50
50
|
outline: 'none',
|
|
51
51
|
},
|
|
52
52
|
|
|
53
|
+
// NOTE: See https://codemirror.net/docs/guide (DOM Structure).
|
|
53
54
|
'.cm-scroller': {
|
|
54
|
-
//
|
|
55
|
+
// TODO(burdon): Reconcile with docs: https://codemirror.net/docs/guide
|
|
56
|
+
// Inside of that is the scroller element. If the editor has its own scrollbar, this one should be styled with overflow: auto. But it doesn't have to—the editor also supports growing to accomodate its content, or growing up to a certain max-height and then scrolling.
|
|
57
|
+
overflowY: 'auto',
|
|
55
58
|
fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
|
|
56
|
-
lineHeight: 1.
|
|
59
|
+
lineHeight: 1.5,
|
|
57
60
|
},
|
|
58
61
|
|
|
59
62
|
'.cm-content': {
|
|
60
|
-
|
|
63
|
+
// TODO(burdon): Is it possible to remove the padding value from CM's default theme?
|
|
64
|
+
// padding: 'unset',
|
|
61
65
|
// NOTE: Base font size (otherwise defined by HTML tag, which might be different for storybook).
|
|
62
66
|
fontSize: '16px',
|
|
63
67
|
},
|