@dxos/react-ui-editor 0.4.6-main.3500f01 → 0.4.6-main.6d26f8f
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 +45 -68
- 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 +0 -2
- 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 +3 -2
- 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 +25 -26
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +1 -1
- package/src/components/TextEditor/TextEditor.tsx +18 -36
- package/src/components/TextEditor/hooks.stories.tsx +8 -11
- package/src/components/Toolbar/Toolbar.stories.tsx +6 -9
- package/src/components/Toolbar/Toolbar.tsx +2 -22
- package/src/hooks/useTextEditor.ts +3 -3
- package/src/index.ts +7 -1
- package/src/styles/layout.ts +5 -2
- package/src/themes/default.ts +3 -7
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
|
|
6
|
-
import { EditorView
|
|
6
|
+
import { EditorView } 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
|
-
type KeyboardEventHandler,
|
|
12
11
|
forwardRef,
|
|
12
|
+
type KeyboardEventHandler,
|
|
13
13
|
useCallback,
|
|
14
14
|
useEffect,
|
|
15
15
|
useImperativeHandle,
|
|
@@ -52,8 +52,6 @@ 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;
|
|
57
55
|
lineWrapping?: boolean;
|
|
58
56
|
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
59
57
|
selection?: { anchor: number; head?: number };
|
|
@@ -70,19 +68,7 @@ export type TextEditorProps = {
|
|
|
70
68
|
// TODO(burdon): Replace with useTextEditor.
|
|
71
69
|
export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
72
70
|
(
|
|
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
|
-
},
|
|
71
|
+
{ model, readonly, autoFocus, scrollTo, selection, theme, slots = defaultSlots, extensions = [], debug },
|
|
86
72
|
forwardedRef,
|
|
87
73
|
) => {
|
|
88
74
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
@@ -97,7 +83,12 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
97
83
|
|
|
98
84
|
// Set focus.
|
|
99
85
|
useEffect(() => {
|
|
100
|
-
if (autoFocus) {
|
|
86
|
+
if (autoFocus && !view?.hasFocus) {
|
|
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
|
+
}
|
|
101
92
|
view?.focus();
|
|
102
93
|
}
|
|
103
94
|
}, [view, autoFocus]);
|
|
@@ -123,15 +114,12 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
123
114
|
}),
|
|
124
115
|
|
|
125
116
|
// Theme.
|
|
126
|
-
// TODO(burdon): Make
|
|
117
|
+
// TODO(burdon): Make configurable.
|
|
127
118
|
EditorView.baseTheme(defaultTheme),
|
|
128
|
-
|
|
119
|
+
EditorView.theme(theme ?? {}),
|
|
129
120
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// NOTE: Assumes default line height.
|
|
134
|
-
_scrollPastEnd && scrollPastEnd(),
|
|
121
|
+
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
122
|
+
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
135
123
|
|
|
136
124
|
// Focus.
|
|
137
125
|
EditorView.updateListener.of((update) => {
|
|
@@ -159,7 +147,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
159
147
|
// EditorView
|
|
160
148
|
// https://codemirror.net/docs/ref/#view.EditorViewConfig
|
|
161
149
|
//
|
|
162
|
-
const
|
|
150
|
+
const newView = new EditorView({
|
|
163
151
|
state,
|
|
164
152
|
parent: rootRef.current,
|
|
165
153
|
scrollTo,
|
|
@@ -173,12 +161,8 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
173
161
|
},
|
|
174
162
|
});
|
|
175
163
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (moveToEndOfLine) {
|
|
179
|
-
const { to } = view.state.doc.lineAt(0);
|
|
180
|
-
view?.dispatch({ selection: { anchor: to } });
|
|
181
|
-
}
|
|
164
|
+
view?.destroy();
|
|
165
|
+
setView(newView);
|
|
182
166
|
|
|
183
167
|
// Remove tabster attribute (rely on custom keymap).
|
|
184
168
|
if (state.facet(editorMode).noTabster) {
|
|
@@ -186,7 +170,7 @@ export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
186
170
|
}
|
|
187
171
|
|
|
188
172
|
return () => {
|
|
189
|
-
|
|
173
|
+
newView?.destroy();
|
|
190
174
|
setView(null);
|
|
191
175
|
};
|
|
192
176
|
// TODO(wittjosiah): Does `rootRef` ever change? Only `.current` changes?
|
|
@@ -231,7 +215,6 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
231
215
|
() => [createBasicBundle({ themeMode, placeholder, lineWrapping }), ...(_extensions ?? [])],
|
|
232
216
|
[themeMode, placeholder, lineWrapping, _extensions],
|
|
233
217
|
);
|
|
234
|
-
|
|
235
218
|
return (
|
|
236
219
|
<BaseTextEditor
|
|
237
220
|
ref={forwardedRef}
|
|
@@ -253,7 +236,6 @@ export const MarkdownEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
253
236
|
() => [createMarkdownExtensions({ themeMode, placeholder }), ...(_extensions ?? [])],
|
|
254
237
|
[themeMode, placeholder, _extensions],
|
|
255
238
|
);
|
|
256
|
-
|
|
257
239
|
return (
|
|
258
240
|
<BaseTextEditor
|
|
259
241
|
ref={forwardedRef}
|
|
@@ -272,7 +254,7 @@ export const defaultSlots: TextEditorSlots = {
|
|
|
272
254
|
className: focusRing,
|
|
273
255
|
},
|
|
274
256
|
editor: {
|
|
275
|
-
className: 'bs-full',
|
|
257
|
+
className: 'min-bs-full',
|
|
276
258
|
},
|
|
277
259
|
};
|
|
278
260
|
|
|
@@ -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
|
|
9
|
+
import { Toolbar as NaturalToolbar, Select, useThemeContext } 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,6 +22,7 @@ import {
|
|
|
22
22
|
useFormattingState,
|
|
23
23
|
} from '../../extensions';
|
|
24
24
|
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
25
|
+
import { editorFillLayoutEditor, editorWithToolbarLayout } from '../../styles';
|
|
25
26
|
import { markdownTheme } from '../../themes';
|
|
26
27
|
import translations from '../../translations';
|
|
27
28
|
import { Toolbar } from '../Toolbar';
|
|
@@ -51,7 +52,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
51
52
|
themeMode,
|
|
52
53
|
theme: markdownTheme,
|
|
53
54
|
slots: {
|
|
54
|
-
|
|
55
|
+
content: { className: '!p-4' },
|
|
55
56
|
},
|
|
56
57
|
}),
|
|
57
58
|
// TODO(burdon): Move lineWrapping.
|
|
@@ -69,16 +70,16 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
69
70
|
|
|
70
71
|
const handleAction = useActionHandler(view);
|
|
71
72
|
|
|
72
|
-
// TODO(marijn)
|
|
73
|
+
// TODO(marijn) This doesn't update the state on view changes.
|
|
73
74
|
// Also not sure if view is even guaranteed to exist at this point.
|
|
74
75
|
return (
|
|
75
|
-
<div role='none' className={mx('fixed inset-0
|
|
76
|
+
<div role='none' className={mx('fixed inset-0', editorWithToolbarLayout)}>
|
|
76
77
|
<Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
|
|
77
78
|
<Toolbar.Markdown />
|
|
78
79
|
<EditorModeToolbar editorMode={editorMode} setEditorMode={setEditorMode} />
|
|
79
80
|
</Toolbar.Root>
|
|
80
|
-
<div role='none' className='
|
|
81
|
-
<div role='textbox' className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
|
|
81
|
+
<div role='none' className='overflow-y-auto'>
|
|
82
|
+
<div role='textbox' className={mx(textBlockWidth, attentionSurface, editorFillLayoutEditor)} ref={parentRef} />
|
|
82
83
|
</div>
|
|
83
84
|
</div>
|
|
84
85
|
);
|
|
@@ -118,11 +119,7 @@ export default {
|
|
|
118
119
|
title: 'react-ui-editor/useTextEditor',
|
|
119
120
|
component: TextEditor,
|
|
120
121
|
decorators: [withTheme],
|
|
121
|
-
render: (args: StoryProps) =>
|
|
122
|
-
<Tooltip.Provider>
|
|
123
|
-
<Story {...args} />
|
|
124
|
-
</Tooltip.Provider>
|
|
125
|
-
),
|
|
122
|
+
render: (args: StoryProps) => <Story {...args} />,
|
|
126
123
|
parameters: { translations, layout: 'fullscreen' },
|
|
127
124
|
};
|
|
128
125
|
|
|
@@ -9,7 +9,6 @@ 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';
|
|
13
12
|
import { mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
14
13
|
import { withTheme } from '@dxos/storybook-utils';
|
|
15
14
|
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
useFormattingState,
|
|
25
24
|
} from '../../extensions';
|
|
26
25
|
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', editorWithToolbarLayout)}>
|
|
65
65
|
<Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
|
|
66
66
|
<Toolbar.Markdown />
|
|
67
67
|
<Toolbar.Separator />
|
|
@@ -72,8 +72,9 @@ 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:
|
|
75
|
+
root: { className: mx(textBlockWidth, editorFillLayoutRoot) },
|
|
76
|
+
editor: { className: editorFillLayoutEditor },
|
|
77
|
+
content: { className: '!pli-2' },
|
|
77
78
|
}}
|
|
78
79
|
/>
|
|
79
80
|
</div>
|
|
@@ -83,11 +84,7 @@ const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) =
|
|
|
83
84
|
export default {
|
|
84
85
|
title: 'react-ui-editor/Toolbar',
|
|
85
86
|
component: Toolbar,
|
|
86
|
-
render: (args: any) =>
|
|
87
|
-
<Tooltip.Provider>
|
|
88
|
-
<Story {...args} />
|
|
89
|
-
</Tooltip.Provider>
|
|
90
|
-
),
|
|
87
|
+
render: (args: any) => <Story {...args} />,
|
|
91
88
|
decorators: [withTheme],
|
|
92
89
|
parameters: { translations, layout: 'fullscreen' },
|
|
93
90
|
};
|
|
@@ -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
|
|
28
|
+
import React, { type PropsWithChildren } from 'react';
|
|
29
29
|
|
|
30
30
|
import {
|
|
31
31
|
DensityProvider,
|
|
@@ -141,31 +141,11 @@ 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);
|
|
147
144
|
return (
|
|
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
|
-
>
|
|
145
|
+
<Tooltip.Root>
|
|
159
146
|
<Select.Root
|
|
160
147
|
disabled={value === null}
|
|
161
148
|
value={value ?? '0'}
|
|
162
|
-
open={selectOpen}
|
|
163
|
-
onOpenChange={(nextOpen: boolean) => {
|
|
164
|
-
if (!nextOpen) {
|
|
165
|
-
suppressNextTooltip.current = true;
|
|
166
|
-
}
|
|
167
|
-
return setSelectOpen(nextOpen);
|
|
168
|
-
}}
|
|
169
149
|
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
170
150
|
>
|
|
171
151
|
<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,7 +26,6 @@ export type UseTextEditorOptions = {
|
|
|
26
26
|
debug?: boolean;
|
|
27
27
|
} & EditorStateConfig;
|
|
28
28
|
|
|
29
|
-
// TODO(burdon): Return tuple?
|
|
30
29
|
export type UseTextEditor = {
|
|
31
30
|
parentRef: RefObject<HTMLDivElement>;
|
|
32
31
|
view?: EditorView;
|
|
@@ -88,7 +87,8 @@ export const useTextEditor = ({
|
|
|
88
87
|
return () => {
|
|
89
88
|
view?.destroy();
|
|
90
89
|
};
|
|
91
|
-
|
|
90
|
+
// TODO(wittjosiah): Does `parentRef` ever change? Only `.current` changes?
|
|
91
|
+
}, [parentRef, extensions]);
|
|
92
92
|
|
|
93
93
|
useEffect(() => {
|
|
94
94
|
if (view) {
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,13 @@ 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 {
|
|
15
|
+
export {
|
|
16
|
+
getToken,
|
|
17
|
+
editorWithToolbarLayout,
|
|
18
|
+
editorFillLayoutEditor,
|
|
19
|
+
editorFillLayoutRoot,
|
|
20
|
+
editorHalfViewportOverscrollContent,
|
|
21
|
+
} from './styles';
|
|
16
22
|
export * from './themes';
|
|
17
23
|
export * from './util';
|
|
18
24
|
export { translations };
|
package/src/styles/layout.ts
CHANGED
|
@@ -5,5 +5,8 @@
|
|
|
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 =
|
|
8
|
+
export const editorFillLayoutRoot = 'bs-full overflow-hidden grid';
|
|
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';
|
package/src/themes/default.ts
CHANGED
|
@@ -50,18 +50,14 @@ export const defaultTheme: ThemeStyles = {
|
|
|
50
50
|
outline: 'none',
|
|
51
51
|
},
|
|
52
52
|
|
|
53
|
-
// NOTE: See https://codemirror.net/docs/guide (DOM Structure).
|
|
54
53
|
'.cm-scroller': {
|
|
55
|
-
//
|
|
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',
|
|
54
|
+
// overflow: 'visible',
|
|
58
55
|
fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
|
|
59
|
-
lineHeight: 1.
|
|
56
|
+
lineHeight: 1.4,
|
|
60
57
|
},
|
|
61
58
|
|
|
62
59
|
'.cm-content': {
|
|
63
|
-
|
|
64
|
-
// padding: 'unset',
|
|
60
|
+
padding: 0,
|
|
65
61
|
// NOTE: Base font size (otherwise defined by HTML tag, which might be different for storybook).
|
|
66
62
|
fontSize: '16px',
|
|
67
63
|
},
|