@dxos/react-ui-editor 0.4.3 → 0.4.4-main.01e2847
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 +200 -142
- 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 +3 -2
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -3
- 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 +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +15 -7
- 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/markdown/code.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/hooks/useEditorView.d.ts +6 -4
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +3 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/layout.d.ts +4 -1
- package/dist/types/src/styles/layout.d.ts.map +1 -1
- package/package.json +24 -24
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +3 -2
- package/src/components/TextEditor/TextEditor.tsx +3 -3
- package/src/components/TextEditor/hooks.stories.tsx +4 -4
- package/src/components/Toolbar/Toolbar.stories.tsx +9 -6
- package/src/components/Toolbar/Toolbar.tsx +10 -13
- package/src/extensions/autocomplete.ts +1 -1
- package/src/extensions/automerge/cursor.ts +15 -4
- package/src/extensions/automerge/defs.ts +0 -1
- package/src/extensions/comments.ts +111 -82
- package/src/extensions/cursor.ts +4 -2
- package/src/extensions/markdown/code.ts +37 -25
- package/src/extensions/markdown/formatting.test.ts +1 -1
- package/src/extensions/markdown/formatting.ts +1 -0
- package/src/hooks/useEditorView.ts +13 -11
- package/src/hooks/useTextModel.ts +7 -7
- package/src/index.ts +7 -1
- package/src/styles/layout.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-editor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4-main.01e2847",
|
|
4
4
|
"description": "Document editing experience within a DXOS shell.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -36,20 +36,20 @@
|
|
|
36
36
|
"lodash.pick": "^4.4.0",
|
|
37
37
|
"lodash.sortby": "^4.7.0",
|
|
38
38
|
"style-mod": "^4.1.0",
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
42
|
-
"@dxos/
|
|
43
|
-
"@dxos/display-name": "0.4.
|
|
44
|
-
"@dxos/
|
|
45
|
-
"@dxos/
|
|
46
|
-
"@dxos/
|
|
47
|
-
"@dxos/protocols": "0.4.
|
|
48
|
-
"@dxos/react-async": "0.4.
|
|
49
|
-
"@dxos/react-ui
|
|
50
|
-
"@dxos/react-ui": "0.4.
|
|
51
|
-
"@dxos/
|
|
52
|
-
"@dxos/
|
|
39
|
+
"@dxos/context": "0.4.4-main.01e2847",
|
|
40
|
+
"@dxos/async": "0.4.4-main.01e2847",
|
|
41
|
+
"@dxos/debug": "0.4.4-main.01e2847",
|
|
42
|
+
"@dxos/automerge": "0.4.4-main.01e2847",
|
|
43
|
+
"@dxos/display-name": "0.4.4-main.01e2847",
|
|
44
|
+
"@dxos/log": "0.4.4-main.01e2847",
|
|
45
|
+
"@dxos/echo-schema": "0.4.4-main.01e2847",
|
|
46
|
+
"@dxos/invariant": "0.4.4-main.01e2847",
|
|
47
|
+
"@dxos/protocols": "0.4.4-main.01e2847",
|
|
48
|
+
"@dxos/react-async": "0.4.4-main.01e2847",
|
|
49
|
+
"@dxos/react-ui": "0.4.4-main.01e2847",
|
|
50
|
+
"@dxos/react-ui-theme": "0.4.4-main.01e2847",
|
|
51
|
+
"@dxos/util": "0.4.4-main.01e2847",
|
|
52
|
+
"@dxos/text-model": "0.4.4-main.01e2847"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@automerge/automerge-repo-network-broadcastchannel": "^1.1.1",
|
|
@@ -76,20 +76,20 @@
|
|
|
76
76
|
"vite-plugin-top-level-await": "^1.4.1",
|
|
77
77
|
"vite-plugin-wasm": "^3.3.0",
|
|
78
78
|
"vitest": "^1.2.1",
|
|
79
|
-
"@dxos/
|
|
80
|
-
"@dxos/echo-
|
|
81
|
-
"@dxos/
|
|
82
|
-
"@dxos/
|
|
83
|
-
"@dxos/react-ui": "0.4.
|
|
84
|
-
"@dxos/
|
|
85
|
-
"@dxos/random": "0.4.
|
|
86
|
-
"@dxos/
|
|
79
|
+
"@dxos/echo-signals": "0.4.4-main.01e2847",
|
|
80
|
+
"@dxos/echo-typegen": "0.4.4-main.01e2847",
|
|
81
|
+
"@dxos/keyboard": "0.4.4-main.01e2847",
|
|
82
|
+
"@dxos/react-client": "0.4.4-main.01e2847",
|
|
83
|
+
"@dxos/react-ui": "0.4.4-main.01e2847",
|
|
84
|
+
"@dxos/storybook-utils": "0.4.4-main.01e2847",
|
|
85
|
+
"@dxos/random": "0.4.4-main.01e2847",
|
|
86
|
+
"@dxos/config": "0.4.4-main.01e2847"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"@phosphor-icons/react": "^2.0.5",
|
|
90
90
|
"react": "^18.2.0",
|
|
91
91
|
"react-dom": "^18.2.0",
|
|
92
|
-
"@dxos/react-client": "0.4.
|
|
92
|
+
"@dxos/react-client": "0.4.4-main.01e2847"
|
|
93
93
|
},
|
|
94
94
|
"publishConfig": {
|
|
95
95
|
"access": "public"
|
|
@@ -211,15 +211,16 @@ const onRenderLink: LinkOptions['onRender'] = (el, url) => {
|
|
|
211
211
|
};
|
|
212
212
|
|
|
213
213
|
type StoryProps = {
|
|
214
|
+
id?: string;
|
|
214
215
|
text?: string;
|
|
215
216
|
comments?: Comment[];
|
|
216
217
|
} & Pick<TextEditorProps, 'readonly' | 'placeholder' | 'extensions'>;
|
|
217
218
|
|
|
218
|
-
const Story = ({ text, comments, placeholder = 'New document.', ...props }: StoryProps) => {
|
|
219
|
+
const Story = ({ id = 'test', text, comments, placeholder = 'New document.', ...props }: StoryProps) => {
|
|
219
220
|
const [item] = useState({ text: new TextObject(text) });
|
|
220
221
|
const view = useRef<EditorView>(null);
|
|
221
222
|
const model = useTextModel({ text: item.text });
|
|
222
|
-
useComments(view.current, comments);
|
|
223
|
+
useComments(view.current, id, comments);
|
|
223
224
|
if (!model) {
|
|
224
225
|
return null;
|
|
225
226
|
}
|
|
@@ -65,7 +65,7 @@ export type TextEditorProps = {
|
|
|
65
65
|
* Base text editor.
|
|
66
66
|
*/
|
|
67
67
|
// TODO(burdon): Replace with useTextEditor.
|
|
68
|
-
export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
68
|
+
export const BaseTextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
69
69
|
(
|
|
70
70
|
{ model, readonly, autoFocus, scrollTo, selection, theme, slots = defaultSlots, extensions = [], debug },
|
|
71
71
|
forwardedRef,
|
|
@@ -202,7 +202,7 @@ export const BaseTextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
202
202
|
},
|
|
203
203
|
);
|
|
204
204
|
|
|
205
|
-
export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
205
|
+
export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
206
206
|
({ readonly, placeholder, lineWrapping, theme = textTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
207
207
|
const { themeMode } = useThemeContext();
|
|
208
208
|
const updatedSlots = defaultsDeep({}, slots, defaultTextSlots);
|
|
@@ -219,7 +219,7 @@ export const TextEditor = forwardRef<EditorView, TextEditorProps>(
|
|
|
219
219
|
},
|
|
220
220
|
);
|
|
221
221
|
|
|
222
|
-
export const MarkdownEditor = forwardRef<EditorView, TextEditorProps>(
|
|
222
|
+
export const MarkdownEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
223
223
|
({ readonly, placeholder, theme = markdownTheme, slots, extensions = [], ...props }, forwardedRef) => {
|
|
224
224
|
const { themeMode } = useThemeContext();
|
|
225
225
|
const updatedSlots = defaultsDeep({}, slots, defaultMarkdownSlots);
|
|
@@ -7,7 +7,7 @@ import '@dxosTheme';
|
|
|
7
7
|
import React from 'react';
|
|
8
8
|
|
|
9
9
|
import { useThemeContext } from '@dxos/react-ui';
|
|
10
|
-
import { mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
10
|
+
import { attentionSurface, mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
11
|
import { withTheme } from '@dxos/storybook-utils';
|
|
12
12
|
|
|
13
13
|
import { TextEditor } from './TextEditor';
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
useFormattingState,
|
|
25
25
|
} from '../../extensions';
|
|
26
26
|
import { createDataExtensions, createThemeExtensions, useActionHandler, useTextEditor } from '../../hooks';
|
|
27
|
-
import { editorWithToolbarLayout } from '../../styles
|
|
27
|
+
import { editorFillLayoutEditor, editorFillLayoutRoot, editorWithToolbarLayout } from '../../styles';
|
|
28
28
|
import { markdownTheme } from '../../themes';
|
|
29
29
|
import translations from '../../translations';
|
|
30
30
|
import { Toolbar } from '../Toolbar';
|
|
@@ -55,7 +55,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
55
55
|
themeMode,
|
|
56
56
|
theme: markdownTheme,
|
|
57
57
|
slots: {
|
|
58
|
-
editor: { className:
|
|
58
|
+
editor: { className: editorFillLayoutEditor },
|
|
59
59
|
},
|
|
60
60
|
}),
|
|
61
61
|
// TODO(burdon): Move lineWrapping.
|
|
@@ -85,7 +85,7 @@ const Story = ({ autoFocus, placeholder, doc, readonly }: StoryProps) => {
|
|
|
85
85
|
<div role='none' className='overflow-y-auto'>
|
|
86
86
|
<div
|
|
87
87
|
role='textbox'
|
|
88
|
-
className={mx(textBlockWidth, '
|
|
88
|
+
className={mx(textBlockWidth, attentionSurface, editorFillLayoutRoot, 'p-4')}
|
|
89
89
|
ref={parentRef}
|
|
90
90
|
/>
|
|
91
91
|
</div>
|
|
@@ -25,16 +25,16 @@ import {
|
|
|
25
25
|
useFormattingState,
|
|
26
26
|
} from '../../extensions';
|
|
27
27
|
import { type Comment, useActionHandler, useEditorView, useTextModel } from '../../hooks';
|
|
28
|
-
import { editorWithToolbarLayout } from '../../styles';
|
|
28
|
+
import { editorFillLayoutEditor, editorFillLayoutRoot, editorWithToolbarLayout } from '../../styles';
|
|
29
29
|
import translations from '../../translations';
|
|
30
30
|
import { MarkdownEditor } from '../TextEditor';
|
|
31
31
|
|
|
32
32
|
faker.seed(101);
|
|
33
33
|
|
|
34
|
-
const Story: FC<{ content: string }> = ({ content }) => {
|
|
34
|
+
const Story: FC<{ id?: string; content: string }> = ({ id = 'test', content }) => {
|
|
35
35
|
const [item] = useState({ text: new TextObject(content) });
|
|
36
36
|
const [_comments, setComments] = useState<Comment[]>([]);
|
|
37
|
-
const [editorRef,
|
|
37
|
+
const [editorRef, viewInvalidated] = useEditorView(id);
|
|
38
38
|
const model = useTextModel({ text: item.text });
|
|
39
39
|
const [formattingState, formattingObserver] = useFormattingState();
|
|
40
40
|
const extensions = useMemo(
|
|
@@ -57,8 +57,8 @@ const Story: FC<{ content: string }> = ({ content }) => {
|
|
|
57
57
|
[],
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
-
useComments(
|
|
61
|
-
const handleAction = useActionHandler(
|
|
60
|
+
useComments(viewInvalidated ? null : editorRef.current, id, _comments);
|
|
61
|
+
const handleAction = useActionHandler(editorRef.current);
|
|
62
62
|
|
|
63
63
|
if (!model) {
|
|
64
64
|
return null;
|
|
@@ -75,7 +75,10 @@ const Story: FC<{ content: string }> = ({ content }) => {
|
|
|
75
75
|
ref={editorRef}
|
|
76
76
|
model={model}
|
|
77
77
|
extensions={extensions}
|
|
78
|
-
slots={{
|
|
78
|
+
slots={{
|
|
79
|
+
root: { className: mx(textBlockWidth, editorFillLayoutRoot, 'pli-2') },
|
|
80
|
+
editor: { className: editorFillLayoutEditor },
|
|
81
|
+
}}
|
|
79
82
|
/>
|
|
80
83
|
</div>
|
|
81
84
|
);
|
|
@@ -122,7 +122,7 @@ const ToolbarButton = ({ Icon, children, ...props }: ToolbarButtonProps) => {
|
|
|
122
122
|
const ToolbarSeparator = () => <div role='separator' className='grow' />;
|
|
123
123
|
|
|
124
124
|
const MarkdownHeading = () => {
|
|
125
|
-
|
|
125
|
+
const { t } = useTranslation(translationKey);
|
|
126
126
|
const { onAction, state } = useToolbarContext('MarkdownFormatting');
|
|
127
127
|
const blockType = state ? state.blockType : 'paragraph';
|
|
128
128
|
const header = blockType && /heading(\d)/.exec(blockType);
|
|
@@ -134,15 +134,12 @@ const MarkdownHeading = () => {
|
|
|
134
134
|
value={value ?? '0'}
|
|
135
135
|
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
136
136
|
>
|
|
137
|
-
<
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{/* <CaretDown className={getSize(2)} weight='bold' /> */}
|
|
144
|
-
{/* </NaturalToolbar.Button> */}
|
|
145
|
-
</Select.TriggerButton>
|
|
137
|
+
<NaturalToolbar.Button asChild>
|
|
138
|
+
<Select.TriggerButton variant='ghost' classNames={buttonStyles}>
|
|
139
|
+
<span className='sr-only'>{t('heading label')}</span>
|
|
140
|
+
<HeadingIcon className={iconStyles} />
|
|
141
|
+
</Select.TriggerButton>
|
|
142
|
+
</NaturalToolbar.Button>
|
|
146
143
|
<Select.Portal>
|
|
147
144
|
<Select.Content>
|
|
148
145
|
<Select.ScrollUpButton />
|
|
@@ -211,7 +208,7 @@ const MarkdownLists = () => {
|
|
|
211
208
|
const { onAction, state } = useToolbarContext('MarkdownStyles');
|
|
212
209
|
const { t } = useTranslation(translationKey);
|
|
213
210
|
return (
|
|
214
|
-
<NaturalToolbar.ToggleGroup type='single' value={state?.listStyle
|
|
211
|
+
<NaturalToolbar.ToggleGroup type='single' value={state?.listStyle ? `list-${state.listStyle}` : ''}>
|
|
215
212
|
{markdownLists.map(({ type, getState, Icon }) => (
|
|
216
213
|
<ToolbarButton
|
|
217
214
|
key={type}
|
|
@@ -235,7 +232,7 @@ const markdownBlocks: ButtonProps[] = [
|
|
|
235
232
|
{
|
|
236
233
|
type: 'codeblock',
|
|
237
234
|
Icon: CodeBlock,
|
|
238
|
-
getState: (state) => state.
|
|
235
|
+
getState: (state) => state.blockType === 'codeblock',
|
|
239
236
|
disabled: (state) => !state.blankLine,
|
|
240
237
|
},
|
|
241
238
|
{
|
|
@@ -251,7 +248,7 @@ const MarkdownBlocks = () => {
|
|
|
251
248
|
const { t } = useTranslation(translationKey);
|
|
252
249
|
const value = markdownBlocks.find(({ getState }) => state && getState(state));
|
|
253
250
|
return (
|
|
254
|
-
<NaturalToolbar.ToggleGroup type='single' value={value?.type}>
|
|
251
|
+
<NaturalToolbar.ToggleGroup type='single' value={value?.type ?? ''}>
|
|
255
252
|
{markdownBlocks.map(({ type, disabled, getState, Icon }) => (
|
|
256
253
|
<ToolbarButton
|
|
257
254
|
key={type}
|
|
@@ -35,7 +35,7 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
|
35
35
|
// https://codemirror.net/examples/autocompletion
|
|
36
36
|
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
37
37
|
autocompletion({
|
|
38
|
-
activateOnTyping:
|
|
38
|
+
activateOnTyping: false,
|
|
39
39
|
|
|
40
40
|
// closeOnBlur: false,
|
|
41
41
|
// defaultKeymap: false,
|
|
@@ -6,6 +6,7 @@ import get from 'lodash.get';
|
|
|
6
6
|
|
|
7
7
|
import type { Prop } from '@dxos/automerge/automerge';
|
|
8
8
|
import { next as A } from '@dxos/automerge/automerge';
|
|
9
|
+
import { log } from '@dxos/log';
|
|
9
10
|
|
|
10
11
|
import { type IDocHandle } from './defs';
|
|
11
12
|
import { type CursorConverter } from '../cursor';
|
|
@@ -23,8 +24,13 @@ export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConvert
|
|
|
23
24
|
return 'end';
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
try {
|
|
28
|
+
// NOTE: Slice is needed because getCursor mutates the array.
|
|
29
|
+
return A.getCursor(doc, path.slice(), pos);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
log.catch(err);
|
|
32
|
+
return ''; // In case of invalid request (e.g., wrong document).
|
|
33
|
+
}
|
|
28
34
|
},
|
|
29
35
|
|
|
30
36
|
fromCursor: (cursor) => {
|
|
@@ -46,7 +52,12 @@ export const cursorConverter = (handle: IDocHandle, path: Prop[]): CursorConvert
|
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
try {
|
|
56
|
+
// NOTE: Slice is needed because getCursor mutates the array.
|
|
57
|
+
return A.getCursorPosition(doc, path.slice(), cursor);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
log.catch(err);
|
|
60
|
+
return 0; // In case of invalid request (e.g., wrong document).
|
|
61
|
+
}
|
|
51
62
|
},
|
|
52
63
|
});
|
|
@@ -35,7 +35,6 @@ export type IDocHandle<T = any> = {
|
|
|
35
35
|
docSync(): Doc<T> | undefined;
|
|
36
36
|
change(callback: ChangeFn<T>, options?: ChangeOptions<T>): void;
|
|
37
37
|
changeAt(heads: Heads, callback: ChangeFn<T>, options?: ChangeOptions<T>): string[] | undefined;
|
|
38
|
-
|
|
39
38
|
addListener(event: 'change', listener: () => void): void;
|
|
40
39
|
removeListener(event: 'change', listener: () => void): void;
|
|
41
40
|
};
|
|
@@ -16,31 +16,11 @@ import { type Comment, type Range } from '../hooks';
|
|
|
16
16
|
import { getToken } from '../styles';
|
|
17
17
|
import { callbackWrapper } from '../util';
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'&dark .cm-comment, &dark .cm-comment-current': { mixBlendMode: 'plus-lighter' },
|
|
23
|
-
'&light .cm-comment': {
|
|
24
|
-
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
25
|
-
},
|
|
26
|
-
'&light .cm-comment-current': {
|
|
27
|
-
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
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
|
-
},
|
|
37
|
-
});
|
|
19
|
+
//
|
|
20
|
+
// State management.
|
|
21
|
+
//
|
|
38
22
|
|
|
39
|
-
const
|
|
40
|
-
const commentCurrentMark = Decoration.mark({
|
|
41
|
-
class: 'cm-comment-current',
|
|
42
|
-
attributes: { 'data-testid': 'cm-comment' },
|
|
43
|
-
});
|
|
23
|
+
const documentId = Facet.define<string | undefined, string | undefined>({ combine: (values) => values[0] });
|
|
44
24
|
|
|
45
25
|
type CommentState = {
|
|
46
26
|
comment: Comment;
|
|
@@ -54,30 +34,12 @@ type SelectionState = {
|
|
|
54
34
|
};
|
|
55
35
|
|
|
56
36
|
export type CommentsState = {
|
|
37
|
+
id?: string;
|
|
57
38
|
comments: CommentState[];
|
|
58
39
|
selection: SelectionState;
|
|
59
40
|
};
|
|
60
41
|
|
|
61
|
-
export const
|
|
62
|
-
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
|
|
63
|
-
if (!comment?.comment.cursor) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const range = Cursor.getRangeFromCursor(view.state, comment.comment.cursor);
|
|
68
|
-
if (range) {
|
|
69
|
-
view.dispatch({
|
|
70
|
-
selection: { anchor: range.from },
|
|
71
|
-
effects: [
|
|
72
|
-
//
|
|
73
|
-
EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
|
|
74
|
-
setSelection.of({ current: id }),
|
|
75
|
-
],
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export const setComments = StateEffect.define<Comment[]>();
|
|
42
|
+
export const setComments = StateEffect.define<{ id: string; comments: Comment[] }>();
|
|
81
43
|
|
|
82
44
|
export const setSelection = StateEffect.define<SelectionState>();
|
|
83
45
|
|
|
@@ -88,7 +50,7 @@ const setCommentState = StateEffect.define<CommentsState>();
|
|
|
88
50
|
* The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
|
|
89
51
|
*/
|
|
90
52
|
const commentsState = StateField.define<CommentsState>({
|
|
91
|
-
create: () => ({ comments: [], selection: {} }),
|
|
53
|
+
create: (state) => ({ id: state.facet(documentId), comments: [], selection: {} }),
|
|
92
54
|
update: (value, tr) => {
|
|
93
55
|
for (const effect of tr.effects) {
|
|
94
56
|
// Update selection.
|
|
@@ -98,7 +60,8 @@ const commentsState = StateField.define<CommentsState>({
|
|
|
98
60
|
|
|
99
61
|
// Update range from store.
|
|
100
62
|
if (effect.is(setComments)) {
|
|
101
|
-
const comments
|
|
63
|
+
const { comments } = effect.value;
|
|
64
|
+
const commentStates: CommentState[] = comments
|
|
102
65
|
.map((comment) => {
|
|
103
66
|
// Skip cut/deleted comments.
|
|
104
67
|
if (!comment.cursor) {
|
|
@@ -110,7 +73,7 @@ const commentsState = StateField.define<CommentsState>({
|
|
|
110
73
|
})
|
|
111
74
|
.filter(nonNullable);
|
|
112
75
|
|
|
113
|
-
return { ...value, comments };
|
|
76
|
+
return { ...value, comments: commentStates };
|
|
114
77
|
}
|
|
115
78
|
|
|
116
79
|
// Update entire state.
|
|
@@ -123,6 +86,35 @@ const commentsState = StateField.define<CommentsState>({
|
|
|
123
86
|
},
|
|
124
87
|
});
|
|
125
88
|
|
|
89
|
+
//
|
|
90
|
+
// UX
|
|
91
|
+
//
|
|
92
|
+
|
|
93
|
+
const styles = EditorView.baseTheme({
|
|
94
|
+
'&light .cm-comment, &light .cm-comment-current': { mixBlendMode: 'darken' },
|
|
95
|
+
'&dark .cm-comment, &dark .cm-comment-current': { mixBlendMode: 'plus-lighter' },
|
|
96
|
+
'&light .cm-comment': {
|
|
97
|
+
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
98
|
+
},
|
|
99
|
+
'&light .cm-comment-current': {
|
|
100
|
+
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
101
|
+
},
|
|
102
|
+
'&dark .cm-comment': {
|
|
103
|
+
color: getToken('extend.colors.yellow.50'),
|
|
104
|
+
backgroundColor: getToken('extend.colors.yellow.900'),
|
|
105
|
+
},
|
|
106
|
+
'&dark .cm-comment-current': {
|
|
107
|
+
color: getToken('extend.colors.yellow.100'),
|
|
108
|
+
backgroundColor: getToken('extend.colors.yellow.950'),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const commentMark = Decoration.mark({ class: 'cm-comment', attributes: { 'data-testid': 'cm-comment' } });
|
|
113
|
+
const commentCurrentMark = Decoration.mark({
|
|
114
|
+
class: 'cm-comment-current',
|
|
115
|
+
attributes: { 'data-testid': 'cm-comment' },
|
|
116
|
+
});
|
|
117
|
+
|
|
126
118
|
/**
|
|
127
119
|
* Decorate ranges.
|
|
128
120
|
*/
|
|
@@ -151,32 +143,9 @@ const commentsDecorations = EditorView.decorations.compute([commentsState], (sta
|
|
|
151
143
|
return Decoration.set(decorations);
|
|
152
144
|
});
|
|
153
145
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
*/
|
|
158
|
-
key?: string;
|
|
159
|
-
/**
|
|
160
|
-
* Called to create a new thread and return the thread id.
|
|
161
|
-
*/
|
|
162
|
-
onCreate?: (params: { cursor: string; from: number; location?: Rect | null }) => string | undefined;
|
|
163
|
-
/**
|
|
164
|
-
* Selection cut/deleted.
|
|
165
|
-
*/
|
|
166
|
-
onDelete?: (params: { id: string }) => void;
|
|
167
|
-
/**
|
|
168
|
-
* Called when a comment is moved.
|
|
169
|
-
*/
|
|
170
|
-
onUpdate?: (params: { id: string; cursor: string }) => void;
|
|
171
|
-
/**
|
|
172
|
-
* Called to notify which thread is currently closest to the cursor.
|
|
173
|
-
*/
|
|
174
|
-
onSelect?: (state: CommentsState) => void;
|
|
175
|
-
/**
|
|
176
|
-
* Called to render tooltip.
|
|
177
|
-
*/
|
|
178
|
-
onHover?: (el: Element, shortcut: string) => void;
|
|
179
|
-
};
|
|
146
|
+
//
|
|
147
|
+
// Cut-and-paste.
|
|
148
|
+
//
|
|
180
149
|
|
|
181
150
|
type TrackedComment = { id: string; from: number; to: number };
|
|
182
151
|
|
|
@@ -288,14 +257,12 @@ const mapTrackedComment = (comment: TrackedComment, changes: ChangeDesc) => ({
|
|
|
288
257
|
to: changes.mapPos(comment.to, 1),
|
|
289
258
|
});
|
|
290
259
|
|
|
291
|
-
|
|
292
|
-
|
|
260
|
+
/**
|
|
261
|
+
* These are attached to undone/redone transactions in the editor for the purpose of restoring comments
|
|
262
|
+
* that were deleted by the original changes.
|
|
263
|
+
*/
|
|
293
264
|
const restoreCommentEffect = StateEffect.define<TrackedComment>({ map: mapTrackedComment });
|
|
294
265
|
|
|
295
|
-
const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({
|
|
296
|
-
combine: (providers) => providers[0],
|
|
297
|
-
});
|
|
298
|
-
|
|
299
266
|
/**
|
|
300
267
|
* Create comment thread action.
|
|
301
268
|
*/
|
|
@@ -334,6 +301,43 @@ export const createComment: Command = (view) => {
|
|
|
334
301
|
return false;
|
|
335
302
|
};
|
|
336
303
|
|
|
304
|
+
//
|
|
305
|
+
// Options
|
|
306
|
+
//
|
|
307
|
+
|
|
308
|
+
export type CommentsOptions = {
|
|
309
|
+
/**
|
|
310
|
+
* Document id.
|
|
311
|
+
*/
|
|
312
|
+
id?: string;
|
|
313
|
+
/**
|
|
314
|
+
* Key shortcut to create a new thread.
|
|
315
|
+
*/
|
|
316
|
+
key?: string;
|
|
317
|
+
/**
|
|
318
|
+
* Called to create a new thread and return the thread id.
|
|
319
|
+
*/
|
|
320
|
+
onCreate?: (params: { cursor: string; from: number; location?: Rect | null }) => string | undefined;
|
|
321
|
+
/**
|
|
322
|
+
* Selection cut/deleted.
|
|
323
|
+
*/
|
|
324
|
+
onDelete?: (params: { id: string }) => void;
|
|
325
|
+
/**
|
|
326
|
+
* Called when a comment is moved.
|
|
327
|
+
*/
|
|
328
|
+
onUpdate?: (params: { id: string; cursor: string }) => void;
|
|
329
|
+
/**
|
|
330
|
+
* Called to notify which thread is currently closest to the cursor.
|
|
331
|
+
*/
|
|
332
|
+
onSelect?: (state: CommentsState) => void;
|
|
333
|
+
/**
|
|
334
|
+
* Called to render tooltip.
|
|
335
|
+
*/
|
|
336
|
+
onHover?: (el: Element, shortcut: string) => void;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({ combine: (providers) => providers[0] });
|
|
340
|
+
|
|
337
341
|
/**
|
|
338
342
|
* Comment threads.
|
|
339
343
|
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
@@ -352,6 +356,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
352
356
|
|
|
353
357
|
return [
|
|
354
358
|
optionsFacet.of(options),
|
|
359
|
+
documentId.of(options.id),
|
|
355
360
|
commentsState,
|
|
356
361
|
commentsDecorations,
|
|
357
362
|
styles,
|
|
@@ -462,6 +467,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
462
467
|
// Update callback.
|
|
463
468
|
handleSelect({
|
|
464
469
|
selection,
|
|
470
|
+
id: state.facet(documentId),
|
|
465
471
|
comments: comments.map(({ comment, range }) => ({
|
|
466
472
|
comment,
|
|
467
473
|
range,
|
|
@@ -475,15 +481,38 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
475
481
|
];
|
|
476
482
|
};
|
|
477
483
|
|
|
484
|
+
//
|
|
485
|
+
// Utils.
|
|
486
|
+
//
|
|
487
|
+
|
|
488
|
+
export const focusComment = (view: EditorView, id: string, center = true) => {
|
|
489
|
+
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
|
|
490
|
+
if (!comment?.comment.cursor) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const range = Cursor.getRangeFromCursor(view.state, comment.comment.cursor);
|
|
495
|
+
if (range) {
|
|
496
|
+
view.dispatch({
|
|
497
|
+
selection: { anchor: range.from },
|
|
498
|
+
effects: [
|
|
499
|
+
//
|
|
500
|
+
EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
|
|
501
|
+
setSelection.of({ current: id }),
|
|
502
|
+
],
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
478
507
|
/**
|
|
479
508
|
* Update comments state field.
|
|
480
509
|
*/
|
|
481
|
-
export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
|
|
510
|
+
export const useComments = (view: EditorView | null, id: string, comments: Comment[] = []) => {
|
|
482
511
|
useEffect(() => {
|
|
483
512
|
if (view) {
|
|
484
513
|
view.dispatch({
|
|
485
|
-
effects: setComments.of(comments),
|
|
514
|
+
effects: setComments.of({ id, comments }),
|
|
486
515
|
});
|
|
487
516
|
}
|
|
488
|
-
}, [view, comments]);
|
|
517
|
+
}, [id, view, comments]);
|
|
489
518
|
};
|
package/src/extensions/cursor.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { Range } from '../hooks';
|
|
|
14
14
|
* - https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
15
15
|
*
|
|
16
16
|
* @param {assoc} number Negative values will associate the cursor with the previous character
|
|
17
|
-
*
|
|
17
|
+
* while positive - with the next one.
|
|
18
18
|
*/
|
|
19
19
|
export interface CursorConverter {
|
|
20
20
|
toCursor(position: number, assoc?: -1 | 1 | undefined): string;
|
|
@@ -28,7 +28,9 @@ const defaultCursorConverter: CursorConverter = {
|
|
|
28
28
|
|
|
29
29
|
export class Cursor {
|
|
30
30
|
static readonly converter = Facet.define<CursorConverter, CursorConverter>({
|
|
31
|
-
combine: (providers) =>
|
|
31
|
+
combine: (providers) => {
|
|
32
|
+
return providers[0] ?? defaultCursorConverter;
|
|
33
|
+
},
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
static readonly getCursorFromRange = (state: EditorState, range: Range) => {
|