@dxos/react-ui-editor 0.4.3-main.c3bfede → 0.4.3-main.d01b40e
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 +157 -118
- 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 +6 -3
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +0 -3
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -3
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts +3 -3
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts +3 -3
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +3 -3
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- 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/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +11 -0
- package/dist/types/src/extensions/modes.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +3 -3
- package/package.json +25 -25
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +10 -1
- package/src/components/TextEditor/TextEditor.tsx +29 -50
- package/src/components/Toolbar/Toolbar.tsx +81 -69
- package/src/extensions/comments.ts +5 -2
- package/src/extensions/index.ts +1 -0
- package/src/extensions/markdown/formatting.test.ts +1 -1
- package/src/extensions/markdown/formatting.ts +5 -2
- package/src/extensions/modes.ts +36 -0
- package/src/hooks/useActionHandler.ts +1 -1
- package/src/index.ts +3 -2
- package/src/translations.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-editor",
|
|
3
|
-
"version": "0.4.3-main.
|
|
3
|
+
"version": "0.4.3-main.d01b40e",
|
|
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",
|
|
@@ -36,23 +36,23 @@
|
|
|
36
36
|
"lodash.pick": "^4.4.0",
|
|
37
37
|
"lodash.sortby": "^4.7.0",
|
|
38
38
|
"style-mod": "^4.1.0",
|
|
39
|
-
"@dxos/async": "0.4.3-main.
|
|
40
|
-
"@dxos/automerge": "0.4.3-main.
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/
|
|
48
|
-
"@dxos/
|
|
49
|
-
"@dxos/react-ui": "0.4.3-main.
|
|
50
|
-
"@dxos/react-ui-theme": "0.4.3-main.
|
|
51
|
-
"@dxos/text-model": "0.4.3-main.
|
|
52
|
-
"@dxos/util": "0.4.3-main.
|
|
39
|
+
"@dxos/async": "0.4.3-main.d01b40e",
|
|
40
|
+
"@dxos/automerge": "0.4.3-main.d01b40e",
|
|
41
|
+
"@dxos/context": "0.4.3-main.d01b40e",
|
|
42
|
+
"@dxos/debug": "0.4.3-main.d01b40e",
|
|
43
|
+
"@dxos/display-name": "0.4.3-main.d01b40e",
|
|
44
|
+
"@dxos/echo-schema": "0.4.3-main.d01b40e",
|
|
45
|
+
"@dxos/invariant": "0.4.3-main.d01b40e",
|
|
46
|
+
"@dxos/log": "0.4.3-main.d01b40e",
|
|
47
|
+
"@dxos/protocols": "0.4.3-main.d01b40e",
|
|
48
|
+
"@dxos/react-async": "0.4.3-main.d01b40e",
|
|
49
|
+
"@dxos/react-ui": "0.4.3-main.d01b40e",
|
|
50
|
+
"@dxos/react-ui-theme": "0.4.3-main.d01b40e",
|
|
51
|
+
"@dxos/text-model": "0.4.3-main.d01b40e",
|
|
52
|
+
"@dxos/util": "0.4.3-main.d01b40e"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@automerge/automerge-repo-network-broadcastchannel": "^1.1.
|
|
55
|
+
"@automerge/automerge-repo-network-broadcastchannel": "^1.1.1",
|
|
56
56
|
"@phosphor-icons/react": "^2.0.5",
|
|
57
57
|
"@preact/signals-react": "^1.3.6",
|
|
58
58
|
"@testing-library/dom": "^8.17.1",
|
|
@@ -76,20 +76,20 @@
|
|
|
76
76
|
"vite-plugin-top-level-await": "^1.4.1",
|
|
77
77
|
"vite-plugin-wasm": "^3.3.0",
|
|
78
78
|
"vitest": "^1.2.1",
|
|
79
|
-
"@dxos/config": "0.4.3-main.
|
|
80
|
-
"@dxos/echo-signals": "0.4.3-main.
|
|
81
|
-
"@dxos/
|
|
82
|
-
"@dxos/
|
|
83
|
-
"@dxos/
|
|
84
|
-
"@dxos/react-
|
|
85
|
-
"@dxos/
|
|
86
|
-
"@dxos/
|
|
79
|
+
"@dxos/config": "0.4.3-main.d01b40e",
|
|
80
|
+
"@dxos/echo-signals": "0.4.3-main.d01b40e",
|
|
81
|
+
"@dxos/echo-typegen": "0.4.3-main.d01b40e",
|
|
82
|
+
"@dxos/react-client": "0.4.3-main.d01b40e",
|
|
83
|
+
"@dxos/keyboard": "0.4.3-main.d01b40e",
|
|
84
|
+
"@dxos/react-ui": "0.4.3-main.d01b40e",
|
|
85
|
+
"@dxos/storybook-utils": "0.4.3-main.d01b40e",
|
|
86
|
+
"@dxos/random": "0.4.3-main.d01b40e"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"@phosphor-icons/react": "^2.0.5",
|
|
90
90
|
"react": "^18.2.0",
|
|
91
91
|
"react-dom": "^18.2.0",
|
|
92
|
-
"@dxos/react-client": "0.4.3-main.
|
|
92
|
+
"@dxos/react-client": "0.4.3-main.d01b40e"
|
|
93
93
|
},
|
|
94
94
|
"publishConfig": {
|
|
95
95
|
"access": "public"
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
|
-
|
|
7
6
|
import { type EditorView } from '@codemirror/view';
|
|
8
7
|
import { ArrowSquareOut } from '@phosphor-icons/react';
|
|
9
8
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
@@ -37,6 +36,7 @@ import {
|
|
|
37
36
|
useComments,
|
|
38
37
|
formatting,
|
|
39
38
|
annotations,
|
|
39
|
+
EditorModes,
|
|
40
40
|
} from '../../extensions';
|
|
41
41
|
import { type Comment, useTextModel } from '../../hooks';
|
|
42
42
|
import translations from '../../translations';
|
|
@@ -390,6 +390,15 @@ export const HorizontalRule = {
|
|
|
390
390
|
),
|
|
391
391
|
};
|
|
392
392
|
|
|
393
|
+
export const Vim = {
|
|
394
|
+
render: () => (
|
|
395
|
+
<Story
|
|
396
|
+
text={str('# Vim Mode', '', 'The distant future. The year 2000.', '', text.paragraphs)}
|
|
397
|
+
extensions={[defaults, EditorModes.vim]}
|
|
398
|
+
/>
|
|
399
|
+
),
|
|
400
|
+
};
|
|
401
|
+
|
|
393
402
|
export const Annotations = {
|
|
394
403
|
render: () => <Story text={str('# Annotations', '', large)} extensions={[annotations({ match: /volup/gi })]} />,
|
|
395
404
|
};
|
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { EditorState, type Extension, type StateEffect } from '@codemirror/state';
|
|
6
6
|
import { EditorView } from '@codemirror/view';
|
|
7
|
-
|
|
8
|
-
import { vim } from '@replit/codemirror-vim';
|
|
7
|
+
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
9
8
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
10
9
|
import React, {
|
|
11
10
|
type ComponentProps,
|
|
12
|
-
type KeyboardEvent,
|
|
13
11
|
forwardRef,
|
|
12
|
+
type KeyboardEventHandler,
|
|
14
13
|
useCallback,
|
|
15
14
|
useEffect,
|
|
16
15
|
useImperativeHandle,
|
|
@@ -23,16 +22,12 @@ import { useThemeContext } from '@dxos/react-ui';
|
|
|
23
22
|
import { focusRing } from '@dxos/react-ui-theme';
|
|
24
23
|
import { isNotFalsy } from '@dxos/util';
|
|
25
24
|
|
|
26
|
-
import { createBasicBundle, createMarkdownExtensions } from '../../extensions';
|
|
25
|
+
import { createBasicBundle, createMarkdownExtensions, editorMode } from '../../extensions';
|
|
27
26
|
import { type EditorModel } from '../../hooks';
|
|
28
27
|
import { type ThemeStyles } from '../../styles';
|
|
29
28
|
import { defaultTheme, markdownTheme, textTheme } from '../../themes';
|
|
30
29
|
import { logChanges } from '../../util';
|
|
31
30
|
|
|
32
|
-
// TODO(burdon): Change to enum?
|
|
33
|
-
export const EditorModes = ['default', 'vim'] as const;
|
|
34
|
-
export type EditorMode = (typeof EditorModes)[number];
|
|
35
|
-
|
|
36
31
|
export type CursorInfo = {
|
|
37
32
|
from: number;
|
|
38
33
|
to: number;
|
|
@@ -52,7 +47,6 @@ export type TextEditorSlots = {
|
|
|
52
47
|
};
|
|
53
48
|
};
|
|
54
49
|
|
|
55
|
-
// TODO(burdon): Spellcheck?
|
|
56
50
|
export type TextEditorProps = {
|
|
57
51
|
model: EditorModel; // TODO(burdon): Optional (e.g., just provide content if readonly).
|
|
58
52
|
readonly?: boolean; // TODO(burdon): Move into model.
|
|
@@ -60,7 +54,6 @@ export type TextEditorProps = {
|
|
|
60
54
|
lineWrapping?: boolean;
|
|
61
55
|
scrollTo?: StateEffect<any>; // TODO(burdon): Restore scroll position: scrollTo EditorView.scrollSnapshot().
|
|
62
56
|
selection?: { anchor: number; head?: number };
|
|
63
|
-
editorMode?: EditorMode; // TODO(burdon): Factor out.
|
|
64
57
|
placeholder?: string;
|
|
65
58
|
theme?: ThemeStyles;
|
|
66
59
|
slots?: TextEditorSlots;
|
|
@@ -74,26 +67,10 @@ export type TextEditorProps = {
|
|
|
74
67
|
// TODO(burdon): Replace with useTextEditor.
|
|
75
68
|
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
76
69
|
(
|
|
77
|
-
{
|
|
78
|
-
model,
|
|
79
|
-
readonly,
|
|
80
|
-
autoFocus,
|
|
81
|
-
scrollTo,
|
|
82
|
-
selection,
|
|
83
|
-
editorMode,
|
|
84
|
-
theme,
|
|
85
|
-
slots = defaultSlots,
|
|
86
|
-
extensions = [],
|
|
87
|
-
debug,
|
|
88
|
-
},
|
|
70
|
+
{ model, readonly, autoFocus, scrollTo, selection, theme, slots = defaultSlots, extensions = [], debug },
|
|
89
71
|
forwardedRef,
|
|
90
72
|
) => {
|
|
91
|
-
|
|
92
|
-
// Uncaught TypeError: Cannot read properties of undefined (reading 'relatedTarget')
|
|
93
|
-
// Uses event.detail, which is deprecated (not event.details). At runtime the event has a property `details`.
|
|
94
|
-
// https://github.com/microsoft/tabster/blob/master/src/State/FocusedElement.ts#L348 (e.detail.relatedTarget)
|
|
95
|
-
// https://github.com/microsoft/keyborg/blob/49e49b2c3ba0a5f6cc518ac46825d7551def8109/src/FocusEvent.ts#L58
|
|
96
|
-
// const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
73
|
+
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
97
74
|
const { themeMode } = useThemeContext();
|
|
98
75
|
|
|
99
76
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
@@ -143,6 +120,15 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
143
120
|
EditorView.editorAttributes.of({ class: slots.editor?.className ?? '' }),
|
|
144
121
|
EditorView.contentAttributes.of({ class: slots.content?.className ?? '' }),
|
|
145
122
|
|
|
123
|
+
// Focus.
|
|
124
|
+
EditorView.updateListener.of((update) => {
|
|
125
|
+
update.transactions.forEach((transaction) => {
|
|
126
|
+
if (transaction.isUserEvent('focus.container')) {
|
|
127
|
+
rootRef.current?.focus();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}),
|
|
131
|
+
|
|
146
132
|
// State.
|
|
147
133
|
EditorView.editable.of(!readonly),
|
|
148
134
|
EditorState.readOnly.of(!!readonly),
|
|
@@ -151,9 +137,6 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
151
137
|
// NOTE: This must come before user extensions.
|
|
152
138
|
model.extension,
|
|
153
139
|
|
|
154
|
-
// TODO(burdon): Factor out? (Requires special handling for Escape/Enter below).
|
|
155
|
-
editorMode === 'vim' && vim(),
|
|
156
|
-
|
|
157
140
|
// Custom.
|
|
158
141
|
...extensions,
|
|
159
142
|
].filter(isNotFalsy),
|
|
@@ -180,44 +163,40 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
180
163
|
view?.destroy();
|
|
181
164
|
setView(newView);
|
|
182
165
|
|
|
166
|
+
// Remove tabster attribute (rely on custom keymap).
|
|
167
|
+
if (state.facet(editorMode).noTabster) {
|
|
168
|
+
rootRef.current.removeAttribute('data-tabster');
|
|
169
|
+
}
|
|
170
|
+
|
|
183
171
|
return () => {
|
|
184
172
|
newView?.destroy();
|
|
185
173
|
setView(null);
|
|
186
174
|
};
|
|
187
|
-
}, [rootRef, model, readonly,
|
|
175
|
+
}, [rootRef, model, readonly, themeMode]);
|
|
188
176
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
(event: KeyboardEvent) => {
|
|
194
|
-
const { key, altKey, shiftKey, metaKey, ctrlKey } = event;
|
|
177
|
+
// Focus editor on Enter (e.g., when tabbing to this component).
|
|
178
|
+
const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
|
179
|
+
(event) => {
|
|
180
|
+
const { key } = event;
|
|
195
181
|
switch (key) {
|
|
196
182
|
case 'Enter': {
|
|
197
183
|
view?.focus();
|
|
198
184
|
break;
|
|
199
185
|
}
|
|
200
|
-
|
|
201
|
-
case 'Escape': {
|
|
202
|
-
if (editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey)) {
|
|
203
|
-
view?.focus();
|
|
204
|
-
}
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
186
|
}
|
|
208
187
|
},
|
|
209
|
-
[view
|
|
188
|
+
[view],
|
|
210
189
|
);
|
|
211
190
|
|
|
212
191
|
return (
|
|
213
192
|
<div
|
|
214
|
-
key={model.id}
|
|
215
193
|
role='none'
|
|
194
|
+
ref={rootRef}
|
|
195
|
+
key={model.id}
|
|
216
196
|
tabIndex={0}
|
|
217
|
-
{...slots.root}
|
|
218
|
-
// {...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
219
197
|
onKeyUp={handleKeyUp}
|
|
220
|
-
|
|
198
|
+
{...slots.root}
|
|
199
|
+
{...tabsterDOMAttribute}
|
|
221
200
|
/>
|
|
222
201
|
);
|
|
223
202
|
},
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
TextHSix,
|
|
24
24
|
TextItalic,
|
|
25
25
|
Quotes,
|
|
26
|
-
CaretDown,
|
|
27
26
|
} from '@phosphor-icons/react';
|
|
28
27
|
import { createContext } from '@radix-ui/react-context';
|
|
29
28
|
import React, { type PropsWithChildren } from 'react';
|
|
@@ -65,7 +64,7 @@ export type ActionType =
|
|
|
65
64
|
| 'link'
|
|
66
65
|
| 'list-bullet'
|
|
67
66
|
| 'list-ordered'
|
|
68
|
-
| 'list-
|
|
67
|
+
| 'list-task'
|
|
69
68
|
| 'mention'
|
|
70
69
|
| 'prompt'
|
|
71
70
|
| 'strikethrough'
|
|
@@ -99,12 +98,17 @@ const ToolbarRoot = ({ children, onAction, classNames, state }: ToolbarProps) =>
|
|
|
99
98
|
);
|
|
100
99
|
};
|
|
101
100
|
|
|
102
|
-
|
|
101
|
+
const buttonStyles = 'min-bs-0 p-2';
|
|
102
|
+
const iconStyles = getSize(5);
|
|
103
|
+
|
|
104
|
+
type ButtonProps = {
|
|
105
|
+
type: ActionType;
|
|
103
106
|
Icon: Icon;
|
|
107
|
+
getState: (state: Formatting) => boolean;
|
|
108
|
+
disabled?: (state: Formatting) => boolean;
|
|
104
109
|
};
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
const iconStyles = getSize(4);
|
|
111
|
+
type ToolbarButtonProps = ToolbarToggleGroupItemProps & { Icon: Icon };
|
|
108
112
|
|
|
109
113
|
const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
110
114
|
return (
|
|
@@ -118,7 +122,7 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
|
118
122
|
const ToolbarSeparator = () => <div role='separator' className='grow' />;
|
|
119
123
|
|
|
120
124
|
const MarkdownHeading = () => {
|
|
121
|
-
const { t } = useTranslation(translationKey);
|
|
125
|
+
// const { t } = useTranslation(translationKey);
|
|
122
126
|
const { onAction, state } = useToolbarContext('MarkdownFormatting');
|
|
123
127
|
const blockType = state ? state.blockType : 'paragraph';
|
|
124
128
|
const header = blockType && /heading(\d)/.exec(blockType);
|
|
@@ -130,21 +134,30 @@ const MarkdownHeading = () => {
|
|
|
130
134
|
value={value ?? '0'}
|
|
131
135
|
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
132
136
|
>
|
|
133
|
-
<Select.
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
<Select.TriggerButton>
|
|
138
|
+
<HeadingIcon className={iconStyles} />
|
|
139
|
+
{/* TODO(burdon): Incorrectly positioned. */}
|
|
140
|
+
{/* <NaturalToolbar.Button variant='ghost' classNames={buttonStyles}> */}
|
|
141
|
+
{/* <span className='sr-only'>{t('heading label')}</span> */}
|
|
142
|
+
{/* <HeadingIcon className={iconStyles} /> */}
|
|
143
|
+
{/* <CaretDown className={getSize(2)} weight='bold' /> */}
|
|
144
|
+
{/* </NaturalToolbar.Button> */}
|
|
145
|
+
</Select.TriggerButton>
|
|
140
146
|
<Select.Portal>
|
|
141
147
|
<Select.Content>
|
|
142
148
|
<Select.ScrollUpButton />
|
|
143
149
|
<Select.Viewport>
|
|
144
|
-
<Select.Option value='0'>
|
|
150
|
+
<Select.Option value='0'>
|
|
151
|
+
<Paragraph className={iconStyles} />
|
|
152
|
+
</Select.Option>
|
|
145
153
|
{[1, 2, 3, 4, 5, 6].map((level) => (
|
|
146
154
|
<Select.Option key={level} value={String(level)}>
|
|
147
|
-
|
|
155
|
+
{level === 1 && <TextHOne className={iconStyles} />}
|
|
156
|
+
{level === 2 && <TextHTwo className={iconStyles} />}
|
|
157
|
+
{level === 3 && <TextHThree className={iconStyles} />}
|
|
158
|
+
{level === 4 && <TextHFour className={iconStyles} />}
|
|
159
|
+
{level === 5 && <TextHFive className={iconStyles} />}
|
|
160
|
+
{level === 6 && <TextHSix className={iconStyles} />}
|
|
148
161
|
</Select.Option>
|
|
149
162
|
))}
|
|
150
163
|
</Select.Viewport>
|
|
@@ -156,14 +169,12 @@ const MarkdownHeading = () => {
|
|
|
156
169
|
);
|
|
157
170
|
};
|
|
158
171
|
|
|
159
|
-
const markdownStyles = [
|
|
160
|
-
{
|
|
161
|
-
{
|
|
162
|
-
{
|
|
163
|
-
{
|
|
164
|
-
{
|
|
165
|
-
// <ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
|
|
166
|
-
// <ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
|
|
172
|
+
const markdownStyles: ButtonProps[] = [
|
|
173
|
+
{ type: 'strong', Icon: TextB, getState: (state) => state.strong },
|
|
174
|
+
{ type: 'emphasis', Icon: TextItalic, getState: (state) => state.emphasis },
|
|
175
|
+
{ type: 'strikethrough', Icon: TextStrikethrough, getState: (state) => state.strikethrough },
|
|
176
|
+
{ type: 'code', Icon: Code, getState: (state) => state.code },
|
|
177
|
+
{ type: 'link', Icon: Link, getState: (state) => state.link },
|
|
167
178
|
];
|
|
168
179
|
|
|
169
180
|
const MarkdownStyles = () => {
|
|
@@ -173,27 +184,27 @@ const MarkdownStyles = () => {
|
|
|
173
184
|
return (
|
|
174
185
|
<NaturalToolbar.ToggleGroup
|
|
175
186
|
type='multiple'
|
|
176
|
-
value={markdownStyles.filter(({
|
|
187
|
+
value={markdownStyles.filter(({ getState }) => state && getState(state)).map(({ type }) => type)}
|
|
177
188
|
>
|
|
178
|
-
{markdownStyles.map(({
|
|
189
|
+
{markdownStyles.map(({ type, getState, Icon }) => (
|
|
179
190
|
<ToolbarButton
|
|
180
|
-
key={
|
|
181
|
-
value={
|
|
191
|
+
key={type}
|
|
192
|
+
value={type}
|
|
182
193
|
Icon={Icon}
|
|
183
194
|
disabled={state?.blockType === 'codeblock'}
|
|
184
|
-
onClick={() => onAction?.({ type
|
|
195
|
+
onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
185
196
|
>
|
|
186
|
-
{t(`${
|
|
197
|
+
{t(`${type} label`)}
|
|
187
198
|
</ToolbarButton>
|
|
188
199
|
))}
|
|
189
200
|
</NaturalToolbar.ToggleGroup>
|
|
190
201
|
);
|
|
191
202
|
};
|
|
192
203
|
|
|
193
|
-
const markdownLists = [
|
|
194
|
-
{
|
|
195
|
-
{
|
|
196
|
-
{
|
|
204
|
+
const markdownLists: ButtonProps[] = [
|
|
205
|
+
{ type: 'list-bullet', Icon: ListBullets, getState: (state) => state.listStyle === 'bullet' },
|
|
206
|
+
{ type: 'list-ordered', Icon: ListNumbers, getState: (state) => state.listStyle === 'ordered' },
|
|
207
|
+
{ type: 'list-task', Icon: ListChecks, getState: (state) => state.listStyle === 'task' },
|
|
197
208
|
];
|
|
198
209
|
|
|
199
210
|
const MarkdownLists = () => {
|
|
@@ -201,62 +212,58 @@ const MarkdownLists = () => {
|
|
|
201
212
|
const { t } = useTranslation(translationKey);
|
|
202
213
|
return (
|
|
203
214
|
<NaturalToolbar.ToggleGroup type='single' value={state?.listStyle ?? undefined}>
|
|
204
|
-
{markdownLists.map(({
|
|
215
|
+
{markdownLists.map(({ type, getState, Icon }) => (
|
|
205
216
|
<ToolbarButton
|
|
206
|
-
key={
|
|
207
|
-
value={
|
|
217
|
+
key={type}
|
|
218
|
+
value={type}
|
|
208
219
|
Icon={Icon}
|
|
209
|
-
onClick={() => onAction?.({ type
|
|
220
|
+
onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
210
221
|
>
|
|
211
|
-
{t(`${
|
|
222
|
+
{t(`${type} label`)}
|
|
212
223
|
</ToolbarButton>
|
|
213
224
|
))}
|
|
214
225
|
</NaturalToolbar.ToggleGroup>
|
|
215
226
|
);
|
|
216
227
|
};
|
|
217
228
|
|
|
218
|
-
const markdownBlocks = [
|
|
229
|
+
const markdownBlocks: ButtonProps[] = [
|
|
219
230
|
{
|
|
220
|
-
|
|
231
|
+
type: 'blockquote',
|
|
221
232
|
Icon: Quotes,
|
|
222
|
-
|
|
223
|
-
action: (state: Formatting | null) => ({ type: 'blockquote' as const, data: state ? !state.blockQuote : null }),
|
|
224
|
-
disabled: (state: Formatting | null) => false,
|
|
233
|
+
getState: (state) => state.blockQuote,
|
|
225
234
|
},
|
|
226
235
|
{
|
|
227
|
-
|
|
236
|
+
type: 'codeblock',
|
|
228
237
|
Icon: CodeBlock,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
238
|
+
getState: (state) => state.blockQuote,
|
|
239
|
+
disabled: (state) => !state.blankLine,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: 'table',
|
|
243
|
+
Icon: Table,
|
|
244
|
+
getState: (state) => state.blockType === 'tablecell',
|
|
245
|
+
disabled: (state) => !state.blankLine,
|
|
232
246
|
},
|
|
233
247
|
];
|
|
234
248
|
|
|
235
249
|
const MarkdownBlocks = () => {
|
|
236
250
|
const { onAction, state } = useToolbarContext('MarkdownStyles');
|
|
237
251
|
const { t } = useTranslation(translationKey);
|
|
252
|
+
const value = markdownBlocks.find(({ getState }) => state && getState(state));
|
|
238
253
|
return (
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
disabled={!state?.blankLine}
|
|
253
|
-
onClick={() => onAction?.({ type: 'table' })}
|
|
254
|
-
classNames={buttonStyles}
|
|
255
|
-
>
|
|
256
|
-
<Table className={iconStyles} />
|
|
257
|
-
<span className='sr-only'>{t('table label')}</span>
|
|
258
|
-
</NaturalToolbar.Button>
|
|
259
|
-
</>
|
|
254
|
+
<NaturalToolbar.ToggleGroup type='single' value={value?.type}>
|
|
255
|
+
{markdownBlocks.map(({ type, disabled, getState, Icon }) => (
|
|
256
|
+
<ToolbarButton
|
|
257
|
+
key={type}
|
|
258
|
+
value={type}
|
|
259
|
+
Icon={Icon}
|
|
260
|
+
disabled={!state || disabled?.(state)}
|
|
261
|
+
onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
262
|
+
>
|
|
263
|
+
{t(`${type} label`)}
|
|
264
|
+
</ToolbarButton>
|
|
265
|
+
))}
|
|
266
|
+
</NaturalToolbar.ToggleGroup>
|
|
260
267
|
);
|
|
261
268
|
};
|
|
262
269
|
|
|
@@ -273,7 +280,12 @@ const MarkdownExtended = () => {
|
|
|
273
280
|
const { onAction } = useToolbarContext('MarkdownStyles');
|
|
274
281
|
const { t } = useTranslation(translationKey);
|
|
275
282
|
return (
|
|
276
|
-
<NaturalToolbar.Button
|
|
283
|
+
<NaturalToolbar.Button
|
|
284
|
+
variant='ghost'
|
|
285
|
+
data-testid='editor.toolbar.comment'
|
|
286
|
+
onClick={() => onAction?.({ type: 'comment' })}
|
|
287
|
+
classNames={buttonStyles}
|
|
288
|
+
>
|
|
277
289
|
<ChatText className={iconStyles} />
|
|
278
290
|
<span className='sr-only'>{t('comment label')}</span>
|
|
279
291
|
</NaturalToolbar.Button>
|
|
@@ -36,8 +36,11 @@ const styles = EditorView.baseTheme({
|
|
|
36
36
|
},
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
const commentMark = Decoration.mark({ class: 'cm-comment' });
|
|
40
|
-
const commentCurrentMark = Decoration.mark({
|
|
39
|
+
const commentMark = Decoration.mark({ class: 'cm-comment', attributes: { 'data-testid': 'cm-comment' } });
|
|
40
|
+
const commentCurrentMark = Decoration.mark({
|
|
41
|
+
class: 'cm-comment-current',
|
|
42
|
+
attributes: { 'data-testid': 'cm-comment' },
|
|
43
|
+
});
|
|
41
44
|
|
|
42
45
|
type CommentState = {
|
|
43
46
|
comment: Comment;
|
package/src/extensions/index.ts
CHANGED
|
@@ -370,7 +370,7 @@ describe('removeBlockquote', () => {
|
|
|
370
370
|
});
|
|
371
371
|
|
|
372
372
|
describe('addCodeblock', () => {
|
|
373
|
-
testCommand('add a code block to a blank line', '{}', addCodeblock, '```{}\n
|
|
373
|
+
testCommand('add a code block to a blank line', '{}', addCodeblock, '```{}\n```');
|
|
374
374
|
|
|
375
375
|
testCommand(
|
|
376
376
|
'can turn a paragraph into a code block',
|
|
@@ -356,7 +356,6 @@ const snippets = {
|
|
|
356
356
|
[
|
|
357
357
|
//
|
|
358
358
|
'```#{}',
|
|
359
|
-
'#{}',
|
|
360
359
|
'```',
|
|
361
360
|
].join('\n'),
|
|
362
361
|
),
|
|
@@ -515,6 +514,7 @@ export const addLink: StateCommand = ({ state, dispatch }) => {
|
|
|
515
514
|
// Lists
|
|
516
515
|
//
|
|
517
516
|
|
|
517
|
+
// TODO(burdon): Cursor is positioned incorrectly.
|
|
518
518
|
export const addList =
|
|
519
519
|
(type: List): StateCommand =>
|
|
520
520
|
({ state, dispatch }) => {
|
|
@@ -576,18 +576,21 @@ export const addList =
|
|
|
576
576
|
// Insert a new list item if the selection is empty.
|
|
577
577
|
const { from, to } = state.doc.lineAt(state.selection.main.anchor);
|
|
578
578
|
if (from === to) {
|
|
579
|
+
const insert = type === List.Bullet ? '- ' : type === List.Ordered ? '1. ' : '- [ ] ';
|
|
579
580
|
dispatch(
|
|
580
581
|
state.update({
|
|
581
582
|
changes: [
|
|
582
583
|
{
|
|
583
584
|
from,
|
|
584
|
-
insert
|
|
585
|
+
insert,
|
|
585
586
|
},
|
|
586
587
|
],
|
|
588
|
+
selection: { anchor: from + insert.length },
|
|
587
589
|
userEvent: 'format.list.add',
|
|
588
590
|
scrollIntoView: true,
|
|
589
591
|
}),
|
|
590
592
|
);
|
|
593
|
+
|
|
591
594
|
return true;
|
|
592
595
|
}
|
|
593
596
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, Facet } from '@codemirror/state';
|
|
6
|
+
import { keymap } from '@codemirror/view';
|
|
7
|
+
import { vim } from '@replit/codemirror-vim';
|
|
8
|
+
|
|
9
|
+
export type EditorMode = 'default' | 'vim' | undefined;
|
|
10
|
+
|
|
11
|
+
export type EditorConfig = {
|
|
12
|
+
type: string;
|
|
13
|
+
noTabster?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const editorMode = Facet.define<EditorConfig, EditorConfig>({
|
|
17
|
+
combine: (modes) => modes[0] ?? {},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const EditorModes: { [mode: string]: Extension } = {
|
|
21
|
+
default: [],
|
|
22
|
+
vim: [
|
|
23
|
+
vim(),
|
|
24
|
+
editorMode.of({ type: 'vim', noTabster: true }),
|
|
25
|
+
keymap.of([
|
|
26
|
+
{
|
|
27
|
+
key: 'Alt-Escape',
|
|
28
|
+
run: (view) => {
|
|
29
|
+
// Focus container for tab navigation.
|
|
30
|
+
view.dispatch({ userEvent: 'focus.container' });
|
|
31
|
+
return true;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
]),
|
|
35
|
+
],
|
|
36
|
+
};
|
|
@@ -54,7 +54,7 @@ export const useActionHandler = (view?: EditorView | null): ToolbarProps['onActi
|
|
|
54
54
|
|
|
55
55
|
case 'list-ordered':
|
|
56
56
|
case 'list-bullet':
|
|
57
|
-
case 'list-
|
|
57
|
+
case 'list-task':
|
|
58
58
|
listType =
|
|
59
59
|
action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
|
|
60
60
|
(action.data === false
|
package/src/index.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
export { type Extension, type EditorState } from '@codemirror/state';
|
|
6
|
+
export { type EditorView, keymap } from '@codemirror/view';
|
|
7
|
+
|
|
5
8
|
export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
|
|
6
9
|
export { Doc, YText, YXmlFragment } from '@dxos/text-model';
|
|
7
|
-
export { type Extension } from '@codemirror/state';
|
|
8
|
-
export { type EditorView, keymap } from '@codemirror/view';
|
|
9
10
|
|
|
10
11
|
export * from './components';
|
|
11
12
|
export * from './extensions';
|