@dxos/react-ui-editor 0.3.11-main.5c52a13 → 0.3.11-main.5e05862
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 +1164 -151
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +8 -4
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +5 -3
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +5 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +47 -9
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
- package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
- package/package.json +24 -24
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +2 -2
- package/src/components/TextEditor/hooks.stories.tsx +6 -1
- package/src/components/Toolbar/Toolbar.stories.tsx +15 -3
- package/src/components/Toolbar/Toolbar.tsx +100 -23
- package/src/extensions/comments.ts +14 -4
- package/src/extensions/markdown/formatting.test.ts +481 -0
- package/src/extensions/markdown/formatting.ts +1036 -67
- package/src/hooks/useActionHandler.ts +48 -15
- package/src/themes/default.ts +2 -2
|
@@ -13,7 +13,17 @@ import { fixedInsetFlexLayout, groupSurface, mx } from '@dxos/react-ui-theme';
|
|
|
13
13
|
import { withTheme } from '@dxos/storybook-utils';
|
|
14
14
|
|
|
15
15
|
import { Toolbar } from './Toolbar';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
code,
|
|
18
|
+
comments,
|
|
19
|
+
formatting,
|
|
20
|
+
heading,
|
|
21
|
+
image,
|
|
22
|
+
table,
|
|
23
|
+
tasklist,
|
|
24
|
+
useComments,
|
|
25
|
+
useFormattingState,
|
|
26
|
+
} from '../../extensions';
|
|
17
27
|
import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
|
|
18
28
|
import { MarkdownEditor } from '../TextEditor';
|
|
19
29
|
|
|
@@ -24,11 +34,12 @@ const Story: FC<{ content: string }> = ({ content }) => {
|
|
|
24
34
|
const [_comments, setComments] = useState<Comment[]>([]);
|
|
25
35
|
const [editorRef, editorView] = useEditorView();
|
|
26
36
|
const model = useTextModel({ text: item.text });
|
|
37
|
+
const [formattingState, formattingObserver] = useFormattingState();
|
|
27
38
|
const extensions = useMemo(
|
|
28
39
|
() => [
|
|
29
40
|
code(),
|
|
30
41
|
comments({
|
|
31
|
-
onCreate: (cursor) => {
|
|
42
|
+
onCreate: ({ cursor }) => {
|
|
32
43
|
const id = PublicKey.random().toHex();
|
|
33
44
|
setComments((comments) => [...comments, { id, cursor }]);
|
|
34
45
|
return id;
|
|
@@ -39,6 +50,7 @@ const Story: FC<{ content: string }> = ({ content }) => {
|
|
|
39
50
|
image(),
|
|
40
51
|
table(),
|
|
41
52
|
tasklist(),
|
|
53
|
+
formattingObserver,
|
|
42
54
|
],
|
|
43
55
|
[],
|
|
44
56
|
);
|
|
@@ -54,7 +66,7 @@ const Story: FC<{ content: string }> = ({ content }) => {
|
|
|
54
66
|
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
55
67
|
<div className='flex h-full justify-center'>
|
|
56
68
|
<div className='flex flex-col h-full w-[800px]'>
|
|
57
|
-
<Toolbar.Root onAction={handleAction}>
|
|
69
|
+
<Toolbar.Root onAction={handleAction} state={formattingState}>
|
|
58
70
|
<Toolbar.Markdown />
|
|
59
71
|
<Toolbar.Separator />
|
|
60
72
|
<Toolbar.Extended />
|
|
@@ -22,19 +22,22 @@ import { createContext } from '@radix-ui/react-context';
|
|
|
22
22
|
import React, { type PropsWithChildren } from 'react';
|
|
23
23
|
|
|
24
24
|
import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
|
|
25
|
-
import { getSize } from '@dxos/react-ui-theme';
|
|
25
|
+
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
26
|
+
|
|
27
|
+
import { type Formatting } from '../../extensions/markdown';
|
|
26
28
|
|
|
27
29
|
// TODO(burdon): Revert to string to make extensible?
|
|
28
30
|
export type ActionType =
|
|
29
31
|
| 'blockquote'
|
|
30
|
-
| '
|
|
32
|
+
| 'strong'
|
|
31
33
|
| 'codeblock'
|
|
32
34
|
| 'comment'
|
|
33
35
|
| 'heading'
|
|
34
36
|
| 'image'
|
|
35
|
-
| '
|
|
37
|
+
| 'emphasis'
|
|
38
|
+
| 'code'
|
|
36
39
|
| 'link'
|
|
37
|
-
| 'list'
|
|
40
|
+
| 'list-bullet'
|
|
38
41
|
| 'list-ordered'
|
|
39
42
|
| 'list-tasks'
|
|
40
43
|
| 'mention'
|
|
@@ -48,14 +51,15 @@ export type Action = {
|
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
export type ToolbarProps = PropsWithChildren<{
|
|
54
|
+
state: Formatting | null;
|
|
51
55
|
onAction?: (action: Action) => void;
|
|
52
56
|
}>;
|
|
53
57
|
|
|
54
58
|
const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
|
|
55
59
|
|
|
56
|
-
const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
|
|
60
|
+
const ToolbarRoot = ({ children, onAction, state }: ToolbarProps) => {
|
|
57
61
|
return (
|
|
58
|
-
<ToolbarContextProvider onAction={onAction}>
|
|
62
|
+
<ToolbarContextProvider onAction={onAction} state={state}>
|
|
59
63
|
<DensityProvider density='fine'>
|
|
60
64
|
<div role='toolbar' className='flex w-full shrink-0 p-2 gap-4 items-center whitespace-nowrap overflow-hidden'>
|
|
61
65
|
{children}
|
|
@@ -67,20 +71,31 @@ const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
|
|
|
67
71
|
|
|
68
72
|
type ToolbarButtonProps = {
|
|
69
73
|
Icon: Icon;
|
|
70
|
-
onClick: () => Action | undefined;
|
|
74
|
+
onClick: (state: Formatting | null) => Action | undefined;
|
|
75
|
+
getState?: (state: Formatting) => boolean;
|
|
76
|
+
disable?: (state: Formatting) => boolean;
|
|
71
77
|
} & NonNullable<Pick<ButtonProps, 'title'>>;
|
|
72
78
|
|
|
73
|
-
const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
|
|
74
|
-
const { onAction } = useToolbarContext('ToolbarButton');
|
|
75
|
-
const handleClick = () => {
|
|
76
|
-
const action = onClick();
|
|
79
|
+
const ToolbarButton = ({ Icon, onClick, title, getState, disable }: ToolbarButtonProps) => {
|
|
80
|
+
const { onAction, state } = useToolbarContext('ToolbarButton');
|
|
81
|
+
const handleClick = (event: React.MouseEvent) => {
|
|
82
|
+
const action = onClick(state);
|
|
77
83
|
if (action) {
|
|
78
84
|
onAction?.(action);
|
|
85
|
+
event.preventDefault();
|
|
79
86
|
}
|
|
80
87
|
};
|
|
88
|
+
const active = getState && state ? getState(state) : false;
|
|
89
|
+
const disabled = disable && state ? disable(state) : false;
|
|
81
90
|
|
|
82
91
|
return (
|
|
83
|
-
<Button
|
|
92
|
+
<Button
|
|
93
|
+
variant='ghost'
|
|
94
|
+
classNames={mx('p-2', active && 'ring')}
|
|
95
|
+
onMouseDown={handleClick}
|
|
96
|
+
title={title}
|
|
97
|
+
disabled={disabled}
|
|
98
|
+
>
|
|
84
99
|
<Icon className={getSize(5)} />
|
|
85
100
|
</Button>
|
|
86
101
|
);
|
|
@@ -89,9 +104,16 @@ const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
|
|
|
89
104
|
const ToolbarSeparator = () => <div className='grow' />;
|
|
90
105
|
|
|
91
106
|
const MarkdownHeading = () => {
|
|
92
|
-
const { onAction } = useToolbarContext('MarkdownFormatting');
|
|
107
|
+
const { onAction, state } = useToolbarContext('MarkdownFormatting');
|
|
108
|
+
const blockType = state ? state.blockType : 'paragraph';
|
|
109
|
+
const header = blockType && /heading(\d)/.exec(blockType);
|
|
110
|
+
const value = header ? header[1] : blockType === 'paragraph' || !blockType ? '0' : null;
|
|
93
111
|
return (
|
|
94
|
-
<Select.Root
|
|
112
|
+
<Select.Root
|
|
113
|
+
disabled={value === null}
|
|
114
|
+
value={value ?? '0'}
|
|
115
|
+
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
116
|
+
>
|
|
95
117
|
<Select.TriggerButton classNames='w-[8rem]' />
|
|
96
118
|
<Select.Portal>
|
|
97
119
|
<Select.Content>
|
|
@@ -111,31 +133,86 @@ const MarkdownHeading = () => {
|
|
|
111
133
|
|
|
112
134
|
const MarkdownStyles = () => (
|
|
113
135
|
<div role='none'>
|
|
114
|
-
<ToolbarButton
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
<ToolbarButton
|
|
137
|
+
Icon={TextB}
|
|
138
|
+
title='String'
|
|
139
|
+
onClick={(s) => ({ type: 'strong', data: s ? !s.strong : null })}
|
|
140
|
+
getState={(s) => s.strong}
|
|
141
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
142
|
+
/>
|
|
143
|
+
<ToolbarButton
|
|
144
|
+
Icon={TextItalic}
|
|
145
|
+
title='Emphasis'
|
|
146
|
+
onClick={(s) => ({ type: 'emphasis', data: s ? !s.emphasis : null })}
|
|
147
|
+
getState={(s) => s.emphasis}
|
|
148
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
149
|
+
/>
|
|
150
|
+
<ToolbarButton
|
|
151
|
+
Icon={TextStrikethrough}
|
|
152
|
+
title='Strike-through'
|
|
153
|
+
onClick={(s) => ({ type: 'strikethrough', data: s ? !s.strikethrough : null })}
|
|
154
|
+
getState={(s) => s.strikethrough}
|
|
155
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
156
|
+
/>
|
|
157
|
+
<ToolbarButton
|
|
158
|
+
Icon={Code}
|
|
159
|
+
title='Inline code'
|
|
160
|
+
onClick={(s) => ({ type: 'code', data: s ? !s.code : null })}
|
|
161
|
+
getState={(s) => s.code}
|
|
162
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
163
|
+
/>
|
|
117
164
|
</div>
|
|
118
165
|
);
|
|
119
166
|
|
|
120
167
|
const MarkdownLists = () => (
|
|
121
168
|
<div role='none'>
|
|
122
|
-
<ToolbarButton
|
|
123
|
-
|
|
124
|
-
|
|
169
|
+
<ToolbarButton
|
|
170
|
+
Icon={ListBullets}
|
|
171
|
+
title='Bullet list'
|
|
172
|
+
onClick={(s) => ({ type: 'list-bullet', data: s ? s.listStyle !== 'bullet' : null })}
|
|
173
|
+
getState={(s) => s.listStyle === 'bullet'}
|
|
174
|
+
/>
|
|
175
|
+
<ToolbarButton
|
|
176
|
+
Icon={ListNumbers}
|
|
177
|
+
title='Numbered list'
|
|
178
|
+
onClick={(s) => ({ type: 'list-ordered', data: s ? s.listStyle !== 'ordered' : null })}
|
|
179
|
+
getState={(s) => s.listStyle === 'ordered'}
|
|
180
|
+
/>
|
|
181
|
+
<ToolbarButton
|
|
182
|
+
Icon={ListChecks}
|
|
183
|
+
title='Task list'
|
|
184
|
+
onClick={(s) => ({ type: 'list-tasks', data: s ? s.listStyle !== 'task' : null })}
|
|
185
|
+
getState={(s) => s.listStyle === 'task'}
|
|
186
|
+
/>
|
|
125
187
|
</div>
|
|
126
188
|
);
|
|
127
189
|
|
|
128
190
|
const MarkdownBlocks = () => (
|
|
129
191
|
<div role='none'>
|
|
130
|
-
<ToolbarButton
|
|
131
|
-
|
|
192
|
+
<ToolbarButton
|
|
193
|
+
Icon={CaretRight}
|
|
194
|
+
title='Block quote'
|
|
195
|
+
onClick={(s) => ({ type: 'blockquote', data: s ? !s.blockquote : null })}
|
|
196
|
+
getState={(s) => s.blockquote}
|
|
197
|
+
/>
|
|
198
|
+
<ToolbarButton
|
|
199
|
+
Icon={Code}
|
|
200
|
+
title='Code block'
|
|
201
|
+
onClick={(s) => ({ type: 'codeblock', data: s ? s.blockType !== 'codeblock' : null })}
|
|
202
|
+
getState={(s) => s.blockType === 'codeblock'}
|
|
203
|
+
/>
|
|
132
204
|
<ToolbarButton Icon={Table} title='Table' onClick={() => ({ type: 'table' })} />
|
|
133
205
|
</div>
|
|
134
206
|
);
|
|
135
207
|
|
|
136
208
|
const MarkdownLinks = () => (
|
|
137
209
|
<div role='none'>
|
|
138
|
-
<ToolbarButton
|
|
210
|
+
<ToolbarButton
|
|
211
|
+
Icon={Link}
|
|
212
|
+
title='Link'
|
|
213
|
+
onClick={(s) => ({ type: 'link', data: s ? !s.link : null })}
|
|
214
|
+
getState={(s) => s.link}
|
|
215
|
+
/>
|
|
139
216
|
<ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
|
|
140
217
|
<ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
|
|
141
218
|
</div>
|
|
@@ -18,12 +18,22 @@ import { callbackWrapper } from '../util';
|
|
|
18
18
|
|
|
19
19
|
// TODO(burdon): Reconcile with theme.
|
|
20
20
|
const styles = EditorView.baseTheme({
|
|
21
|
-
'& .cm-comment': {
|
|
21
|
+
'&light .cm-comment, &light .cm-comment-current': { mixBlendMode: 'darken' },
|
|
22
|
+
'&dark .cm-comment, &dark .cm-comment-current': { mixBlendMode: 'plus-lighter' },
|
|
23
|
+
'&light .cm-comment': {
|
|
22
24
|
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
23
25
|
},
|
|
24
|
-
'& .cm-comment-current': {
|
|
26
|
+
'&light .cm-comment-current': {
|
|
25
27
|
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
26
28
|
},
|
|
29
|
+
'&dark .cm-comment': {
|
|
30
|
+
color: getToken('extend.colors.yellow.50'),
|
|
31
|
+
backgroundColor: getToken('extend.colors.yellow.900'),
|
|
32
|
+
},
|
|
33
|
+
'&dark .cm-comment-current': {
|
|
34
|
+
color: getToken('extend.colors.yellow.100'),
|
|
35
|
+
backgroundColor: getToken('extend.colors.yellow.950'),
|
|
36
|
+
},
|
|
27
37
|
});
|
|
28
38
|
|
|
29
39
|
const commentMark = Decoration.mark({ class: 'cm-comment' });
|
|
@@ -146,7 +156,7 @@ export type CommentsOptions = {
|
|
|
146
156
|
/**
|
|
147
157
|
* Called to create a new thread and return the thread id.
|
|
148
158
|
*/
|
|
149
|
-
onCreate?: (cursor: string
|
|
159
|
+
onCreate?: (params: { cursor: string; from: number; location?: Rect | null }) => string | undefined;
|
|
150
160
|
/**
|
|
151
161
|
* Selection cut/deleted.
|
|
152
162
|
*/
|
|
@@ -306,7 +316,7 @@ export const createComment: Command = (view) => {
|
|
|
306
316
|
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
307
317
|
if (cursor) {
|
|
308
318
|
// Create thread via callback.
|
|
309
|
-
const id = options.onCreate?.(cursor, view.coordsAtPos(from));
|
|
319
|
+
const id = options.onCreate?.({ cursor, from, location: view.coordsAtPos(from) });
|
|
310
320
|
if (id) {
|
|
311
321
|
// Update range.
|
|
312
322
|
view.dispatch({
|