@dxos/react-ui-editor 0.3.11-next.ee2b64c → 0.4.0
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 +1178 -159
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +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 +55 -10
- 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 +3 -3
- package/src/components/TextEditor/hooks.stories.tsx +6 -1
- package/src/components/Toolbar/Toolbar.stories.tsx +16 -4
- package/src/components/Toolbar/Toolbar.tsx +128 -30
- package/src/extensions/comments.ts +2 -2
- package/src/extensions/markdown/formatting.test.ts +482 -0
- package/src/extensions/markdown/formatting.ts +1118 -69
- package/src/hooks/useActionHandler.ts +47 -16
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
link,
|
|
22
22
|
table,
|
|
23
23
|
tasklist,
|
|
24
|
+
useFormattingState,
|
|
24
25
|
} from '../../extensions';
|
|
25
26
|
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
26
27
|
import { markdownTheme } from '../../themes';
|
|
@@ -41,6 +42,7 @@ type StoryProps = {
|
|
|
41
42
|
|
|
42
43
|
const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
43
44
|
const { themeMode } = useThemeContext();
|
|
45
|
+
const [formattingState, trackFormatting] = useFormattingState();
|
|
44
46
|
const { parentRef, view } = useTextEditor({
|
|
45
47
|
autoFocus,
|
|
46
48
|
doc,
|
|
@@ -69,16 +71,19 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
69
71
|
link(),
|
|
70
72
|
table(),
|
|
71
73
|
tasklist(),
|
|
74
|
+
trackFormatting,
|
|
72
75
|
],
|
|
73
76
|
});
|
|
74
77
|
|
|
75
78
|
const handleAction = useActionHandler(view);
|
|
76
79
|
|
|
80
|
+
// FIXME This doesn't update the state on view changes. Also not
|
|
81
|
+
// sure if view is even guaranteed to exist at this point.
|
|
77
82
|
return (
|
|
78
83
|
<div className={mx(fixedInsetFlexLayout, groupSurface)}>
|
|
79
84
|
<div className='flex h-full justify-center'>
|
|
80
85
|
<div className='flex flex-col h-full w-[800px]'>
|
|
81
|
-
<Toolbar.Root onAction={handleAction}>
|
|
86
|
+
<Toolbar.Root onAction={handleAction} state={formattingState}>
|
|
82
87
|
<Toolbar.Markdown />
|
|
83
88
|
</Toolbar.Root>
|
|
84
89
|
|
|
@@ -4,16 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxosTheme';
|
|
6
6
|
|
|
7
|
-
import { faker } from '@faker-js/faker';
|
|
8
7
|
import React, { type FC, useMemo, useState } from 'react';
|
|
9
8
|
|
|
10
9
|
import { TextObject } from '@dxos/echo-schema';
|
|
11
10
|
import { PublicKey } from '@dxos/keys';
|
|
11
|
+
import { faker } from '@dxos/random';
|
|
12
12
|
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 />
|
|
@@ -4,37 +4,56 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
type Icon,
|
|
7
|
-
At,
|
|
8
7
|
CaretRight,
|
|
9
8
|
ChatText,
|
|
10
9
|
Code,
|
|
11
|
-
|
|
10
|
+
CodeBlock,
|
|
12
11
|
Link,
|
|
13
12
|
ListBullets,
|
|
14
13
|
ListChecks,
|
|
15
14
|
ListNumbers,
|
|
15
|
+
Paragraph,
|
|
16
16
|
TextStrikethrough,
|
|
17
17
|
Table,
|
|
18
18
|
TextB,
|
|
19
|
+
TextHOne,
|
|
20
|
+
TextHTwo,
|
|
21
|
+
TextHThree,
|
|
22
|
+
TextHFour,
|
|
23
|
+
TextHFive,
|
|
24
|
+
TextHSix,
|
|
19
25
|
TextItalic,
|
|
20
26
|
} from '@phosphor-icons/react';
|
|
21
27
|
import { createContext } from '@radix-ui/react-context';
|
|
22
28
|
import React, { type PropsWithChildren } from 'react';
|
|
23
29
|
|
|
24
30
|
import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
|
|
25
|
-
import { getSize } from '@dxos/react-ui-theme';
|
|
31
|
+
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
32
|
+
|
|
33
|
+
import { type Formatting } from '../../extensions';
|
|
34
|
+
|
|
35
|
+
const HeadingIcons: { [key: string]: Icon } = {
|
|
36
|
+
'0': Paragraph,
|
|
37
|
+
'1': TextHOne,
|
|
38
|
+
'2': TextHTwo,
|
|
39
|
+
'3': TextHThree,
|
|
40
|
+
'4': TextHFour,
|
|
41
|
+
'5': TextHFive,
|
|
42
|
+
'6': TextHSix,
|
|
43
|
+
};
|
|
26
44
|
|
|
27
45
|
// TODO(burdon): Revert to string to make extensible?
|
|
28
46
|
export type ActionType =
|
|
29
47
|
| 'blockquote'
|
|
30
|
-
| '
|
|
48
|
+
| 'strong'
|
|
31
49
|
| 'codeblock'
|
|
32
50
|
| 'comment'
|
|
33
51
|
| 'heading'
|
|
34
52
|
| 'image'
|
|
35
|
-
| '
|
|
53
|
+
| 'emphasis'
|
|
54
|
+
| 'code'
|
|
36
55
|
| 'link'
|
|
37
|
-
| 'list'
|
|
56
|
+
| 'list-bullet'
|
|
38
57
|
| 'list-ordered'
|
|
39
58
|
| 'list-tasks'
|
|
40
59
|
| 'mention'
|
|
@@ -48,16 +67,17 @@ export type Action = {
|
|
|
48
67
|
};
|
|
49
68
|
|
|
50
69
|
export type ToolbarProps = PropsWithChildren<{
|
|
70
|
+
state: Formatting | null;
|
|
51
71
|
onAction?: (action: Action) => void;
|
|
52
72
|
}>;
|
|
53
73
|
|
|
54
74
|
const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
|
|
55
75
|
|
|
56
|
-
const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
|
|
76
|
+
const ToolbarRoot = ({ children, onAction, state }: ToolbarProps) => {
|
|
57
77
|
return (
|
|
58
|
-
<ToolbarContextProvider onAction={onAction}>
|
|
78
|
+
<ToolbarContextProvider onAction={onAction} state={state}>
|
|
59
79
|
<DensityProvider density='fine'>
|
|
60
|
-
<div role='toolbar' className='flex w-full shrink-0 p-
|
|
80
|
+
<div role='toolbar' className='flex w-full shrink-0 p-1 gap-4 items-center whitespace-nowrap overflow-hidden'>
|
|
61
81
|
{children}
|
|
62
82
|
</div>
|
|
63
83
|
</DensityProvider>
|
|
@@ -67,20 +87,32 @@ const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
|
|
|
67
87
|
|
|
68
88
|
type ToolbarButtonProps = {
|
|
69
89
|
Icon: Icon;
|
|
70
|
-
|
|
90
|
+
disable?: (state: Formatting) => boolean;
|
|
91
|
+
getState?: (state: Formatting) => boolean;
|
|
92
|
+
onClick: (state: Formatting | null) => Action | undefined;
|
|
71
93
|
} & NonNullable<Pick<ButtonProps, 'title'>>;
|
|
72
94
|
|
|
73
|
-
const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
|
|
74
|
-
const { onAction } = useToolbarContext('ToolbarButton');
|
|
75
|
-
const
|
|
76
|
-
|
|
95
|
+
const ToolbarButton = ({ Icon, onClick, title, getState, disable }: ToolbarButtonProps) => {
|
|
96
|
+
const { onAction, state } = useToolbarContext('ToolbarButton');
|
|
97
|
+
const active = getState && state ? getState(state) : false;
|
|
98
|
+
const disabled = disable && state ? disable(state) : false;
|
|
99
|
+
|
|
100
|
+
const handleClick = (event: React.MouseEvent) => {
|
|
101
|
+
const action = onClick(state);
|
|
77
102
|
if (action) {
|
|
78
103
|
onAction?.(action);
|
|
104
|
+
event.preventDefault();
|
|
79
105
|
}
|
|
80
106
|
};
|
|
81
107
|
|
|
82
108
|
return (
|
|
83
|
-
<Button
|
|
109
|
+
<Button
|
|
110
|
+
variant='ghost'
|
|
111
|
+
classNames={mx('p-2', active && 'ring-[1px]')}
|
|
112
|
+
onMouseDown={handleClick}
|
|
113
|
+
title={title}
|
|
114
|
+
disabled={disabled}
|
|
115
|
+
>
|
|
84
116
|
<Icon className={getSize(5)} />
|
|
85
117
|
</Button>
|
|
86
118
|
);
|
|
@@ -89,10 +121,20 @@ const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
|
|
|
89
121
|
const ToolbarSeparator = () => <div className='grow' />;
|
|
90
122
|
|
|
91
123
|
const MarkdownHeading = () => {
|
|
92
|
-
const { onAction } = useToolbarContext('MarkdownFormatting');
|
|
124
|
+
const { onAction, state } = useToolbarContext('MarkdownFormatting');
|
|
125
|
+
const blockType = state ? state.blockType : 'paragraph';
|
|
126
|
+
const header = blockType && /heading(\d)/.exec(blockType);
|
|
127
|
+
const value = header ? header[1] : blockType === 'paragraph' || !blockType ? '0' : undefined;
|
|
128
|
+
const HeadingIcon = HeadingIcons[value ?? '0'];
|
|
93
129
|
return (
|
|
94
|
-
<Select.Root
|
|
95
|
-
|
|
130
|
+
<Select.Root
|
|
131
|
+
disabled={value === null}
|
|
132
|
+
value={value ?? '0'}
|
|
133
|
+
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
134
|
+
>
|
|
135
|
+
<Select.TriggerButton>
|
|
136
|
+
<HeadingIcon className={getSize(5)} />
|
|
137
|
+
</Select.TriggerButton>
|
|
96
138
|
<Select.Portal>
|
|
97
139
|
<Select.Content>
|
|
98
140
|
<Select.Viewport>
|
|
@@ -111,33 +153,89 @@ const MarkdownHeading = () => {
|
|
|
111
153
|
|
|
112
154
|
const MarkdownStyles = () => (
|
|
113
155
|
<div role='none'>
|
|
114
|
-
<ToolbarButton
|
|
115
|
-
|
|
116
|
-
|
|
156
|
+
<ToolbarButton
|
|
157
|
+
Icon={TextB}
|
|
158
|
+
title='String'
|
|
159
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
160
|
+
getState={(s) => s.strong}
|
|
161
|
+
onClick={(s) => ({ type: 'strong', data: s ? !s.strong : null })}
|
|
162
|
+
/>
|
|
163
|
+
<ToolbarButton
|
|
164
|
+
Icon={TextItalic}
|
|
165
|
+
title='Emphasis'
|
|
166
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
167
|
+
getState={(s) => s.emphasis}
|
|
168
|
+
onClick={(s) => ({ type: 'emphasis', data: s ? !s.emphasis : null })}
|
|
169
|
+
/>
|
|
170
|
+
<ToolbarButton
|
|
171
|
+
Icon={TextStrikethrough}
|
|
172
|
+
title='Strike-through'
|
|
173
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
174
|
+
getState={(s) => s.strikethrough}
|
|
175
|
+
onClick={(s) => ({ type: 'strikethrough', data: s ? !s.strikethrough : null })}
|
|
176
|
+
/>
|
|
177
|
+
<ToolbarButton
|
|
178
|
+
Icon={Code}
|
|
179
|
+
title='Inline code'
|
|
180
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
181
|
+
getState={(s) => s.code}
|
|
182
|
+
onClick={(s) => ({ type: 'code', data: s ? !s.code : null })}
|
|
183
|
+
/>
|
|
117
184
|
</div>
|
|
118
185
|
);
|
|
119
186
|
|
|
120
187
|
const MarkdownLists = () => (
|
|
121
188
|
<div role='none'>
|
|
122
|
-
<ToolbarButton
|
|
123
|
-
|
|
124
|
-
|
|
189
|
+
<ToolbarButton
|
|
190
|
+
Icon={ListBullets}
|
|
191
|
+
title='Bullet list'
|
|
192
|
+
getState={(s) => s.listStyle === 'bullet'}
|
|
193
|
+
onClick={(s) => ({ type: 'list-bullet', data: s ? s.listStyle !== 'bullet' : null })}
|
|
194
|
+
/>
|
|
195
|
+
<ToolbarButton
|
|
196
|
+
Icon={ListNumbers}
|
|
197
|
+
title='Numbered list'
|
|
198
|
+
getState={(s) => s.listStyle === 'ordered'}
|
|
199
|
+
onClick={(s) => ({ type: 'list-ordered', data: s ? s.listStyle !== 'ordered' : null })}
|
|
200
|
+
/>
|
|
201
|
+
<ToolbarButton
|
|
202
|
+
Icon={ListChecks}
|
|
203
|
+
title='Task list'
|
|
204
|
+
getState={(s) => s.listStyle === 'task'}
|
|
205
|
+
onClick={(s) => ({ type: 'list-tasks', data: s ? s.listStyle !== 'task' : null })}
|
|
206
|
+
/>
|
|
125
207
|
</div>
|
|
126
208
|
);
|
|
127
209
|
|
|
128
210
|
const MarkdownBlocks = () => (
|
|
129
211
|
<div role='none'>
|
|
130
|
-
<ToolbarButton
|
|
131
|
-
|
|
132
|
-
|
|
212
|
+
<ToolbarButton
|
|
213
|
+
Icon={CaretRight}
|
|
214
|
+
title='Block quote'
|
|
215
|
+
getState={(s) => s.blockQuote}
|
|
216
|
+
onClick={(s) => ({ type: 'blockquote', data: s ? !s.blockQuote : null })}
|
|
217
|
+
/>
|
|
218
|
+
<ToolbarButton
|
|
219
|
+
Icon={CodeBlock}
|
|
220
|
+
title='Code block'
|
|
221
|
+
disable={(s) => !s.blankLine}
|
|
222
|
+
getState={(s) => s.blockType === 'codeblock'}
|
|
223
|
+
onClick={(s) => ({ type: 'codeblock', data: s ? s.blockType !== 'codeblock' : null })}
|
|
224
|
+
/>
|
|
225
|
+
<ToolbarButton Icon={Table} title='Table' disable={(s) => !s.blankLine} onClick={() => ({ type: 'table' })} />
|
|
133
226
|
</div>
|
|
134
227
|
);
|
|
135
228
|
|
|
136
229
|
const MarkdownLinks = () => (
|
|
137
230
|
<div role='none'>
|
|
138
|
-
<ToolbarButton
|
|
139
|
-
|
|
140
|
-
|
|
231
|
+
<ToolbarButton
|
|
232
|
+
Icon={Link}
|
|
233
|
+
title='Link'
|
|
234
|
+
getState={(s) => s.link}
|
|
235
|
+
onClick={(s) => ({ type: 'link', data: s ? !s.link : null })}
|
|
236
|
+
/>
|
|
237
|
+
{/* <ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} /> */}
|
|
238
|
+
{/* <ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} /> */}
|
|
141
239
|
</div>
|
|
142
240
|
);
|
|
143
241
|
|
|
@@ -156,7 +156,7 @@ export type CommentsOptions = {
|
|
|
156
156
|
/**
|
|
157
157
|
* Called to create a new thread and return the thread id.
|
|
158
158
|
*/
|
|
159
|
-
onCreate?: (cursor: string
|
|
159
|
+
onCreate?: (params: { cursor: string; from: number; location?: Rect | null }) => string | undefined;
|
|
160
160
|
/**
|
|
161
161
|
* Selection cut/deleted.
|
|
162
162
|
*/
|
|
@@ -316,7 +316,7 @@ export const createComment: Command = (view) => {
|
|
|
316
316
|
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
317
317
|
if (cursor) {
|
|
318
318
|
// Create thread via callback.
|
|
319
|
-
const id = options.onCreate?.(cursor, view.coordsAtPos(from));
|
|
319
|
+
const id = options.onCreate?.({ cursor, from, location: view.coordsAtPos(from) });
|
|
320
320
|
if (id) {
|
|
321
321
|
// Update range.
|
|
322
322
|
view.dispatch({
|