@dxos/react-ui-editor 0.6.5 → 0.6.6-main.e1a6e1f
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 +137 -41
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +4 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +5 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +7 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts +2 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +4 -0
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/debug.d.ts +3 -0
- package/dist/types/src/extensions/debug.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +1 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts +3 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +7 -4
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts +4 -0
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +4 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +27 -27
- package/src/components/TextEditor/TextEditor.stories.tsx +2 -2
- package/src/components/TextEditor/TextEditor.tsx +3 -3
- package/src/components/Toolbar/Toolbar.stories.tsx +17 -7
- package/src/components/Toolbar/Toolbar.tsx +93 -3
- package/src/extensions/autocomplete.ts +3 -3
- package/src/extensions/comments.ts +1 -0
- package/src/extensions/debug.ts +15 -0
- package/src/extensions/index.ts +1 -0
- package/src/extensions/markdown/action.ts +1 -0
- package/src/extensions/modes.ts +9 -6
- package/src/hooks/useTextEditor.stories.tsx +13 -14
- package/src/translations.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-editor",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6-main.e1a6e1f",
|
|
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",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"src"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@codemirror/autocomplete": "^6.
|
|
24
|
+
"@codemirror/autocomplete": "^6.18.0",
|
|
25
25
|
"@codemirror/commands": "^6.6.0",
|
|
26
26
|
"@codemirror/lang-javascript": "^6.2.2",
|
|
27
27
|
"@codemirror/lang-markdown": "^6.2.5",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@fluentui/react-tabster": "^9.19.0",
|
|
36
36
|
"@lezer/common": "^1.2.1",
|
|
37
37
|
"@lezer/highlight": "^1.2.0",
|
|
38
|
-
"@lezer/markdown": "^1.
|
|
38
|
+
"@lezer/markdown": "^1.3.0",
|
|
39
39
|
"@radix-ui/react-context": "^1.0.0",
|
|
40
40
|
"@replit/codemirror-vim": "^6.0.14",
|
|
41
41
|
"@replit/codemirror-vscode-keymap": "^6.0.2",
|
|
@@ -47,19 +47,19 @@
|
|
|
47
47
|
"lodash.sortby": "^4.7.0",
|
|
48
48
|
"react-dropzone": "^14.2.3",
|
|
49
49
|
"style-mod": "^4.1.0",
|
|
50
|
-
"@dxos/async": "0.6.
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/
|
|
56
|
-
"@dxos/log": "0.6.
|
|
57
|
-
"@dxos/
|
|
58
|
-
"@dxos/protocols": "0.6.
|
|
59
|
-
"@dxos/react-
|
|
60
|
-
"@dxos/react-ui": "0.6.
|
|
61
|
-
"@dxos/react-
|
|
62
|
-
"@dxos/util": "0.6.
|
|
50
|
+
"@dxos/async": "0.6.6-main.e1a6e1f",
|
|
51
|
+
"@dxos/automerge": "0.6.6-main.e1a6e1f",
|
|
52
|
+
"@dxos/context": "0.6.6-main.e1a6e1f",
|
|
53
|
+
"@dxos/debug": "0.6.6-main.e1a6e1f",
|
|
54
|
+
"@dxos/invariant": "0.6.6-main.e1a6e1f",
|
|
55
|
+
"@dxos/echo-schema": "0.6.6-main.e1a6e1f",
|
|
56
|
+
"@dxos/log": "0.6.6-main.e1a6e1f",
|
|
57
|
+
"@dxos/display-name": "0.6.6-main.e1a6e1f",
|
|
58
|
+
"@dxos/protocols": "0.6.6-main.e1a6e1f",
|
|
59
|
+
"@dxos/react-async": "0.6.6-main.e1a6e1f",
|
|
60
|
+
"@dxos/react-ui": "0.6.6-main.e1a6e1f",
|
|
61
|
+
"@dxos/react-ui-theme": "0.6.6-main.e1a6e1f",
|
|
62
|
+
"@dxos/util": "0.6.6-main.e1a6e1f"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@phosphor-icons/react": "^2.1.5",
|
|
@@ -85,22 +85,22 @@
|
|
|
85
85
|
"vite-plugin-top-level-await": "^1.4.1",
|
|
86
86
|
"vite-plugin-wasm": "^3.3.0",
|
|
87
87
|
"vitest": "^1.5.0",
|
|
88
|
-
"@dxos/automerge": "0.6.
|
|
89
|
-
"@dxos/config": "0.6.
|
|
90
|
-
"@dxos/echo-signals": "0.6.
|
|
91
|
-
"@dxos/keyboard": "0.6.
|
|
92
|
-
"@dxos/echo-typegen": "0.6.
|
|
93
|
-
"@dxos/
|
|
94
|
-
"@dxos/
|
|
95
|
-
"@dxos/react-
|
|
96
|
-
"@dxos/storybook-utils": "0.6.
|
|
97
|
-
"@braneframe/types": "0.6.
|
|
88
|
+
"@dxos/automerge": "0.6.6-main.e1a6e1f",
|
|
89
|
+
"@dxos/config": "0.6.6-main.e1a6e1f",
|
|
90
|
+
"@dxos/echo-signals": "0.6.6-main.e1a6e1f",
|
|
91
|
+
"@dxos/keyboard": "0.6.6-main.e1a6e1f",
|
|
92
|
+
"@dxos/echo-typegen": "0.6.6-main.e1a6e1f",
|
|
93
|
+
"@dxos/random": "0.6.6-main.e1a6e1f",
|
|
94
|
+
"@dxos/react-client": "0.6.6-main.e1a6e1f",
|
|
95
|
+
"@dxos/react-ui": "0.6.6-main.e1a6e1f",
|
|
96
|
+
"@dxos/storybook-utils": "0.6.6-main.e1a6e1f",
|
|
97
|
+
"@braneframe/types": "0.6.6-main.e1a6e1f"
|
|
98
98
|
},
|
|
99
99
|
"peerDependencies": {
|
|
100
100
|
"@phosphor-icons/react": "^2.1.5",
|
|
101
101
|
"react": "^18.0.0",
|
|
102
102
|
"react-dom": "^18.0.0",
|
|
103
|
-
"@dxos/react-client": "0.6.
|
|
103
|
+
"@dxos/react-client": "0.6.6-main.e1a6e1f"
|
|
104
104
|
},
|
|
105
105
|
"publishConfig": {
|
|
106
106
|
"access": "public"
|
|
@@ -23,7 +23,7 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
23
23
|
|
|
24
24
|
import { TextEditor, type TextEditorProps } from './TextEditor';
|
|
25
25
|
import {
|
|
26
|
-
|
|
26
|
+
InputModeExtensions,
|
|
27
27
|
annotations,
|
|
28
28
|
autocomplete,
|
|
29
29
|
blast,
|
|
@@ -511,7 +511,7 @@ export const Vim = {
|
|
|
511
511
|
render: () => (
|
|
512
512
|
<Story
|
|
513
513
|
text={str('# Vim Mode', '', 'The distant future. The year 2000.', '', text.paragraphs)}
|
|
514
|
-
extensions={[defaults,
|
|
514
|
+
extensions={[defaults, InputModeExtensions.vim]}
|
|
515
515
|
/>
|
|
516
516
|
),
|
|
517
517
|
};
|
|
@@ -19,7 +19,7 @@ import { log } from '@dxos/log';
|
|
|
19
19
|
import { useDefaultValue } from '@dxos/react-ui';
|
|
20
20
|
import { isNotFalsy } from '@dxos/util';
|
|
21
21
|
|
|
22
|
-
import { documentId,
|
|
22
|
+
import { documentId, editorInputMode, focusEvent } from '../../extensions';
|
|
23
23
|
import { logChanges } from '../../util';
|
|
24
24
|
|
|
25
25
|
export type CursorInfo = {
|
|
@@ -143,7 +143,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
// Remove tabster attribute (rely on custom keymap).
|
|
146
|
-
if (state.facet(
|
|
146
|
+
if (state.facet(editorInputMode).noTabster) {
|
|
147
147
|
rootRef.current?.removeAttribute('data-tabster');
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -153,7 +153,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
153
153
|
log('destroy', { id, instanceId });
|
|
154
154
|
view?.destroy();
|
|
155
155
|
};
|
|
156
|
-
}, [id, selection, scrollTo,
|
|
156
|
+
}, [id, selection, scrollTo, extensions]);
|
|
157
157
|
|
|
158
158
|
// Focus editor on Enter (e.g., when tabbing to this component).
|
|
159
159
|
const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
|
@@ -10,13 +10,14 @@ import { TextType } from '@braneframe/types';
|
|
|
10
10
|
import { create } from '@dxos/echo-schema';
|
|
11
11
|
import { PublicKey } from '@dxos/keys';
|
|
12
12
|
import { faker } from '@dxos/random';
|
|
13
|
-
import { createDocAccessor } from '@dxos/react-client/echo';
|
|
13
|
+
import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
|
|
14
14
|
import { Tooltip, useThemeContext } from '@dxos/react-ui';
|
|
15
15
|
import { textBlockWidth } from '@dxos/react-ui-theme';
|
|
16
16
|
import { withTheme } from '@dxos/storybook-utils';
|
|
17
17
|
|
|
18
18
|
import { Toolbar } from './Toolbar';
|
|
19
19
|
import {
|
|
20
|
+
type Action,
|
|
20
21
|
type Comment,
|
|
21
22
|
comments,
|
|
22
23
|
createBasicExtensions,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
createMarkdownExtensions,
|
|
25
26
|
createThemeExtensions,
|
|
26
27
|
decorateMarkdown,
|
|
28
|
+
type EditorViewMode,
|
|
27
29
|
formattingKeymap,
|
|
28
30
|
image,
|
|
29
31
|
table,
|
|
@@ -37,15 +39,16 @@ faker.seed(101);
|
|
|
37
39
|
|
|
38
40
|
const Story: FC<{ content: string }> = ({ content }) => {
|
|
39
41
|
const { themeMode } = useThemeContext();
|
|
40
|
-
const [text] = useState(create(TextType, { content }));
|
|
42
|
+
const [text] = useState(createEchoObject(create(TextType, { content })));
|
|
41
43
|
const [formattingState, formattingObserver] = useFormattingState();
|
|
44
|
+
const [viewMode, setViewMode] = useState<EditorViewMode>('preview');
|
|
42
45
|
const { parentRef, view } = useTextEditor(() => {
|
|
43
46
|
return {
|
|
44
47
|
id: text.id,
|
|
45
48
|
doc: text.content,
|
|
46
49
|
extensions: [
|
|
47
50
|
formattingObserver,
|
|
48
|
-
createBasicExtensions(),
|
|
51
|
+
createBasicExtensions({ readonly: viewMode === 'readonly' }),
|
|
49
52
|
createMarkdownExtensions({ themeMode }),
|
|
50
53
|
createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2' } } }),
|
|
51
54
|
createDataExtensions({ id: text.id, text: createDocAccessor(text, ['content']) }),
|
|
@@ -56,15 +59,21 @@ const Story: FC<{ content: string }> = ({ content }) => {
|
|
|
56
59
|
return id;
|
|
57
60
|
},
|
|
58
61
|
}),
|
|
59
|
-
decorateMarkdown(),
|
|
60
62
|
formattingKeymap(),
|
|
61
63
|
image(),
|
|
62
|
-
table(),
|
|
64
|
+
...(viewMode !== 'source' ? [decorateMarkdown(), table()] : []),
|
|
63
65
|
],
|
|
64
66
|
};
|
|
65
|
-
}, [text, formattingObserver, themeMode]);
|
|
67
|
+
}, [text, formattingObserver, viewMode, themeMode]);
|
|
66
68
|
|
|
67
|
-
const
|
|
69
|
+
const handleToolbarAction = useActionHandler(view);
|
|
70
|
+
const handleAction = (action: Action) => {
|
|
71
|
+
if (action.type === 'view-mode') {
|
|
72
|
+
setViewMode(action.data);
|
|
73
|
+
} else {
|
|
74
|
+
handleToolbarAction?.(action);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
68
77
|
|
|
69
78
|
const [_comments, setComments] = useState<Comment[]>([]);
|
|
70
79
|
useComments(view, text.id, _comments);
|
|
@@ -73,6 +82,7 @@ const Story: FC<{ content: string }> = ({ content }) => {
|
|
|
73
82
|
<Tooltip.Provider>
|
|
74
83
|
<div role='none' className='fixed inset-0 flex flex-col'>
|
|
75
84
|
<Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
|
|
85
|
+
<Toolbar.View mode={viewMode} />
|
|
76
86
|
<Toolbar.Markdown />
|
|
77
87
|
<Toolbar.Custom onUpload={async (file) => ({ url: file.name })} />
|
|
78
88
|
<Toolbar.Separator />
|
|
@@ -26,6 +26,9 @@ import {
|
|
|
26
26
|
TextItalic,
|
|
27
27
|
CaretDown,
|
|
28
28
|
Check,
|
|
29
|
+
PencilSimpleSlash,
|
|
30
|
+
MarkdownLogo,
|
|
31
|
+
PencilSimple,
|
|
29
32
|
} from '@phosphor-icons/react';
|
|
30
33
|
import { createContext } from '@radix-ui/react-context';
|
|
31
34
|
import React, { type PropsWithChildren, useEffect, useRef, useState } from 'react';
|
|
@@ -45,7 +48,7 @@ import {
|
|
|
45
48
|
} from '@dxos/react-ui';
|
|
46
49
|
import { getSize } from '@dxos/react-ui-theme';
|
|
47
50
|
|
|
48
|
-
import { type Action, type ActionType, type Formatting } from '../../extensions';
|
|
51
|
+
import { type EditorViewMode, type Action, type ActionType, type Formatting, EditorViewModes } from '../../extensions';
|
|
49
52
|
import { translationKey } from '../../translations';
|
|
50
53
|
|
|
51
54
|
const iconStyles = getSize(5);
|
|
@@ -60,7 +63,7 @@ const ToolbarSeparator = () => <div role='separator' className='grow' />;
|
|
|
60
63
|
|
|
61
64
|
export type ToolbarProps = ThemedClassName<
|
|
62
65
|
PropsWithChildren<{
|
|
63
|
-
state: (Formatting & { comment?: boolean; selection?: boolean }) | undefined;
|
|
66
|
+
state: (Formatting & { comment?: boolean; mode?: EditorViewMode; selection?: boolean }) | undefined;
|
|
64
67
|
onAction?: (action: Action) => void;
|
|
65
68
|
}>
|
|
66
69
|
>;
|
|
@@ -72,7 +75,10 @@ const ToolbarRoot = ({ children, onAction, classNames, state }: ToolbarProps) =>
|
|
|
72
75
|
<ToolbarContextProvider onAction={onAction} state={state}>
|
|
73
76
|
<DensityProvider density='fine'>
|
|
74
77
|
<ElevationProvider elevation='chrome'>
|
|
75
|
-
<NaturalToolbar.Root
|
|
78
|
+
<NaturalToolbar.Root
|
|
79
|
+
classNames={['p-1 is-full shrink-0 overflow-x-auto overflow-y-hidden', classNames]}
|
|
80
|
+
style={{ contain: 'layout' }}
|
|
81
|
+
>
|
|
76
82
|
{children}
|
|
77
83
|
</NaturalToolbar.Root>
|
|
78
84
|
</ElevationProvider>
|
|
@@ -134,6 +140,89 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
|
134
140
|
);
|
|
135
141
|
};
|
|
136
142
|
|
|
143
|
+
//
|
|
144
|
+
// View Mode
|
|
145
|
+
//
|
|
146
|
+
|
|
147
|
+
const ViewModeIcons: Record<EditorViewMode, Icon> = {
|
|
148
|
+
preview: PencilSimple,
|
|
149
|
+
readonly: PencilSimpleSlash,
|
|
150
|
+
source: MarkdownLogo,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const MarkdownView = ({ mode }: { mode: EditorViewMode }) => {
|
|
154
|
+
const { t } = useTranslation(translationKey);
|
|
155
|
+
const { onAction } = useToolbarContext('ViewMode');
|
|
156
|
+
const ModeIcon = ViewModeIcons[mode ?? 'preview'];
|
|
157
|
+
const suppressNextTooltip = useRef<boolean>(false);
|
|
158
|
+
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false);
|
|
159
|
+
const [selectOpen, setSelectOpen] = useState<boolean>(false);
|
|
160
|
+
return (
|
|
161
|
+
<Tooltip.Root
|
|
162
|
+
open={tooltipOpen}
|
|
163
|
+
onOpenChange={(nextOpen) => {
|
|
164
|
+
if (nextOpen && suppressNextTooltip.current) {
|
|
165
|
+
suppressNextTooltip.current = false;
|
|
166
|
+
return setTooltipOpen(false);
|
|
167
|
+
} else {
|
|
168
|
+
return setTooltipOpen(nextOpen);
|
|
169
|
+
}
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
{/* TODO(thure): `Select` encounters a ref error if used here (repro: select a heading, then select another
|
|
173
|
+
heading). Determine the root cause and fix or report to Radix. */}
|
|
174
|
+
<DropdownMenu.Root
|
|
175
|
+
open={selectOpen}
|
|
176
|
+
onOpenChange={(nextOpen: boolean) => {
|
|
177
|
+
if (!nextOpen) {
|
|
178
|
+
suppressNextTooltip.current = true;
|
|
179
|
+
}
|
|
180
|
+
return setSelectOpen(nextOpen);
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
<Tooltip.Trigger asChild>
|
|
184
|
+
<NaturalToolbar.Button asChild>
|
|
185
|
+
<DropdownMenu.Trigger asChild>
|
|
186
|
+
<Button variant='ghost' classNames={buttonStyles}>
|
|
187
|
+
<span className='sr-only'>{t('mode label')}</span>
|
|
188
|
+
<ModeIcon className={iconStyles} />
|
|
189
|
+
<CaretDown />
|
|
190
|
+
</Button>
|
|
191
|
+
</DropdownMenu.Trigger>
|
|
192
|
+
</NaturalToolbar.Button>
|
|
193
|
+
</Tooltip.Trigger>
|
|
194
|
+
<DropdownMenu.Portal>
|
|
195
|
+
<DropdownMenu.Content classNames='is-min md:is-min' onCloseAutoFocus={(e) => e.preventDefault()}>
|
|
196
|
+
<DropdownMenu.Viewport>
|
|
197
|
+
{EditorViewModes.map((value) => {
|
|
198
|
+
const Icon = ViewModeIcons[value];
|
|
199
|
+
return (
|
|
200
|
+
<DropdownMenu.CheckboxItem
|
|
201
|
+
key={value}
|
|
202
|
+
checked={value === mode}
|
|
203
|
+
onClick={() => onAction?.({ type: 'view-mode', data: value })}
|
|
204
|
+
>
|
|
205
|
+
<Icon className={iconStyles} />
|
|
206
|
+
<span className='whitespace-nowrap grow'>{t(`${value} mode label`)}</span>
|
|
207
|
+
<Check className={value === mode ? 'visible' : 'invisible'} />
|
|
208
|
+
</DropdownMenu.CheckboxItem>
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
211
|
+
</DropdownMenu.Viewport>
|
|
212
|
+
<DropdownMenu.Arrow />
|
|
213
|
+
</DropdownMenu.Content>
|
|
214
|
+
</DropdownMenu.Portal>
|
|
215
|
+
</DropdownMenu.Root>
|
|
216
|
+
<Tooltip.Portal>
|
|
217
|
+
<Tooltip.Content {...tooltipProps}>
|
|
218
|
+
{t('view mode label')}
|
|
219
|
+
<Tooltip.Arrow />
|
|
220
|
+
</Tooltip.Content>
|
|
221
|
+
</Tooltip.Portal>
|
|
222
|
+
</Tooltip.Root>
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
137
226
|
//
|
|
138
227
|
// Heading
|
|
139
228
|
//
|
|
@@ -420,6 +509,7 @@ export const Toolbar = {
|
|
|
420
509
|
Root: ToolbarRoot,
|
|
421
510
|
Button: ToolbarToggleButton,
|
|
422
511
|
Separator: ToolbarSeparator,
|
|
512
|
+
View: MarkdownView,
|
|
423
513
|
Markdown: MarkdownStandard,
|
|
424
514
|
Custom: MarkdownCustom,
|
|
425
515
|
Actions: MarkdownActions,
|
|
@@ -19,13 +19,14 @@ import { keymap } from '@codemirror/view';
|
|
|
19
19
|
export type AutocompleteResult = Completion;
|
|
20
20
|
|
|
21
21
|
export type AutocompleteOptions = {
|
|
22
|
+
activateOnTyping?: boolean;
|
|
22
23
|
onSearch: (text: string) => Completion[];
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Autocomplete extension.
|
|
27
28
|
*/
|
|
28
|
-
export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
29
|
+
export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions) => {
|
|
29
30
|
return [
|
|
30
31
|
// https://codemirror.net/docs/ref/#view.keymap
|
|
31
32
|
// https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
|
|
@@ -35,7 +36,7 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
|
35
36
|
// https://codemirror.net/examples/autocompletion
|
|
36
37
|
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
37
38
|
autocompletion({
|
|
38
|
-
activateOnTyping
|
|
39
|
+
activateOnTyping,
|
|
39
40
|
|
|
40
41
|
// closeOnBlur: false,
|
|
41
42
|
// defaultKeymap: false,
|
|
@@ -44,7 +45,6 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
|
44
45
|
tooltipClass: () => 'shadow rounded',
|
|
45
46
|
}),
|
|
46
47
|
|
|
47
|
-
// TODO(burdon): Option to create new page?
|
|
48
48
|
// TODO(burdon): Optional decoration via addToOptions
|
|
49
49
|
markdownLanguage.data.of({
|
|
50
50
|
autocomplete: (context: CompletionContext): CompletionResult | null => {
|
|
@@ -40,6 +40,7 @@ import { callbackWrapper } from '../util';
|
|
|
40
40
|
// State management.
|
|
41
41
|
//
|
|
42
42
|
|
|
43
|
+
// TODO(wittjosiah): Factor out, not comments-specific.
|
|
43
44
|
const documentId = Facet.define<string | undefined, string | undefined>({ combine: (values) => values[0] });
|
|
44
45
|
|
|
45
46
|
type CommentState = {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type EditorState, type RangeSet, StateField, type Transaction } from '@codemirror/state';
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line no-console
|
|
9
|
+
export const debugNodeLogger = (log: (...args: any[]) => void = console.log) => {
|
|
10
|
+
const logTokens = (state: EditorState) => syntaxTree(state).iterate({ enter: (node) => log(node.type) });
|
|
11
|
+
return StateField.define<any>({
|
|
12
|
+
create: (state) => logTokens(state),
|
|
13
|
+
update: (_: RangeSet<any>, tr: Transaction) => logTokens(tr.state),
|
|
14
|
+
});
|
|
15
|
+
};
|
package/src/extensions/index.ts
CHANGED
package/src/extensions/modes.ts
CHANGED
|
@@ -9,28 +9,31 @@ import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
|
|
9
9
|
|
|
10
10
|
export const focusEvent = 'focus.container';
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export const EditorViewModes = ['preview', 'readonly', 'source'] as const;
|
|
13
|
+
export type EditorViewMode = (typeof EditorViewModes)[number];
|
|
14
|
+
export const EditorInputModes = ['default', 'vim', 'vscode'] as const;
|
|
15
|
+
export type EditorInputMode = (typeof EditorInputModes)[number];
|
|
13
16
|
|
|
14
|
-
export type
|
|
17
|
+
export type EditorInputConfig = {
|
|
15
18
|
type: string;
|
|
16
19
|
noTabster?: boolean;
|
|
17
20
|
};
|
|
18
21
|
|
|
19
|
-
export const
|
|
22
|
+
export const editorInputMode = Facet.define<EditorInputConfig, EditorInputConfig>({
|
|
20
23
|
combine: (modes) => modes[0] ?? {},
|
|
21
24
|
});
|
|
22
25
|
|
|
23
|
-
export const
|
|
26
|
+
export const InputModeExtensions: { [mode: string]: Extension } = {
|
|
24
27
|
default: [],
|
|
25
28
|
vscode: [
|
|
26
29
|
// https://github.com/replit/codemirror-vscode-keymap
|
|
27
|
-
|
|
30
|
+
editorInputMode.of({ type: 'vscode' }),
|
|
28
31
|
keymap.of(vscodeKeymap),
|
|
29
32
|
],
|
|
30
33
|
vim: [
|
|
31
34
|
// https://github.com/replit/codemirror-vim
|
|
32
35
|
vim(),
|
|
33
|
-
|
|
36
|
+
editorInputMode.of({ type: 'vim', noTabster: true }),
|
|
34
37
|
keymap.of([
|
|
35
38
|
{
|
|
36
39
|
key: 'Alt-Escape',
|
|
@@ -13,10 +13,9 @@ import { withTheme } from '@dxos/storybook-utils';
|
|
|
13
13
|
import { useActionHandler } from './useActionHandler';
|
|
14
14
|
import { useTextEditor } from './useTextEditor';
|
|
15
15
|
import { Toolbar } from '../components';
|
|
16
|
-
import { createBasicExtensions, createThemeExtensions } from '../extensions';
|
|
16
|
+
import { createBasicExtensions, createThemeExtensions, InputModeExtensions } from '../extensions';
|
|
17
17
|
import {
|
|
18
|
-
type
|
|
19
|
-
EditorModes,
|
|
18
|
+
type EditorInputMode,
|
|
20
19
|
decorateMarkdown,
|
|
21
20
|
createMarkdownExtensions,
|
|
22
21
|
formattingKeymap,
|
|
@@ -36,13 +35,13 @@ type StoryProps = {
|
|
|
36
35
|
const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
37
36
|
const { themeMode } = useThemeContext();
|
|
38
37
|
const [formattingState, trackFormatting] = useFormattingState();
|
|
39
|
-
const [
|
|
38
|
+
const [editorInputMode, setEditorInputMode] = useState<EditorInputMode>('default');
|
|
40
39
|
const { parentRef, view } = useTextEditor(
|
|
41
40
|
() => ({
|
|
42
41
|
autoFocus,
|
|
43
42
|
doc,
|
|
44
43
|
extensions: [
|
|
45
|
-
|
|
44
|
+
editorInputMode ? InputModeExtensions[editorInputMode] : [],
|
|
46
45
|
createBasicExtensions({ placeholder, lineWrapping: true, readonly }),
|
|
47
46
|
createMarkdownExtensions({ themeMode }),
|
|
48
47
|
createThemeExtensions({ themeMode }),
|
|
@@ -53,7 +52,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
53
52
|
trackFormatting,
|
|
54
53
|
],
|
|
55
54
|
}),
|
|
56
|
-
[
|
|
55
|
+
[editorInputMode, themeMode, placeholder, readonly],
|
|
57
56
|
);
|
|
58
57
|
|
|
59
58
|
const handleAction = useActionHandler(view);
|
|
@@ -64,7 +63,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
64
63
|
<div role='none' className={mx('fixed inset-0 flex flex-col')}>
|
|
65
64
|
<Toolbar.Root onAction={handleAction} state={formattingState} classNames={textBlockWidth}>
|
|
66
65
|
<Toolbar.Markdown />
|
|
67
|
-
<
|
|
66
|
+
<EditorInputModeToolbar editorInputMode={editorInputMode} setEditorInputMode={setEditorInputMode} />
|
|
68
67
|
</Toolbar.Root>
|
|
69
68
|
<div role='none' className='grow overflow-hidden'>
|
|
70
69
|
<div className={mx(textBlockWidth, attentionSurface)} ref={parentRef} />
|
|
@@ -73,17 +72,17 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
73
72
|
);
|
|
74
73
|
};
|
|
75
74
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
const EditorInputModeToolbar = ({
|
|
76
|
+
editorInputMode,
|
|
77
|
+
setEditorInputMode,
|
|
79
78
|
}: {
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
editorInputMode: EditorInputMode;
|
|
80
|
+
setEditorInputMode: (mode: EditorInputMode) => void;
|
|
82
81
|
}) => {
|
|
83
82
|
return (
|
|
84
|
-
<Select.Root value={
|
|
83
|
+
<Select.Root value={editorInputMode} onValueChange={(value) => setEditorInputMode(value as EditorInputMode)}>
|
|
85
84
|
<NaturalToolbar.Button asChild>
|
|
86
|
-
<Select.TriggerButton variant='ghost'>{
|
|
85
|
+
<Select.TriggerButton variant='ghost'>{editorInputMode}</Select.TriggerButton>
|
|
87
86
|
</NaturalToolbar.Button>
|
|
88
87
|
<Select.Portal>
|
|
89
88
|
<Select.Content>
|
package/src/translations.ts
CHANGED
|
@@ -25,6 +25,10 @@ export default [
|
|
|
25
25
|
'heading level label_zero': 'Paragraph',
|
|
26
26
|
'heading level label_one': 'Heading level {{count}}',
|
|
27
27
|
'heading level label_other': 'Heading level {{count}}',
|
|
28
|
+
'view mode label': 'Editor view',
|
|
29
|
+
'preview mode label': 'Live preview',
|
|
30
|
+
'readonly mode label': 'Read only',
|
|
31
|
+
'source mode label': 'Source',
|
|
28
32
|
},
|
|
29
33
|
},
|
|
30
34
|
},
|