@dxos/react-ui-editor 0.4.8-main.c67d72c → 0.4.8-main.cd60bd2
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 +1926 -1743
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +2 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +12 -11
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +4 -2
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -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/dnd.d.ts +7 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +5 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts +9 -0
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +7 -4
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +6 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +2 -1
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/extensions/state.d.ts +20 -0
- package/dist/types/src/extensions/state.d.ts.map +1 -0
- package/dist/types/src/hooks/useActionHandler.d.ts +2 -2
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useDocAccessor.d.ts +4 -2
- package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts +1 -0
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +26 -24
- package/src/components/TextEditor/TextEditor.stories.tsx +30 -6
- package/src/components/TextEditor/TextEditor.tsx +13 -12
- package/src/components/Toolbar/Toolbar.stories.tsx +23 -24
- package/src/components/Toolbar/Toolbar.tsx +102 -60
- package/src/extensions/automerge/automerge.ts +4 -4
- package/src/extensions/command/state.ts +2 -2
- package/src/extensions/comments.ts +2 -2
- package/src/extensions/dnd.ts +31 -9
- package/src/extensions/factories.ts +41 -15
- package/src/extensions/index.ts +1 -1
- package/src/extensions/markdown/action.ts +112 -0
- package/src/extensions/markdown/bundle.ts +8 -11
- package/src/extensions/markdown/formatting.test.ts +13 -13
- package/src/extensions/markdown/formatting.ts +95 -87
- package/src/extensions/markdown/image.ts +6 -0
- package/src/extensions/markdown/index.ts +1 -0
- package/src/extensions/modes.ts +11 -2
- package/src/extensions/state.ts +90 -0
- package/src/hooks/useActionHandler.ts +4 -85
- package/src/hooks/useDocAccessor.ts +8 -2
- package/src/hooks/useTextEditor.stories.tsx +3 -3
- package/src/hooks/useTextEditor.ts +3 -0
- package/src/themes/default.ts +24 -8
- package/src/translations.ts +1 -0
- package/dist/types/src/extensions/cursor-line-margin.d.ts +0 -7
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +0 -1
- package/src/extensions/cursor-line-margin.ts +0 -94
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
ChatText,
|
|
8
8
|
Code,
|
|
9
9
|
CodeBlock,
|
|
10
|
+
Image,
|
|
10
11
|
Link,
|
|
11
12
|
ListBullets,
|
|
12
13
|
ListChecks,
|
|
13
14
|
ListNumbers,
|
|
14
15
|
Paragraph,
|
|
16
|
+
Quotes,
|
|
15
17
|
TextStrikethrough,
|
|
16
18
|
Table,
|
|
17
19
|
TextB,
|
|
@@ -22,24 +24,25 @@ import {
|
|
|
22
24
|
TextHFive,
|
|
23
25
|
TextHSix,
|
|
24
26
|
TextItalic,
|
|
25
|
-
Quotes,
|
|
26
27
|
} from '@phosphor-icons/react';
|
|
27
28
|
import { createContext } from '@radix-ui/react-context';
|
|
28
|
-
import React, { type PropsWithChildren, useRef, useState } from 'react';
|
|
29
|
+
import React, { type PropsWithChildren, useEffect, useRef, useState } from 'react';
|
|
30
|
+
import { useDropzone } from 'react-dropzone';
|
|
29
31
|
|
|
30
32
|
import {
|
|
31
33
|
DensityProvider,
|
|
32
34
|
ElevationProvider,
|
|
33
35
|
Select,
|
|
34
|
-
type ThemedClassName,
|
|
35
36
|
Toolbar as NaturalToolbar,
|
|
36
37
|
Tooltip,
|
|
37
|
-
type
|
|
38
|
+
type ThemedClassName,
|
|
39
|
+
type ToolbarToggleGroupItemProps as NaturalToolbarToggleGroupItemProps,
|
|
40
|
+
type ToolbarButtonProps as NaturalToolbarButtonProps,
|
|
38
41
|
useTranslation,
|
|
39
42
|
} from '@dxos/react-ui';
|
|
40
43
|
import { getSize } from '@dxos/react-ui-theme';
|
|
41
44
|
|
|
42
|
-
import { type Formatting } from '../../extensions';
|
|
45
|
+
import { type Action, type ActionType, type Formatting } from '../../extensions';
|
|
43
46
|
import { translationKey } from '../../translations';
|
|
44
47
|
|
|
45
48
|
const tooltipProps = { side: 'top' as const, classNames: 'z-10' };
|
|
@@ -54,33 +57,9 @@ const HeadingIcons: { [key: string]: Icon } = {
|
|
|
54
57
|
'6': TextHSix,
|
|
55
58
|
};
|
|
56
59
|
|
|
57
|
-
// TODO(burdon): Revert to string to make extensible?
|
|
58
|
-
export type ActionType =
|
|
59
|
-
| 'blockquote'
|
|
60
|
-
| 'strong'
|
|
61
|
-
| 'codeblock'
|
|
62
|
-
| 'comment'
|
|
63
|
-
| 'heading'
|
|
64
|
-
| 'image'
|
|
65
|
-
| 'emphasis'
|
|
66
|
-
| 'code'
|
|
67
|
-
| 'link'
|
|
68
|
-
| 'list-bullet'
|
|
69
|
-
| 'list-ordered'
|
|
70
|
-
| 'list-task'
|
|
71
|
-
| 'mention'
|
|
72
|
-
| 'prompt'
|
|
73
|
-
| 'strikethrough'
|
|
74
|
-
| 'table';
|
|
75
|
-
|
|
76
|
-
export type Action = {
|
|
77
|
-
type: ActionType;
|
|
78
|
-
data?: any;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
60
|
export type ToolbarProps = ThemedClassName<
|
|
82
61
|
PropsWithChildren<{
|
|
83
|
-
state: Formatting |
|
|
62
|
+
state: Formatting | undefined;
|
|
84
63
|
onAction?: (action: Action) => void;
|
|
85
64
|
}>
|
|
86
65
|
>;
|
|
@@ -111,9 +90,9 @@ type ButtonProps = {
|
|
|
111
90
|
disabled?: (state: Formatting) => boolean;
|
|
112
91
|
};
|
|
113
92
|
|
|
114
|
-
type
|
|
93
|
+
type ToolbarToggleButtonProps = NaturalToolbarToggleGroupItemProps & { Icon: Icon };
|
|
115
94
|
|
|
116
|
-
const
|
|
95
|
+
const ToolbarToggleButton = ({ Icon, children, ...props }: ToolbarToggleButtonProps) => {
|
|
117
96
|
return (
|
|
118
97
|
<Tooltip.Root>
|
|
119
98
|
<Tooltip.Trigger asChild>
|
|
@@ -132,6 +111,27 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
|
132
111
|
);
|
|
133
112
|
};
|
|
134
113
|
|
|
114
|
+
type ToolbarButtonProps = NaturalToolbarButtonProps & { Icon: Icon };
|
|
115
|
+
|
|
116
|
+
const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
117
|
+
return (
|
|
118
|
+
<Tooltip.Root>
|
|
119
|
+
<Tooltip.Trigger asChild>
|
|
120
|
+
<NaturalToolbar.Button variant='ghost' {...props} classNames={buttonStyles}>
|
|
121
|
+
<Icon className={iconStyles} />
|
|
122
|
+
<span className='sr-only'>{children}</span>
|
|
123
|
+
</NaturalToolbar.Button>
|
|
124
|
+
</Tooltip.Trigger>
|
|
125
|
+
<Tooltip.Portal>
|
|
126
|
+
<Tooltip.Content {...tooltipProps}>
|
|
127
|
+
{children}
|
|
128
|
+
<Tooltip.Arrow />
|
|
129
|
+
</Tooltip.Content>
|
|
130
|
+
</Tooltip.Portal>
|
|
131
|
+
</Tooltip.Root>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
135
|
const ToolbarSeparator = () => <div role='separator' className='grow' />;
|
|
136
136
|
|
|
137
137
|
const MarkdownHeading = () => {
|
|
@@ -227,7 +227,7 @@ const MarkdownStyles = () => {
|
|
|
227
227
|
value={markdownStyles.filter(({ getState }) => state && getState(state)).map(({ type }) => type)}
|
|
228
228
|
>
|
|
229
229
|
{markdownStyles.map(({ type, getState, Icon }) => (
|
|
230
|
-
<
|
|
230
|
+
<ToolbarToggleButton
|
|
231
231
|
key={type}
|
|
232
232
|
value={type}
|
|
233
233
|
Icon={Icon}
|
|
@@ -235,7 +235,7 @@ const MarkdownStyles = () => {
|
|
|
235
235
|
onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
236
236
|
>
|
|
237
237
|
{t(`${type} label`)}
|
|
238
|
-
</
|
|
238
|
+
</ToolbarToggleButton>
|
|
239
239
|
))}
|
|
240
240
|
</NaturalToolbar.ToggleGroup>
|
|
241
241
|
);
|
|
@@ -253,14 +253,14 @@ const MarkdownLists = () => {
|
|
|
253
253
|
return (
|
|
254
254
|
<NaturalToolbar.ToggleGroup type='single' value={state?.listStyle ? `list-${state.listStyle}` : ''}>
|
|
255
255
|
{markdownLists.map(({ type, getState, Icon }) => (
|
|
256
|
-
<
|
|
256
|
+
<ToolbarToggleButton
|
|
257
257
|
key={type}
|
|
258
258
|
value={type}
|
|
259
259
|
Icon={Icon}
|
|
260
260
|
onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
261
261
|
>
|
|
262
262
|
{t(`${type} label`)}
|
|
263
|
-
</
|
|
263
|
+
</ToolbarToggleButton>
|
|
264
264
|
))}
|
|
265
265
|
</NaturalToolbar.ToggleGroup>
|
|
266
266
|
);
|
|
@@ -292,7 +292,7 @@ const MarkdownBlocks = () => {
|
|
|
292
292
|
return (
|
|
293
293
|
<NaturalToolbar.ToggleGroup type='single' value={value?.type ?? ''}>
|
|
294
294
|
{markdownBlocks.map(({ type, disabled, getState, Icon }) => (
|
|
295
|
-
<
|
|
295
|
+
<ToolbarToggleButton
|
|
296
296
|
key={type}
|
|
297
297
|
value={type}
|
|
298
298
|
Icon={Icon}
|
|
@@ -300,7 +300,7 @@ const MarkdownBlocks = () => {
|
|
|
300
300
|
onClick={state ? () => onAction?.({ type, data: !getState(state) }) : undefined}
|
|
301
301
|
>
|
|
302
302
|
{t(`${type} label`)}
|
|
303
|
-
</
|
|
303
|
+
</ToolbarToggleButton>
|
|
304
304
|
))}
|
|
305
305
|
</NaturalToolbar.ToggleGroup>
|
|
306
306
|
);
|
|
@@ -315,38 +315,80 @@ const MarkdownStandard = () => (
|
|
|
315
315
|
</>
|
|
316
316
|
);
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
// TODO(burdon): Make extensible.
|
|
319
|
+
export type MarkdownCustomOptions = {
|
|
320
|
+
onUpload?: (file: File) => Promise<{ url?: string }>;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
|
|
319
324
|
const { onAction } = useToolbarContext('MarkdownStyles');
|
|
320
325
|
const { t } = useTranslation(translationKey);
|
|
326
|
+
// https://react-dropzone.js.org/#src
|
|
327
|
+
const { acceptedFiles, getInputProps, open } = useDropzone({
|
|
328
|
+
multiple: false,
|
|
329
|
+
noDrag: true,
|
|
330
|
+
accept: {
|
|
331
|
+
'image/*': ['.jpg', '.jpeg', '.png', '.gif'],
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
if (onUpload && acceptedFiles.length) {
|
|
337
|
+
requestAnimationFrame(async () => {
|
|
338
|
+
// NOTE: Clone file since react-dropzone patches in a non-standard `path` property, which confuses IPFS.
|
|
339
|
+
const f = acceptedFiles[0];
|
|
340
|
+
const file = new File([f], f.name, {
|
|
341
|
+
type: f.type,
|
|
342
|
+
lastModified: f.lastModified,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const { url } = await onUpload(file);
|
|
346
|
+
if (url) {
|
|
347
|
+
onAction?.({ type: 'image', data: url });
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}, [acceptedFiles]);
|
|
352
|
+
|
|
321
353
|
return (
|
|
322
|
-
|
|
323
|
-
<
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
354
|
+
<>
|
|
355
|
+
<input {...getInputProps()} />
|
|
356
|
+
<ToolbarButton value='image' Icon={Image} onClick={() => open()}>
|
|
357
|
+
{t('image label')}
|
|
358
|
+
</ToolbarButton>
|
|
359
|
+
</>
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// TODO(burdon): Make extensible.
|
|
364
|
+
const MarkdownActions = () => {
|
|
365
|
+
const { onAction } = useToolbarContext('MarkdownStyles');
|
|
366
|
+
const { t } = useTranslation(translationKey);
|
|
367
|
+
return (
|
|
368
|
+
<>
|
|
369
|
+
{/* TODO(burdon): Toggle readonly state. */}
|
|
370
|
+
{/* <ToolbarButton value='comment' Icon={BookOpenText} onClick={() => onAction?.({ type: 'comment' })}> */}
|
|
371
|
+
{/* {t('comment label')} */}
|
|
372
|
+
{/* </ToolbarButton> */}
|
|
373
|
+
<ToolbarButton
|
|
374
|
+
value='comment'
|
|
375
|
+
Icon={ChatText}
|
|
376
|
+
data-testid='editor.toolbar.comment'
|
|
377
|
+
onClick={() => onAction?.({ type: 'comment' })}
|
|
378
|
+
>
|
|
379
|
+
{t('comment label')}
|
|
380
|
+
</ToolbarButton>
|
|
381
|
+
</>
|
|
341
382
|
);
|
|
342
383
|
};
|
|
343
384
|
|
|
344
385
|
export const Toolbar = {
|
|
345
386
|
Root: ToolbarRoot,
|
|
346
|
-
Button:
|
|
387
|
+
Button: ToolbarToggleButton,
|
|
347
388
|
Separator: ToolbarSeparator,
|
|
348
389
|
Markdown: MarkdownStandard,
|
|
349
|
-
|
|
390
|
+
Custom: MarkdownCustom,
|
|
391
|
+
Actions: MarkdownActions,
|
|
350
392
|
};
|
|
351
393
|
|
|
352
394
|
export { useToolbarContext };
|
|
@@ -60,16 +60,16 @@ export const automerge = (accessor: DocAccessor): Extension => {
|
|
|
60
60
|
ViewPlugin.fromClass(
|
|
61
61
|
class {
|
|
62
62
|
constructor(private readonly _view: EditorView) {
|
|
63
|
-
accessor.handle.addListener('change', this._handleChange
|
|
63
|
+
accessor.handle.addListener('change', this._handleChange);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
destroy() {
|
|
67
|
-
accessor.handle.removeListener('change', this._handleChange
|
|
67
|
+
accessor.handle.removeListener('change', this._handleChange);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
_handleChange() {
|
|
70
|
+
readonly _handleChange = () => {
|
|
71
71
|
syncer.reconcile(this._view, false);
|
|
72
|
-
}
|
|
72
|
+
};
|
|
73
73
|
},
|
|
74
74
|
),
|
|
75
75
|
|
|
@@ -59,8 +59,8 @@ export const commandState = StateField.define<CommandState>({
|
|
|
59
59
|
selection: { anchor: pos + action.insert.length },
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
-
//
|
|
63
|
-
|
|
62
|
+
// NOTE: Truncates text if set focus immediately.
|
|
63
|
+
requestAnimationFrame(() => view.focus());
|
|
64
64
|
});
|
|
65
65
|
},
|
|
66
66
|
};
|
|
@@ -508,14 +508,14 @@ export const focusComment = (view: EditorView, id: string, center = true) => {
|
|
|
508
508
|
/**
|
|
509
509
|
* Update comments state field.
|
|
510
510
|
*/
|
|
511
|
-
export const useComments = (view: EditorView | null | undefined, id: string, comments
|
|
511
|
+
export const useComments = (view: EditorView | null | undefined, id: string, comments?: Comment[]) => {
|
|
512
512
|
useEffect(() => {
|
|
513
513
|
if (view) {
|
|
514
514
|
// Check same document.
|
|
515
515
|
// NOTE: Hook might be called before editor state is updated.
|
|
516
516
|
if (id === view.state.facet(documentId)) {
|
|
517
517
|
view.dispatch({
|
|
518
|
-
effects: setComments.of({ id, comments }),
|
|
518
|
+
effects: setComments.of({ id, comments: comments ?? [] }),
|
|
519
519
|
});
|
|
520
520
|
}
|
|
521
521
|
}
|
package/src/extensions/dnd.ts
CHANGED
|
@@ -3,15 +3,37 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import type { Extension } from '@codemirror/state';
|
|
6
|
-
import { EditorView } from '@codemirror/view';
|
|
6
|
+
import { dropCursor, EditorView } from '@codemirror/view';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { getToken } from '../styles';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
export type DNDOptions = { onDrop?: (view: EditorView, event: { files: FileList }) => void };
|
|
11
|
+
|
|
12
|
+
const styles = EditorView.baseTheme({
|
|
13
|
+
'.cm-dropCursor': {
|
|
14
|
+
borderLeft: `2px solid ${getToken('extend.colors.primary.500')}`,
|
|
15
|
+
color: getToken('extend.colors.primary.500'),
|
|
16
|
+
padding: '0 4px',
|
|
17
|
+
},
|
|
18
|
+
'.cm-dropCursor:after': {
|
|
19
|
+
content: '"←"',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const dropFile = (options: DNDOptions = {}): Extension => {
|
|
24
|
+
return [
|
|
25
|
+
styles,
|
|
26
|
+
dropCursor(),
|
|
27
|
+
EditorView.domEventHandlers({
|
|
28
|
+
drop: (event, view) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
const files = event.dataTransfer?.files;
|
|
31
|
+
const pos = view.posAtCoords(event);
|
|
32
|
+
if (files?.length && pos !== null) {
|
|
33
|
+
view.dispatch({ selection: { anchor: pos } });
|
|
34
|
+
options.onDrop?.(view, { files });
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
];
|
|
17
39
|
};
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { closeBrackets } from '@codemirror/autocomplete';
|
|
6
|
-
import { history } from '@codemirror/commands';
|
|
5
|
+
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
|
|
7
7
|
import { bracketMatching } from '@codemirror/language';
|
|
8
|
+
import { searchKeymap } from '@codemirror/search';
|
|
8
9
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
9
10
|
import {
|
|
10
11
|
EditorView,
|
|
11
|
-
|
|
12
|
+
type KeyBinding,
|
|
12
13
|
drawSelection,
|
|
13
14
|
dropCursor,
|
|
14
15
|
highlightActiveLine,
|
|
16
|
+
keymap,
|
|
15
17
|
lineNumbers,
|
|
16
18
|
placeholder,
|
|
17
19
|
scrollPastEnd,
|
|
@@ -32,8 +34,6 @@ import { awareness, SpaceAwarenessProvider } from './awareness';
|
|
|
32
34
|
import { type ThemeStyles } from '../styles';
|
|
33
35
|
import { defaultTheme } from '../themes';
|
|
34
36
|
|
|
35
|
-
// TODO(burdon): Move into extensions folder.
|
|
36
|
-
|
|
37
37
|
//
|
|
38
38
|
// Basic
|
|
39
39
|
//
|
|
@@ -41,39 +41,52 @@ import { defaultTheme } from '../themes';
|
|
|
41
41
|
/**
|
|
42
42
|
* https://codemirror.net/docs/extensions
|
|
43
43
|
* https://github.com/codemirror/basic-setup
|
|
44
|
+
* https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
|
44
45
|
*/
|
|
45
|
-
// TODO(burdon): Reconcile with createMarkdownExtensions.
|
|
46
46
|
export type BasicExtensionsOptions = {
|
|
47
47
|
allowMultipleSelections?: boolean;
|
|
48
48
|
bracketMatching?: boolean;
|
|
49
49
|
closeBrackets?: boolean;
|
|
50
|
-
crosshairCursor?: boolean;
|
|
51
50
|
dropCursor?: boolean;
|
|
52
51
|
drawSelection?: boolean;
|
|
53
52
|
editable?: boolean;
|
|
54
53
|
highlightActiveLine?: boolean;
|
|
55
54
|
history?: boolean;
|
|
55
|
+
indentWithTab?: boolean;
|
|
56
|
+
keymap?: null | 'default' | 'standard';
|
|
56
57
|
lineNumbers?: boolean;
|
|
57
58
|
lineWrapping?: boolean;
|
|
58
59
|
placeholder?: string;
|
|
59
60
|
readonly?: boolean;
|
|
61
|
+
search?: boolean;
|
|
60
62
|
scrollPastEnd?: boolean;
|
|
63
|
+
standardKeymap?: boolean;
|
|
61
64
|
tabSize?: number;
|
|
62
65
|
};
|
|
63
66
|
|
|
64
|
-
const
|
|
67
|
+
const defaultBasicOptions: BasicExtensionsOptions = {
|
|
68
|
+
allowMultipleSelections: true,
|
|
65
69
|
bracketMatching: true,
|
|
66
70
|
closeBrackets: true,
|
|
67
71
|
drawSelection: true,
|
|
68
72
|
editable: true,
|
|
69
73
|
history: true,
|
|
74
|
+
keymap: 'standard',
|
|
70
75
|
lineWrapping: true,
|
|
76
|
+
search: true,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const keymaps: { [key: string]: readonly KeyBinding[] } = {
|
|
80
|
+
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
81
|
+
standard: standardKeymap,
|
|
82
|
+
// https://codemirror.net/docs/ref/#commands.defaultKeymap
|
|
83
|
+
default: defaultKeymap,
|
|
71
84
|
};
|
|
72
85
|
|
|
73
86
|
export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
|
|
74
|
-
const props: BasicExtensionsOptions = defaultsDeep({}, _props,
|
|
87
|
+
const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaultBasicOptions);
|
|
75
88
|
return [
|
|
76
|
-
//
|
|
89
|
+
// NOTE: Doesn't catch errors in keymap functions.
|
|
77
90
|
EditorView.exceptionSink.of((err) => {
|
|
78
91
|
log.catch(err);
|
|
79
92
|
}),
|
|
@@ -81,7 +94,6 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
81
94
|
props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
|
|
82
95
|
props.bracketMatching && bracketMatching(),
|
|
83
96
|
props.closeBrackets && closeBrackets(),
|
|
84
|
-
props.crosshairCursor && crosshairCursor(),
|
|
85
97
|
props.dropCursor && dropCursor(),
|
|
86
98
|
props.drawSelection && drawSelection(),
|
|
87
99
|
props.highlightActiveLine && highlightActiveLine(),
|
|
@@ -92,6 +104,21 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
92
104
|
props.readonly && [EditorState.readOnly.of(true), EditorView.editable.of(false)],
|
|
93
105
|
props.scrollPastEnd && scrollPastEnd(),
|
|
94
106
|
props.tabSize && EditorState.tabSize.of(props.tabSize),
|
|
107
|
+
|
|
108
|
+
// https://codemirror.net/docs/ref/#view.KeyBinding
|
|
109
|
+
keymap.of(
|
|
110
|
+
[
|
|
111
|
+
...((props.keymap && keymaps[props.keymap]) ?? []),
|
|
112
|
+
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
113
|
+
...(props.indentWithTab ? [indentWithTab] : []),
|
|
114
|
+
// https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
|
|
115
|
+
...(props.closeBrackets ? closeBracketsKeymap : []),
|
|
116
|
+
// https://codemirror.net/docs/ref/#commands.historyKeymap
|
|
117
|
+
...(props.history ? historyKeymap : []),
|
|
118
|
+
// https://codemirror.net/docs/ref/#search.searchKeymap
|
|
119
|
+
...(props.search ? searchKeymap : []),
|
|
120
|
+
].filter(isNotFalsy),
|
|
121
|
+
),
|
|
95
122
|
].filter(isNotFalsy);
|
|
96
123
|
};
|
|
97
124
|
|
|
@@ -112,14 +139,14 @@ export type ThemeExtensionsOptions = {
|
|
|
112
139
|
};
|
|
113
140
|
};
|
|
114
141
|
|
|
115
|
-
const
|
|
142
|
+
const defaultThemeSlots = {
|
|
116
143
|
editor: {
|
|
117
144
|
className: 'w-full bs-full',
|
|
118
145
|
},
|
|
119
146
|
};
|
|
120
147
|
|
|
121
148
|
export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
|
|
122
|
-
const slots = defaultsDeep({}, _slots,
|
|
149
|
+
const slots = defaultsDeep({}, _slots, defaultThemeSlots);
|
|
123
150
|
return [
|
|
124
151
|
EditorView.baseTheme(defaultTheme),
|
|
125
152
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
@@ -135,12 +162,11 @@ export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: Theme
|
|
|
135
162
|
|
|
136
163
|
export type DataExtensionsProps = {
|
|
137
164
|
id: string;
|
|
138
|
-
text: DocAccessor;
|
|
165
|
+
text: DocAccessor;
|
|
139
166
|
space?: Space;
|
|
140
167
|
identity?: Identity | null;
|
|
141
168
|
};
|
|
142
169
|
|
|
143
|
-
// TODO(burdon): Factor out automerge defs and extension (not hook).
|
|
144
170
|
// TODO(burdon): Move out of react-ui-editor (remove echo deps).
|
|
145
171
|
export const createDataExtensions = ({ id, text, space, identity }: DataExtensionsProps): Extension[] => {
|
|
146
172
|
const extensions: Extension[] = [automerge(text)];
|
package/src/extensions/index.ts
CHANGED
|
@@ -10,7 +10,6 @@ export * from './blast';
|
|
|
10
10
|
export * from './command';
|
|
11
11
|
export * from './comments';
|
|
12
12
|
export * from './cursor';
|
|
13
|
-
export * from './cursor-line-margin';
|
|
14
13
|
export * from './doc';
|
|
15
14
|
export * from './dnd';
|
|
16
15
|
export * from './factories';
|
|
@@ -18,5 +17,6 @@ export * from './listener';
|
|
|
18
17
|
export * from './markdown';
|
|
19
18
|
export * from './mention';
|
|
20
19
|
export * from './modes';
|
|
20
|
+
export * from './state';
|
|
21
21
|
export * from './types';
|
|
22
22
|
export * from './typewriter';
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Inline,
|
|
9
|
+
List,
|
|
10
|
+
addBlockquote,
|
|
11
|
+
addCodeblock,
|
|
12
|
+
addLink,
|
|
13
|
+
addList,
|
|
14
|
+
insertTable,
|
|
15
|
+
removeBlockquote,
|
|
16
|
+
removeCodeblock,
|
|
17
|
+
removeLink,
|
|
18
|
+
removeList,
|
|
19
|
+
setHeading,
|
|
20
|
+
setStyle,
|
|
21
|
+
toggleBlockquote,
|
|
22
|
+
toggleList,
|
|
23
|
+
toggleStyle,
|
|
24
|
+
} from './formatting';
|
|
25
|
+
import { createComment } from '../comments';
|
|
26
|
+
|
|
27
|
+
export type ActionType =
|
|
28
|
+
| 'blockquote'
|
|
29
|
+
| 'strong'
|
|
30
|
+
| 'codeblock'
|
|
31
|
+
| 'comment'
|
|
32
|
+
| 'heading'
|
|
33
|
+
| 'image'
|
|
34
|
+
| 'emphasis'
|
|
35
|
+
| 'code'
|
|
36
|
+
| 'link'
|
|
37
|
+
| 'list-bullet'
|
|
38
|
+
| 'list-ordered'
|
|
39
|
+
| 'list-task'
|
|
40
|
+
| 'mention'
|
|
41
|
+
| 'prompt'
|
|
42
|
+
| 'strikethrough'
|
|
43
|
+
| 'table';
|
|
44
|
+
|
|
45
|
+
export type Action = {
|
|
46
|
+
type: ActionType;
|
|
47
|
+
data?: any;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ActionHandler = (view: EditorView, action: Action) => void;
|
|
51
|
+
|
|
52
|
+
export const processAction: ActionHandler = (view, action) => {
|
|
53
|
+
let inlineType, listType;
|
|
54
|
+
switch (action.type) {
|
|
55
|
+
case 'heading':
|
|
56
|
+
setHeading(parseInt(action.data))(view);
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case 'strong':
|
|
60
|
+
case 'emphasis':
|
|
61
|
+
case 'strikethrough':
|
|
62
|
+
case 'code':
|
|
63
|
+
inlineType =
|
|
64
|
+
action.type === 'strong'
|
|
65
|
+
? Inline.Strong
|
|
66
|
+
: action.type === 'emphasis'
|
|
67
|
+
? Inline.Emphasis
|
|
68
|
+
: action.type === 'strikethrough'
|
|
69
|
+
? Inline.Strikethrough
|
|
70
|
+
: Inline.Code;
|
|
71
|
+
(typeof action.data === 'boolean' ? setStyle(inlineType, action.data) : toggleStyle(inlineType))(view);
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 'list-ordered':
|
|
75
|
+
case 'list-bullet':
|
|
76
|
+
case 'list-task':
|
|
77
|
+
listType =
|
|
78
|
+
action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
|
|
79
|
+
(action.data === false ? removeList(listType) : action.data === true ? addList(listType) : toggleList(listType))(
|
|
80
|
+
view,
|
|
81
|
+
);
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'blockquote':
|
|
85
|
+
(action.data === false ? removeBlockquote : action.data === true ? addBlockquote : toggleBlockquote)(view);
|
|
86
|
+
break;
|
|
87
|
+
case 'codeblock':
|
|
88
|
+
(action.data === false ? removeCodeblock : addCodeblock)(view);
|
|
89
|
+
break;
|
|
90
|
+
case 'table':
|
|
91
|
+
insertTable(view);
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case 'link':
|
|
95
|
+
(action.data === false ? removeLink : addLink())(view);
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'image':
|
|
99
|
+
addLink({ url: action.data, image: true })(view);
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case 'comment':
|
|
103
|
+
createComment(view);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
requestAnimationFrame(() => {
|
|
108
|
+
if (!view.hasFocus) {
|
|
109
|
+
view.focus();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
};
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { completionKeymap } from '@codemirror/autocomplete';
|
|
6
|
+
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
|
7
7
|
import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
|
|
8
8
|
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
9
9
|
import { languages } from '@codemirror/language-data';
|
|
10
|
-
import {
|
|
10
|
+
import { lintKeymap } from '@codemirror/lint';
|
|
11
11
|
import { type Extension } from '@codemirror/state';
|
|
12
12
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
13
13
|
import { keymap } from '@codemirror/view';
|
|
@@ -60,14 +60,11 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
|
|
|
60
60
|
keymap.of([
|
|
61
61
|
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
62
62
|
indentWithTab,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
...
|
|
67
|
-
|
|
68
|
-
...searchKeymap,
|
|
69
|
-
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
70
|
-
...standardKeymap,
|
|
63
|
+
|
|
64
|
+
// https://codemirror.net/docs/ref/#commands.defaultKeymap
|
|
65
|
+
...defaultKeymap,
|
|
66
|
+
...completionKeymap,
|
|
67
|
+
...lintKeymap,
|
|
71
68
|
]),
|
|
72
69
|
];
|
|
73
70
|
};
|