@dxos/react-ui-editor 0.3.11-main.cb8120f → 0.3.11-main.ccc0ca3
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 +362 -45
- 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 +1 -2
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +23 -7
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +10 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/index.d.ts +2 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/basic.d.ts +1 -1
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +2 -2
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +2 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- 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/useEditorView.d.ts +17 -0
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +36 -14
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +26 -25
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +2 -3
- package/src/components/TextEditor/TextEditor.tsx +23 -13
- package/src/components/TextEditor/comments.stories.tsx +3 -3
- package/src/components/TextEditor/hooks.stories.tsx +106 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +7 -3
- package/src/components/Toolbar/Toolbar.tsx +120 -69
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +1 -0
- package/src/extensions/automerge/update-automerge.ts +7 -7
- package/src/extensions/basic.ts +15 -13
- package/src/extensions/comments.ts +2 -2
- package/src/extensions/markdown/bundle.ts +10 -5
- package/src/extensions/markdown/formatting.ts +31 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useActionHandler.ts +22 -5
- package/src/hooks/useEditorView.ts +29 -0
- package/src/hooks/useTextEditor.ts +143 -19
- package/src/themes/default.ts +3 -1
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import {
|
|
6
6
|
type Icon,
|
|
7
7
|
At,
|
|
8
|
-
Brain,
|
|
9
8
|
CaretRight,
|
|
10
9
|
ChatText,
|
|
11
10
|
Code,
|
|
@@ -19,15 +18,17 @@ import {
|
|
|
19
18
|
TextB,
|
|
20
19
|
TextItalic,
|
|
21
20
|
} from '@phosphor-icons/react';
|
|
22
|
-
import
|
|
21
|
+
import { createContext } from '@radix-ui/react-context';
|
|
22
|
+
import React, { type PropsWithChildren } from 'react';
|
|
23
23
|
|
|
24
24
|
import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
|
|
25
25
|
import { getSize } from '@dxos/react-ui-theme';
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// TODO(burdon): Revert to string to make extensible?
|
|
28
|
+
export type ActionType =
|
|
28
29
|
| 'blockquote'
|
|
29
30
|
| 'bold'
|
|
30
|
-
| '
|
|
31
|
+
| 'codeblock'
|
|
31
32
|
| 'comment'
|
|
32
33
|
| 'heading'
|
|
33
34
|
| 'image'
|
|
@@ -41,75 +42,125 @@ export type Action =
|
|
|
41
42
|
| 'strikethrough'
|
|
42
43
|
| 'table';
|
|
43
44
|
|
|
44
|
-
export type
|
|
45
|
-
|
|
45
|
+
export type Action = {
|
|
46
|
+
type: ActionType;
|
|
47
|
+
data?: any;
|
|
46
48
|
};
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<Button variant='ghost' classNames='p-2' onClick={onClick} title={title}>
|
|
54
|
-
<Icon className={getSize(5)} />
|
|
55
|
-
</Button>
|
|
56
|
-
);
|
|
50
|
+
export type ToolbarProps = PropsWithChildren<{
|
|
51
|
+
onAction?: (action: Action) => void;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
export const Toolbar = ({ onAction }: ToolbarProps) => {
|
|
56
|
+
const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
|
|
60
57
|
return (
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
<div>
|
|
64
|
-
|
|
65
|
-
defaultValue='0'
|
|
66
|
-
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
67
|
-
>
|
|
68
|
-
<Select.TriggerButton classNames='w-[8rem]' />
|
|
69
|
-
<Select.Portal>
|
|
70
|
-
<Select.Content>
|
|
71
|
-
<Select.Viewport>
|
|
72
|
-
<Select.Option value='0'>Paragraph</Select.Option>
|
|
73
|
-
{[1, 2, 3, 4, 5, 6].map((level) => (
|
|
74
|
-
<Select.Option key={level} value={String(level)}>
|
|
75
|
-
Heading {level}
|
|
76
|
-
</Select.Option>
|
|
77
|
-
))}
|
|
78
|
-
</Select.Viewport>
|
|
79
|
-
</Select.Content>
|
|
80
|
-
</Select.Portal>
|
|
81
|
-
</Select.Root>
|
|
82
|
-
</div>
|
|
83
|
-
<div>
|
|
84
|
-
<IconButton Icon={TextB} title='Bold' onClick={() => onAction?.({ type: 'bold' })} />
|
|
85
|
-
<IconButton Icon={TextItalic} title='Italic' onClick={() => onAction?.({ type: 'italic' })} />
|
|
86
|
-
<IconButton
|
|
87
|
-
Icon={TextStrikethrough}
|
|
88
|
-
title='Strike-through'
|
|
89
|
-
onClick={() => onAction?.({ type: 'strikethrough' })}
|
|
90
|
-
/>
|
|
91
|
-
</div>
|
|
92
|
-
<div>
|
|
93
|
-
<IconButton Icon={ListBullets} title='Bullet list' onClick={() => onAction?.({ type: 'list' })} />
|
|
94
|
-
<IconButton Icon={ListNumbers} title='Numbered list' onClick={() => onAction?.({ type: 'list-ordered' })} />
|
|
95
|
-
<IconButton Icon={ListChecks} title='Task list' onClick={() => onAction?.({ type: 'list-tasks' })} />
|
|
96
|
-
</div>
|
|
97
|
-
<div>
|
|
98
|
-
<IconButton Icon={CaretRight} title='Block quite' onClick={() => onAction?.({ type: 'blockquote' })} />
|
|
99
|
-
<IconButton Icon={Code} title='Code block' onClick={() => onAction?.({ type: 'code' })} />
|
|
100
|
-
<IconButton Icon={Table} title='Table' onClick={() => onAction?.({ type: 'table' })} />
|
|
58
|
+
<ToolbarContextProvider onAction={onAction}>
|
|
59
|
+
<DensityProvider density='fine'>
|
|
60
|
+
<div role='toolbar' className='flex w-full shrink-0 p-2 gap-4 items-center whitespace-nowrap overflow-hidden'>
|
|
61
|
+
{children}
|
|
101
62
|
</div>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
<IconButton Icon={At} title='Mention' onClick={() => onAction?.({ type: 'mention' })} />
|
|
105
|
-
<IconButton Icon={Image} title='Image' onClick={() => onAction?.({ type: 'image' })} />
|
|
106
|
-
</div>
|
|
107
|
-
<div className='grow' />
|
|
108
|
-
<div>
|
|
109
|
-
<IconButton Icon={Brain} title='AI prompt' onClick={() => onAction?.({ type: 'prompt' })} />
|
|
110
|
-
<IconButton Icon={ChatText} title='Create comment' onClick={() => onAction?.({ type: 'comment' })} />
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
</DensityProvider>
|
|
63
|
+
</DensityProvider>
|
|
64
|
+
</ToolbarContextProvider>
|
|
114
65
|
);
|
|
115
66
|
};
|
|
67
|
+
|
|
68
|
+
type ToolbarButtonProps = {
|
|
69
|
+
Icon: Icon;
|
|
70
|
+
onClick: () => Action | undefined;
|
|
71
|
+
} & NonNullable<Pick<ButtonProps, 'title'>>;
|
|
72
|
+
|
|
73
|
+
const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
|
|
74
|
+
const { onAction } = useToolbarContext('ToolbarButton');
|
|
75
|
+
const handleClick = () => {
|
|
76
|
+
const action = onClick();
|
|
77
|
+
if (action) {
|
|
78
|
+
onAction?.(action);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Button variant='ghost' classNames='p-2' onClick={handleClick} title={title}>
|
|
84
|
+
<Icon className={getSize(5)} />
|
|
85
|
+
</Button>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const ToolbarSeparator = () => <div className='grow' />;
|
|
90
|
+
|
|
91
|
+
const MarkdownHeading = () => {
|
|
92
|
+
const { onAction } = useToolbarContext('MarkdownFormatting');
|
|
93
|
+
return (
|
|
94
|
+
<Select.Root defaultValue='0' onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}>
|
|
95
|
+
<Select.TriggerButton classNames='w-[8rem]' />
|
|
96
|
+
<Select.Portal>
|
|
97
|
+
<Select.Content>
|
|
98
|
+
<Select.Viewport>
|
|
99
|
+
<Select.Option value='0'>Paragraph</Select.Option>
|
|
100
|
+
{[1, 2, 3, 4, 5, 6].map((level) => (
|
|
101
|
+
<Select.Option key={level} value={String(level)}>
|
|
102
|
+
Heading {level}
|
|
103
|
+
</Select.Option>
|
|
104
|
+
))}
|
|
105
|
+
</Select.Viewport>
|
|
106
|
+
</Select.Content>
|
|
107
|
+
</Select.Portal>
|
|
108
|
+
</Select.Root>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const MarkdownStyles = () => (
|
|
113
|
+
<div role='none'>
|
|
114
|
+
<ToolbarButton Icon={TextB} title='Bold' onClick={() => ({ type: 'bold' })} />
|
|
115
|
+
<ToolbarButton Icon={TextItalic} title='Italic' onClick={() => ({ type: 'italic' })} />
|
|
116
|
+
<ToolbarButton Icon={TextStrikethrough} title='Strike-through' onClick={() => ({ type: 'strikethrough' })} />
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const MarkdownLists = () => (
|
|
121
|
+
<div role='none'>
|
|
122
|
+
<ToolbarButton Icon={ListBullets} title='Bullet list' onClick={() => ({ type: 'list' })} />
|
|
123
|
+
<ToolbarButton Icon={ListNumbers} title='Numbered list' onClick={() => ({ type: 'list-ordered' })} />
|
|
124
|
+
<ToolbarButton Icon={ListChecks} title='Task list' onClick={() => ({ type: 'list-tasks' })} />
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const MarkdownBlocks = () => (
|
|
129
|
+
<div role='none'>
|
|
130
|
+
<ToolbarButton Icon={CaretRight} title='Block quite' onClick={() => ({ type: 'blockquote' })} />
|
|
131
|
+
<ToolbarButton Icon={Code} title='Code block' onClick={() => ({ type: 'codeblock' })} />
|
|
132
|
+
<ToolbarButton Icon={Table} title='Table' onClick={() => ({ type: 'table' })} />
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const MarkdownLinks = () => (
|
|
137
|
+
<div role='none'>
|
|
138
|
+
<ToolbarButton Icon={Link} title='Link' onClick={() => ({ type: 'link' })} />
|
|
139
|
+
<ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
|
|
140
|
+
<ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const MarkdownStandard = () => (
|
|
145
|
+
<>
|
|
146
|
+
<MarkdownHeading />
|
|
147
|
+
<MarkdownStyles />
|
|
148
|
+
<MarkdownLists />
|
|
149
|
+
<MarkdownBlocks />
|
|
150
|
+
<MarkdownLinks />
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const MarkdownExtended = () => (
|
|
155
|
+
<Toolbar.Button Icon={ChatText} title='Create comment' onClick={() => ({ type: 'comment' })} />
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
export const Toolbar = {
|
|
159
|
+
Root: ToolbarRoot,
|
|
160
|
+
Button: ToolbarButton,
|
|
161
|
+
Separator: ToolbarSeparator,
|
|
162
|
+
Markdown: MarkdownStandard,
|
|
163
|
+
Extended: MarkdownExtended,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export { useToolbarContext };
|
package/src/components/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Ref: https://github.com/automerge/automerge-codemirror
|
|
5
5
|
//
|
|
6
6
|
|
|
7
|
-
import { type EditorState, type StateField, type Transaction } from '@codemirror/state';
|
|
7
|
+
import { type EditorState, type StateField, type Transaction, type Text } from '@codemirror/state';
|
|
8
8
|
|
|
9
9
|
import { next as A, type Heads } from '@dxos/automerge/automerge';
|
|
10
10
|
|
|
@@ -14,7 +14,7 @@ export const updateAutomerge = (
|
|
|
14
14
|
field: StateField<State>,
|
|
15
15
|
handle: IDocHandle,
|
|
16
16
|
transactions: Transaction[],
|
|
17
|
-
state: EditorState, // TODO(burdon): Just pass in the state field value
|
|
17
|
+
state: EditorState, // TODO(burdon): Just pass in the state field value?
|
|
18
18
|
): Heads | undefined => {
|
|
19
19
|
const { lastHeads, path } = state.field(field);
|
|
20
20
|
|
|
@@ -32,16 +32,16 @@ export const updateAutomerge = (
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const newHeads = handle.changeAt(lastHeads, (doc: A.Doc<unknown>) => {
|
|
35
|
-
const
|
|
35
|
+
const invertedTransactions: { from: number; del: number; insert: Text }[] = [];
|
|
36
36
|
for (const tr of transactions) {
|
|
37
37
|
tr.changes.iterChanges((fromA, toA, _fromB, _toB, insert) => {
|
|
38
|
-
|
|
38
|
+
invertedTransactions.push({ from: fromA, del: toA - fromA, insert });
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
A.splice(doc, path.slice(),
|
|
42
|
+
// TODO(burdon): Hack to apply in reverse order to properly apply range.
|
|
43
|
+
invertedTransactions.reverse().forEach(({ from, del, insert }) => {
|
|
44
|
+
A.splice(doc, path.slice(), from, del, insert.toString());
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
package/src/extensions/basic.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
|
10
10
|
import { drawSelection, EditorView, highlightActiveLine, placeholder } from '@codemirror/view';
|
|
11
11
|
|
|
12
12
|
import { type ThemeMode } from '@dxos/react-ui';
|
|
13
|
+
import { isNotFalsy } from '@dxos/util';
|
|
13
14
|
|
|
14
15
|
import { type TextEditorProps } from '../components';
|
|
15
16
|
|
|
@@ -17,21 +18,22 @@ export type BasicBundleOptions = {
|
|
|
17
18
|
themeMode?: ThemeMode;
|
|
18
19
|
} & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
|
|
19
20
|
|
|
20
|
-
export const
|
|
21
|
+
export const createBasicBundle = ({
|
|
21
22
|
themeMode,
|
|
22
23
|
placeholder: _placeholder,
|
|
23
24
|
lineWrapping = true,
|
|
24
|
-
}: BasicBundleOptions): Extension[] =>
|
|
25
|
-
|
|
25
|
+
}: BasicBundleOptions = {}): Extension[] =>
|
|
26
|
+
[
|
|
27
|
+
lineWrapping && EditorView.lineWrapping,
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
// https://codemirror.net/docs/ref/#codemirror.minimalSetup
|
|
30
|
+
bracketMatching(),
|
|
31
|
+
closeBrackets(),
|
|
32
|
+
drawSelection(),
|
|
33
|
+
highlightActiveLine(),
|
|
34
|
+
history(),
|
|
35
|
+
_placeholder && placeholder(_placeholder),
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
];
|
|
37
|
+
// https://github.com/codemirror/theme-one-dark
|
|
38
|
+
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
39
|
+
].filter(isNotFalsy);
|
|
@@ -45,7 +45,7 @@ export type CommentsState = {
|
|
|
45
45
|
selection: SelectionState;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
export const
|
|
48
|
+
export const focusComment = (view: EditorView, id: string, center = true) => {
|
|
49
49
|
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
|
|
50
50
|
if (!comment?.comment.cursor) {
|
|
51
51
|
return;
|
|
@@ -311,7 +311,7 @@ export const createComment: Command = (view) => {
|
|
|
311
311
|
// Update range.
|
|
312
312
|
view.dispatch({
|
|
313
313
|
effects: setSelection.of({ current: id }),
|
|
314
|
-
selection: { anchor:
|
|
314
|
+
selection: { anchor: to },
|
|
315
315
|
});
|
|
316
316
|
|
|
317
317
|
return true;
|
|
@@ -21,13 +21,14 @@ import {
|
|
|
21
21
|
} from '@codemirror/view';
|
|
22
22
|
|
|
23
23
|
import type { ThemeMode } from '@dxos/react-ui';
|
|
24
|
+
import { isNotFalsy } from '@dxos/util';
|
|
24
25
|
|
|
25
26
|
import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
|
|
26
27
|
import { type TextEditorProps } from '../../components';
|
|
27
28
|
|
|
28
29
|
export type MarkdownBundleOptions = {
|
|
29
30
|
themeMode?: ThemeMode;
|
|
30
|
-
} & Pick<TextEditorProps, 'placeholder'>;
|
|
31
|
+
} & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Markdown bundle.
|
|
@@ -36,11 +37,15 @@ export type MarkdownBundleOptions = {
|
|
|
36
37
|
* https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
37
38
|
*/
|
|
38
39
|
// TODO(burdon): Add Composer here: https://codemirror.net/docs/community
|
|
39
|
-
export const
|
|
40
|
+
export const createMarkdownExtensions = ({
|
|
41
|
+
themeMode,
|
|
42
|
+
placeholder: _placeholder,
|
|
43
|
+
lineWrapping = true,
|
|
44
|
+
}: MarkdownBundleOptions = {}): Extension[] => {
|
|
40
45
|
return [
|
|
46
|
+
lineWrapping && EditorView.lineWrapping,
|
|
41
47
|
EditorState.allowMultipleSelections.of(true),
|
|
42
48
|
EditorState.tabSize.of(2),
|
|
43
|
-
EditorView.lineWrapping,
|
|
44
49
|
|
|
45
50
|
// https://github.com/codemirror/basic-setup
|
|
46
51
|
bracketMatching(),
|
|
@@ -51,7 +56,7 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
51
56
|
highlightActiveLine(),
|
|
52
57
|
history(),
|
|
53
58
|
indentOnInput(),
|
|
54
|
-
_placeholder
|
|
59
|
+
_placeholder && placeholder(_placeholder),
|
|
55
60
|
|
|
56
61
|
// Main extension.
|
|
57
62
|
// https://github.com/codemirror/lang-markdown
|
|
@@ -92,5 +97,5 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
|
|
|
92
97
|
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
93
98
|
...standardKeymap,
|
|
94
99
|
]),
|
|
95
|
-
];
|
|
100
|
+
].filter(isNotFalsy);
|
|
96
101
|
};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { snippet } from '@codemirror/autocomplete';
|
|
5
6
|
import { syntaxTree } from '@codemirror/language';
|
|
6
7
|
import { type Extension, RangeSetBuilder } from '@codemirror/state';
|
|
7
8
|
import {
|
|
@@ -27,10 +28,10 @@ export const setHeading =
|
|
|
27
28
|
const changes = [];
|
|
28
29
|
for (const range of ranges) {
|
|
29
30
|
const { number } = doc.lineAt(range.anchor);
|
|
30
|
-
const { from } = doc.line(number);
|
|
31
|
+
const { from, to } = doc.line(number);
|
|
31
32
|
|
|
32
33
|
// Check heading doesn't already exist.
|
|
33
|
-
const line = doc.sliceString(from,
|
|
34
|
+
const line = doc.sliceString(from, to);
|
|
34
35
|
const [_, marks, spaces] = line.match(/(#+)(\s+)/) ?? [];
|
|
35
36
|
const current = marks?.length ?? 0;
|
|
36
37
|
if (level !== current) {
|
|
@@ -76,6 +77,34 @@ export const toggleStyle =
|
|
|
76
77
|
return true;
|
|
77
78
|
};
|
|
78
79
|
|
|
80
|
+
// TODO(burdon): Define and trigger snippets for codeblock, table, etc.
|
|
81
|
+
const snippets = {
|
|
82
|
+
codeblock: snippet(['```#{lang}', '\t#{}', '```'].join('\n')),
|
|
83
|
+
table: snippet(
|
|
84
|
+
['| #{col1} | #{col2} |', '| ---- | ---- |', '| #{val1} | #{val2} |', '| #{val3} | #{val4} |'].join('\n'),
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const insertCodeblock = (view: EditorView) => {
|
|
89
|
+
const {
|
|
90
|
+
selection: { main },
|
|
91
|
+
doc,
|
|
92
|
+
} = view.state;
|
|
93
|
+
const { number } = doc.lineAt(main.anchor);
|
|
94
|
+
const { from } = doc.line(number);
|
|
95
|
+
snippets.codeblock(view, null, from, from);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const insertTable = (view: EditorView) => {
|
|
99
|
+
const {
|
|
100
|
+
selection: { main },
|
|
101
|
+
doc,
|
|
102
|
+
} = view.state;
|
|
103
|
+
const { number } = doc.lineAt(main.anchor);
|
|
104
|
+
const { from } = doc.line(number);
|
|
105
|
+
snippets.table(view, null, from, from);
|
|
106
|
+
};
|
|
107
|
+
|
|
79
108
|
export const toggleBold = toggleStyle('**');
|
|
80
109
|
export const toggleItalic = toggleStyle('_');
|
|
81
110
|
export const toggleStrikethrough = toggleStyle('~~');
|
package/src/hooks/index.ts
CHANGED
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
import type { EditorView } from '@codemirror/view';
|
|
6
6
|
|
|
7
|
-
import type { ToolbarProps } from '../components
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import type { ToolbarProps } from '../components';
|
|
8
|
+
import {
|
|
9
|
+
createComment,
|
|
10
|
+
insertCodeblock,
|
|
11
|
+
insertTable,
|
|
12
|
+
setHeading,
|
|
13
|
+
toggleBold,
|
|
14
|
+
toggleItalic,
|
|
15
|
+
toggleList,
|
|
16
|
+
toggleStrikethrough,
|
|
17
|
+
} from '../extensions';
|
|
18
|
+
|
|
19
|
+
export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
|
|
11
20
|
return (action) => {
|
|
12
21
|
if (!view) {
|
|
13
22
|
return;
|
|
@@ -32,14 +41,22 @@ export const useActionHandler = (view: EditorView | null): ToolbarProps['onActio
|
|
|
32
41
|
toggleList(view);
|
|
33
42
|
break;
|
|
34
43
|
|
|
44
|
+
case 'codeblock':
|
|
45
|
+
insertCodeblock(view);
|
|
46
|
+
break;
|
|
47
|
+
case 'table':
|
|
48
|
+
insertTable(view);
|
|
49
|
+
break;
|
|
50
|
+
|
|
35
51
|
case 'comment':
|
|
36
52
|
createComment(view);
|
|
37
53
|
break;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
|
-
// TODO(burdon): Hack
|
|
56
|
+
// TODO(burdon): Hack otherwise remains on heading selector.
|
|
41
57
|
setTimeout(() => {
|
|
42
58
|
view.focus();
|
|
59
|
+
view.dispatch({ selection: view.state.selection });
|
|
43
60
|
});
|
|
44
61
|
};
|
|
45
62
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
|
+
import { type RefCallback, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook for accessing the editor view.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const Test = () => {
|
|
13
|
+
* const [editorRef, editorView] = useEditorView();
|
|
14
|
+
* useEffect(() => {
|
|
15
|
+
* editorView?.focus();
|
|
16
|
+
* }, [editorView]);
|
|
17
|
+
* return <TextEditor ref={editorRef} />;
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const useEditorView = (): [RefCallback<EditorView | null>, EditorView | null] => {
|
|
22
|
+
const [view, setView] = useState<EditorView | null>(null);
|
|
23
|
+
return [
|
|
24
|
+
(ref: EditorView | null) => {
|
|
25
|
+
setView(ref);
|
|
26
|
+
},
|
|
27
|
+
view,
|
|
28
|
+
];
|
|
29
|
+
};
|