@dxos/react-ui-editor 0.4.8-main.05fda5d → 0.4.8-main.06fa7e4
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 +1938 -1920
- 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 +18 -34
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +80 -5
- 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.d.ts +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +2 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +1 -15
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +0 -1
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/sync.d.ts +2 -1
- package/dist/types/src/extensions/automerge/sync.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +2 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/{hooks → extensions/awareness}/awareness-provider.d.ts +1 -1
- package/dist/types/src/extensions/awareness/awareness-provider.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness/index.d.ts +3 -0
- package/dist/types/src/extensions/awareness/index.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts +2 -2
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +1 -1
- package/dist/types/src/extensions/cursor.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/doc.d.ts +3 -0
- package/dist/types/src/extensions/doc.d.ts.map +1 -0
- package/dist/types/src/extensions/factories.d.ts +53 -0
- package/dist/types/src/extensions/factories.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +4 -2
- 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 +5 -4
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +8 -5
- 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/markdown/table.d.ts +2 -2
- package/dist/types/src/extensions/markdown/table.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/extensions/types.d.ts +9 -0
- package/dist/types/src/extensions/types.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +1 -4
- 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/useDocAccessor.d.ts +9 -0
- package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +3 -44
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts +7 -0
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts +0 -3
- 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 -27
- package/src/components/TextEditor/TextEditor.stories.tsx +532 -23
- package/src/components/TextEditor/TextEditor.tsx +45 -156
- package/src/components/Toolbar/Toolbar.stories.tsx +54 -55
- package/src/components/Toolbar/Toolbar.tsx +102 -60
- package/src/extensions/automerge/automerge.stories.tsx +16 -17
- package/src/extensions/automerge/automerge.ts +6 -5
- package/src/extensions/automerge/cursor.ts +1 -1
- package/src/extensions/automerge/defs.ts +1 -22
- package/src/extensions/automerge/index.ts +0 -1
- package/src/extensions/automerge/sync.ts +2 -9
- package/src/extensions/automerge/update-automerge.ts +2 -1
- package/src/{hooks → extensions/awareness}/awareness-provider.ts +1 -1
- package/src/extensions/{awareness.ts → awareness/awareness.ts} +1 -1
- package/src/extensions/awareness/index.ts +6 -0
- package/src/extensions/command/state.ts +3 -3
- package/src/extensions/comments.ts +9 -5
- package/src/extensions/cursor.ts +1 -1
- package/src/extensions/dnd.ts +31 -9
- package/src/extensions/doc.ts +14 -0
- package/src/extensions/factories.ts +193 -0
- package/src/extensions/index.ts +4 -2
- package/src/extensions/markdown/action.ts +112 -0
- package/src/extensions/markdown/bundle.ts +17 -37
- package/src/extensions/markdown/decorate.ts +2 -4
- package/src/extensions/markdown/formatting.test.ts +13 -13
- package/src/extensions/markdown/formatting.ts +110 -97
- package/src/extensions/markdown/image.ts +7 -0
- package/src/extensions/markdown/index.ts +1 -0
- package/src/extensions/markdown/table.ts +11 -3
- package/src/extensions/modes.ts +11 -2
- package/src/extensions/state.ts +90 -0
- package/src/extensions/types.ts +16 -0
- package/src/hooks/index.ts +1 -5
- package/src/hooks/useActionHandler.ts +4 -85
- package/src/hooks/useDocAccessor.ts +27 -0
- package/src/hooks/useTextEditor.stories.tsx +40 -35
- package/src/hooks/useTextEditor.ts +14 -120
- package/src/index.ts +1 -1
- package/src/themes/default.ts +25 -36
- package/src/translations.ts +1 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +0 -103
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/awareness.d.ts.map +0 -1
- package/dist/types/src/extensions/cursor-line-margin.d.ts +0 -8
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +0 -1
- package/dist/types/src/extensions/outliner.d.ts +0 -4
- package/dist/types/src/extensions/outliner.d.ts.map +0 -1
- package/dist/types/src/hooks/awareness-provider.d.ts.map +0 -1
- package/dist/types/src/hooks/defs.d.ts +0 -20
- package/dist/types/src/hooks/defs.d.ts.map +0 -1
- package/dist/types/src/hooks/useEditorView.d.ts +0 -20
- package/dist/types/src/hooks/useEditorView.d.ts.map +0 -1
- package/dist/types/src/hooks/useTextModel.d.ts +0 -33
- package/dist/types/src/hooks/useTextModel.d.ts.map +0 -1
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +0 -524
- package/src/extensions/cursor-line-margin.ts +0 -95
- package/src/extensions/outliner.ts +0 -12
- package/src/hooks/defs.ts +0 -30
- package/src/hooks/useEditorView.ts +0 -32
- package/src/hooks/useTextModel.ts +0 -121
- /package/dist/types/src/extensions/{awareness.d.ts → awareness/awareness.d.ts} +0 -0
|
@@ -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 };
|
|
@@ -9,7 +9,7 @@ import '@preact/signals-react';
|
|
|
9
9
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
10
10
|
|
|
11
11
|
import { Repo } from '@dxos/automerge/automerge-repo';
|
|
12
|
-
import { createDocAccessor, Filter } from '@dxos/echo-schema';
|
|
12
|
+
import { createDocAccessor, Filter, DocAccessor } from '@dxos/echo-schema';
|
|
13
13
|
import { type PublicKey } from '@dxos/keys';
|
|
14
14
|
import { Expando, TextObject, useSpace } from '@dxos/react-client/echo';
|
|
15
15
|
import { ClientRepeater } from '@dxos/react-client/testing';
|
|
@@ -17,9 +17,9 @@ import { useThemeContext } from '@dxos/react-ui';
|
|
|
17
17
|
import { withTheme } from '@dxos/storybook-utils';
|
|
18
18
|
|
|
19
19
|
import { automerge } from './automerge';
|
|
20
|
-
import {
|
|
21
|
-
import { createBasicExtensions, createThemeExtensions, useTextEditor } from '../../hooks';
|
|
20
|
+
import { useTextEditor } from '../../hooks';
|
|
22
21
|
import translations from '../../translations';
|
|
22
|
+
import { createBasicExtensions, createThemeExtensions } from '../factories';
|
|
23
23
|
|
|
24
24
|
const initialContent = 'Hello world!';
|
|
25
25
|
|
|
@@ -34,21 +34,19 @@ type EditorProps = {
|
|
|
34
34
|
|
|
35
35
|
const Editor = ({ source, autoFocus }: EditorProps) => {
|
|
36
36
|
const { themeMode } = useThemeContext();
|
|
37
|
-
const
|
|
38
|
-
() =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
const { parentRef } = useTextEditor(
|
|
38
|
+
() => ({
|
|
39
|
+
doc: DocAccessor.getValue(source),
|
|
40
|
+
extensions: [
|
|
41
|
+
createBasicExtensions({ placeholder: 'Type here...' }),
|
|
42
|
+
createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2 bg-white dark:bg-black' } } }),
|
|
43
|
+
automerge(source),
|
|
44
|
+
],
|
|
45
|
+
autoFocus,
|
|
46
|
+
}),
|
|
47
|
+
[source, themeMode],
|
|
44
48
|
);
|
|
45
49
|
|
|
46
|
-
const { parentRef } = useTextEditor({
|
|
47
|
-
doc: DocAccessor.getValue(source),
|
|
48
|
-
extensions,
|
|
49
|
-
autoFocus,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
50
|
return <div ref={parentRef} />;
|
|
53
51
|
};
|
|
54
52
|
|
|
@@ -114,7 +112,8 @@ const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
|
|
|
114
112
|
|
|
115
113
|
export const Default = {};
|
|
116
114
|
|
|
117
|
-
// TODO(burdon):
|
|
115
|
+
// TODO(burdon): Error:
|
|
116
|
+
// chunk-6NX3RPDS.mjs:2021 ControlPipeline#5 Error: invariant violation: Feed already added
|
|
118
117
|
export const WithEcho = {
|
|
119
118
|
decorators: [withTheme],
|
|
120
119
|
render: () => {
|
|
@@ -8,9 +8,10 @@ import { StateField, type Extension } from '@codemirror/state';
|
|
|
8
8
|
import { EditorView, ViewPlugin } from '@codemirror/view';
|
|
9
9
|
|
|
10
10
|
import { next as A } from '@dxos/automerge/automerge';
|
|
11
|
+
import { type DocAccessor } from '@dxos/echo-schema';
|
|
11
12
|
|
|
12
13
|
import { cursorConverter } from './cursor';
|
|
13
|
-
import { updateHeadsEffect, isReconcile, type State
|
|
14
|
+
import { updateHeadsEffect, isReconcile, type State } from './defs';
|
|
14
15
|
import { Syncer } from './sync';
|
|
15
16
|
import { Cursor } from '../cursor';
|
|
16
17
|
|
|
@@ -59,16 +60,16 @@ export const automerge = (accessor: DocAccessor): Extension => {
|
|
|
59
60
|
ViewPlugin.fromClass(
|
|
60
61
|
class {
|
|
61
62
|
constructor(private readonly _view: EditorView) {
|
|
62
|
-
accessor.handle.addListener('change', this._handleChange
|
|
63
|
+
accessor.handle.addListener('change', this._handleChange);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
destroy() {
|
|
66
|
-
accessor.handle.removeListener('change', this._handleChange
|
|
67
|
+
accessor.handle.removeListener('change', this._handleChange);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
_handleChange() {
|
|
70
|
+
readonly _handleChange = () => {
|
|
70
71
|
syncer.reconcile(this._view, false);
|
|
71
|
-
}
|
|
72
|
+
};
|
|
72
73
|
},
|
|
73
74
|
),
|
|
74
75
|
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
import get from 'lodash.get';
|
|
6
6
|
|
|
7
7
|
import { next as A } from '@dxos/automerge/automerge';
|
|
8
|
+
import { type DocAccessor } from '@dxos/echo-schema';
|
|
8
9
|
import { log } from '@dxos/log';
|
|
9
10
|
|
|
10
|
-
import { type DocAccessor } from './defs';
|
|
11
11
|
import { type CursorConverter } from '../cursor';
|
|
12
12
|
|
|
13
13
|
export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter => ({
|
|
@@ -5,9 +5,8 @@
|
|
|
5
5
|
//
|
|
6
6
|
|
|
7
7
|
import { Annotation, StateEffect, type StateField, type EditorState, type Transaction } from '@codemirror/state';
|
|
8
|
-
import get from 'lodash.get';
|
|
9
8
|
|
|
10
|
-
import { type
|
|
9
|
+
import { type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
11
10
|
|
|
12
11
|
export type State = {
|
|
13
12
|
path: Prop[];
|
|
@@ -31,23 +30,3 @@ export const reconcileAnnotation = Annotation.define<boolean>();
|
|
|
31
30
|
export const isReconcile = (tr: Transaction): boolean => {
|
|
32
31
|
return !!tr.annotation(reconcileAnnotation);
|
|
33
32
|
};
|
|
34
|
-
|
|
35
|
-
// TODO(burdon): Reconcile with echo.
|
|
36
|
-
export interface DocAccessor<T = any> {
|
|
37
|
-
handle: IDocHandle<T>;
|
|
38
|
-
get path(): string[]; // TODO(burdon): Getter or prop?
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// TODO(burdon): Reconcile with echo.
|
|
42
|
-
export const DocAccessor = {
|
|
43
|
-
getValue: (accessor: DocAccessor) => get(accessor.handle, accessor.path),
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// TODO(burdon): Reconcile with echo.
|
|
47
|
-
export interface IDocHandle<T = any> {
|
|
48
|
-
docSync(): Doc<T> | undefined;
|
|
49
|
-
change(callback: ChangeFn<T>, options?: ChangeOptions<T>): void;
|
|
50
|
-
changeAt(heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): string[] | undefined;
|
|
51
|
-
addListener(event: 'change', listener: () => void): void;
|
|
52
|
-
removeListener(event: 'change', listener: () => void): void;
|
|
53
|
-
}
|
|
@@ -8,16 +8,9 @@ import { type StateField } from '@codemirror/state';
|
|
|
8
8
|
import { type EditorView } from '@codemirror/view';
|
|
9
9
|
|
|
10
10
|
import { next as A } from '@dxos/automerge/automerge';
|
|
11
|
+
import { type IDocHandle } from '@dxos/echo-schema';
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
-
getLastHeads,
|
|
14
|
-
getPath,
|
|
15
|
-
type IDocHandle,
|
|
16
|
-
type State,
|
|
17
|
-
reconcileAnnotation,
|
|
18
|
-
updateHeads,
|
|
19
|
-
isReconcile,
|
|
20
|
-
} from './defs';
|
|
13
|
+
import { getLastHeads, getPath, type State, reconcileAnnotation, updateHeads, isReconcile } from './defs';
|
|
21
14
|
import { updateAutomerge } from './update-automerge';
|
|
22
15
|
import { updateCodeMirror } from './update-codemirror';
|
|
23
16
|
|
|
@@ -7,8 +7,9 @@
|
|
|
7
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
|
+
import { type IDocHandle } from '@dxos/echo-schema';
|
|
10
11
|
|
|
11
|
-
import { type
|
|
12
|
+
import { type State } from './defs';
|
|
12
13
|
|
|
13
14
|
export const updateAutomerge = (
|
|
14
15
|
field: StateField<State>,
|
|
@@ -9,7 +9,7 @@ import { log } from '@dxos/log';
|
|
|
9
9
|
import { type Space } from '@dxos/react-client/echo';
|
|
10
10
|
import { type GossipMessage } from '@dxos/react-client/mesh';
|
|
11
11
|
|
|
12
|
-
import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from '
|
|
12
|
+
import { type AwarenessInfo, type AwarenessPosition, type AwarenessProvider, type AwarenessState } from './awareness';
|
|
13
13
|
|
|
14
14
|
type ProtocolMessage =
|
|
15
15
|
| {
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { Event } from '@dxos/async';
|
|
17
17
|
import { Context } from '@dxos/context';
|
|
18
18
|
|
|
19
|
-
import { Cursor, type CursorConverter } from '
|
|
19
|
+
import { Cursor, type CursorConverter } from '../cursor';
|
|
20
20
|
|
|
21
21
|
export interface AwarenessProvider {
|
|
22
22
|
remoteStateChange: Event<void>;
|
|
@@ -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
|
};
|
|
@@ -74,7 +74,7 @@ export const commandState = StateField.define<CommandState>({
|
|
|
74
74
|
|
|
75
75
|
return state;
|
|
76
76
|
},
|
|
77
|
-
provide: (
|
|
77
|
+
provide: (field) => [showTooltip.from(field, (value) => value.tooltip ?? null)],
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
export const openEffect = StateEffect.define<{ pos: number; fullWidth?: boolean }>();
|
|
@@ -13,7 +13,7 @@ import { log } from '@dxos/log';
|
|
|
13
13
|
import { nonNullable } from '@dxos/util';
|
|
14
14
|
|
|
15
15
|
import { Cursor } from './cursor';
|
|
16
|
-
import { type Comment, type Range } from '
|
|
16
|
+
import { type Comment, type Range } from './types';
|
|
17
17
|
import { getToken } from '../styles';
|
|
18
18
|
import { callbackWrapper } from '../util';
|
|
19
19
|
|
|
@@ -508,12 +508,16 @@ 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, id: string, comments
|
|
511
|
+
export const useComments = (view: EditorView | null | undefined, id: string, comments?: Comment[]) => {
|
|
512
512
|
useEffect(() => {
|
|
513
513
|
if (view) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
514
|
+
// Check same document.
|
|
515
|
+
// NOTE: Hook might be called before editor state is updated.
|
|
516
|
+
if (id === view.state.facet(documentId)) {
|
|
517
|
+
view.dispatch({
|
|
518
|
+
effects: setComments.of({ id, comments: comments ?? [] }),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
517
521
|
}
|
|
518
522
|
}, [id, view, comments]);
|
|
519
523
|
};
|
package/src/extensions/cursor.ts
CHANGED
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
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Facet } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
import { invariant } from '@dxos/invariant';
|
|
8
|
+
|
|
9
|
+
export const documentId = Facet.define<string, string>({
|
|
10
|
+
combine: (providers) => {
|
|
11
|
+
invariant(providers.length <= 1);
|
|
12
|
+
return providers[0];
|
|
13
|
+
},
|
|
14
|
+
});
|