@dxos/react-ui-editor 0.3.11-main.c244d7e → 0.3.11-main.c865ffb
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 +1385 -929
- 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 +7 -11
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +3 -2
- 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/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/{yjs.stories.d.ts → hooks.stories.d.ts} +13 -2
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- 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/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +5 -5
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +2 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/awareness.d.ts +2 -4
- package/dist/types/src/extensions/awareness.d.ts.map +1 -1
- package/dist/types/src/extensions/basic.d.ts +2 -2
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +9 -5
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +0 -7
- package/dist/types/src/extensions/index.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/code.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts +13 -0
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/heading.d.ts +6 -0
- package/dist/types/src/extensions/markdown/heading.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts +8 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/tasklist.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -1
- package/dist/types/src/hooks/awareness-provider.d.ts +7 -4
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -1
- package/dist/types/src/hooks/defs.d.ts +1 -4
- package/dist/types/src/hooks/defs.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +3 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
- 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 +39 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +12 -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/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/package.json +46 -36
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +41 -46
- package/src/components/TextEditor/TextEditor.tsx +46 -27
- package/src/components/TextEditor/automerge.stories.tsx +1 -2
- package/src/components/TextEditor/hooks.stories.tsx +106 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
- package/src/components/Toolbar/Toolbar.tsx +166 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +1 -0
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +44 -11
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +9 -9
- package/src/extensions/automerge/semaphore.ts +18 -24
- package/src/extensions/automerge/update-automerge.ts +12 -6
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +21 -35
- package/src/extensions/basic.ts +17 -15
- package/src/extensions/blast.ts +4 -1
- package/src/extensions/comments.ts +126 -78
- package/src/extensions/index.ts +0 -7
- package/src/extensions/markdown/bundle.ts +9 -8
- package/src/extensions/markdown/code.ts +150 -0
- package/src/extensions/markdown/formatting.ts +188 -0
- package/src/extensions/markdown/heading.ts +59 -0
- package/src/extensions/markdown/highlight.ts +9 -15
- package/src/extensions/{hr.ts → markdown/hr.ts} +3 -4
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +13 -4
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +2 -3
- package/src/extensions/mention.ts +6 -4
- package/src/hooks/awareness-provider.ts +24 -20
- package/src/hooks/defs.ts +1 -6
- package/src/hooks/index.ts +3 -2
- package/src/hooks/useActionHandler.ts +62 -0
- package/src/hooks/useEditorView.ts +29 -0
- package/src/hooks/useTextEditor.ts +153 -0
- package/src/hooks/useTextModel.ts +65 -12
- package/src/index.ts +1 -1
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +9 -6
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts +0 -12
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +0 -26
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -4
- package/dist/types/src/hooks/automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs.d.ts +0 -28
- package/dist/types/src/hooks/yjs.d.ts.map +0 -1
- package/dist/types/src/testing/index.d.ts +0 -3
- package/dist/types/src/testing/index.d.ts.map +0 -1
- package/dist/types/src/testing/proto/gen/schema.d.ts +0 -15
- package/dist/types/src/testing/proto/gen/schema.d.ts.map +0 -1
- package/dist/types/src/testing/proto/index.d.ts +0 -2
- package/dist/types/src/testing/proto/index.d.ts.map +0 -1
- package/dist/types/src/testing/replicator.d.ts +0 -45
- package/dist/types/src/testing/replicator.d.ts.map +0 -1
- package/src/components/TextEditor/codemirror.stories.ts +0 -115
- package/src/components/TextEditor/comments.stories.tsx +0 -357
- package/src/components/TextEditor/yjs.stories.tsx +0 -56
- package/src/extensions/code.ts +0 -104
- package/src/extensions/yjs/cursor.ts +0 -22
- package/src/extensions/yjs/index.ts +0 -5
- package/src/extensions/yjs/yjs.test.ts +0 -35
- package/src/extensions/yjs/yjs.ts +0 -16
- package/src/hooks/automerge.ts +0 -53
- package/src/hooks/yjs.ts +0 -157
- package/src/testing/index.ts +0 -6
- package/src/testing/proto/gen/schema.ts +0 -49
- package/src/testing/proto/index.ts +0 -5
- package/src/testing/proto/schema.proto +0 -15
- package/src/testing/replicator.ts +0 -184
- /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
- /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
- /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
- /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
- /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
- /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
- /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
- /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
|
@@ -12,16 +12,11 @@ import {
|
|
|
12
12
|
type ViewUpdate,
|
|
13
13
|
WidgetType,
|
|
14
14
|
} from '@codemirror/view';
|
|
15
|
-
import { useEffect } from 'react';
|
|
16
15
|
|
|
17
16
|
import { Event } from '@dxos/async';
|
|
18
17
|
import { Context } from '@dxos/context';
|
|
19
|
-
import { generateName } from '@dxos/display-name';
|
|
20
|
-
import { useThemeContext } from '@dxos/react-ui';
|
|
21
|
-
import { getColorForValue } from '@dxos/react-ui-theme';
|
|
22
18
|
|
|
23
19
|
import { Cursor, type CursorConverter } from './cursor';
|
|
24
|
-
import { type EditorModel } from '../hooks';
|
|
25
20
|
|
|
26
21
|
export interface AwarenessProvider {
|
|
27
22
|
remoteStateChange: Event<void>;
|
|
@@ -29,8 +24,8 @@ export interface AwarenessProvider {
|
|
|
29
24
|
open(): void;
|
|
30
25
|
close(): void;
|
|
31
26
|
|
|
32
|
-
update(position: AwarenessPosition | undefined): void;
|
|
33
27
|
getRemoteStates(): AwarenessState[];
|
|
28
|
+
update(position: AwarenessPosition | undefined): void;
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
const dummyProvider: AwarenessProvider = {
|
|
@@ -39,11 +34,11 @@ const dummyProvider: AwarenessProvider = {
|
|
|
39
34
|
open: () => {},
|
|
40
35
|
close: () => {},
|
|
41
36
|
|
|
42
|
-
update: () => {},
|
|
43
37
|
getRemoteStates: () => [],
|
|
38
|
+
update: () => {},
|
|
44
39
|
};
|
|
45
40
|
|
|
46
|
-
export const
|
|
41
|
+
export const awarenessProvider = Facet.define<AwarenessProvider, AwarenessProvider>({
|
|
47
42
|
combine: (providers) => providers[0] ?? dummyProvider,
|
|
48
43
|
});
|
|
49
44
|
|
|
@@ -73,7 +68,7 @@ export type AwarenessState = {
|
|
|
73
68
|
*/
|
|
74
69
|
export const awareness = (provider = dummyProvider): Extension => {
|
|
75
70
|
return [
|
|
76
|
-
|
|
71
|
+
awarenessProvider.of(provider),
|
|
77
72
|
ViewPlugin.fromClass(RemoteSelectionsDecorator, {
|
|
78
73
|
decorations: (value) => value.decorations,
|
|
79
74
|
}),
|
|
@@ -81,19 +76,6 @@ export const awareness = (provider = dummyProvider): Extension => {
|
|
|
81
76
|
];
|
|
82
77
|
};
|
|
83
78
|
|
|
84
|
-
export const useAwareness = ({ awareness, peer }: EditorModel) => {
|
|
85
|
-
const { themeMode } = useThemeContext();
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
if (awareness && peer) {
|
|
88
|
-
awareness.setLocalStateField('user', {
|
|
89
|
-
name: peer.name ?? generateName(peer.id),
|
|
90
|
-
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
91
|
-
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}, [awareness, peer, themeMode]);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
79
|
/**
|
|
98
80
|
* Generates selection decorations from remote peers.
|
|
99
81
|
*/
|
|
@@ -109,7 +91,7 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
109
91
|
|
|
110
92
|
constructor(view: EditorView) {
|
|
111
93
|
this._cursorConverter = view.state.facet(Cursor.converter);
|
|
112
|
-
this._provider = view.state.facet(
|
|
94
|
+
this._provider = view.state.facet(awarenessProvider);
|
|
113
95
|
this._provider.open();
|
|
114
96
|
this._provider.remoteStateChange.on(this._ctx, () => {
|
|
115
97
|
view.dispatch({ annotations: [RemoteSelectionChangedAnnotation.of([])] });
|
|
@@ -225,7 +207,10 @@ export class RemoteSelectionsDecorator implements PluginValue {
|
|
|
225
207
|
}
|
|
226
208
|
|
|
227
209
|
class RemoteCaretWidget extends WidgetType {
|
|
228
|
-
constructor(
|
|
210
|
+
constructor(
|
|
211
|
+
private readonly _name: string,
|
|
212
|
+
private readonly _color: string,
|
|
213
|
+
) {
|
|
229
214
|
super();
|
|
230
215
|
}
|
|
231
216
|
|
|
@@ -268,7 +253,6 @@ class RemoteCaretWidget extends WidgetType {
|
|
|
268
253
|
}
|
|
269
254
|
}
|
|
270
255
|
|
|
271
|
-
// TODO(burdon): Mixed case?
|
|
272
256
|
const styles = EditorView.baseTheme({
|
|
273
257
|
'.cm-collab-selection': {},
|
|
274
258
|
'.cm-collab-selectionLine': {
|
|
@@ -283,38 +267,40 @@ const styles = EditorView.baseTheme({
|
|
|
283
267
|
marginRight: '-1px',
|
|
284
268
|
boxSizing: 'border-box',
|
|
285
269
|
display: 'inline',
|
|
270
|
+
cursor: 'pointer',
|
|
286
271
|
},
|
|
287
272
|
'.cm-collab-selectionCaretDot': {
|
|
288
273
|
borderRadius: '50%',
|
|
289
274
|
position: 'absolute',
|
|
290
|
-
width: '.
|
|
291
|
-
height: '.
|
|
292
|
-
top: '-.
|
|
293
|
-
left: '-.
|
|
275
|
+
width: '.5em',
|
|
276
|
+
height: '.5em',
|
|
277
|
+
top: '-.25em',
|
|
278
|
+
left: '-.25em',
|
|
294
279
|
backgroundColor: 'inherit',
|
|
295
280
|
transition: 'transform .3s ease-in-out',
|
|
296
281
|
boxSizing: 'border-box',
|
|
297
282
|
},
|
|
298
283
|
'.cm-collab-selectionCaret:hover > .cm-collab-selectionCaretDot': {
|
|
299
|
-
transformOrigin: 'bottom center',
|
|
300
284
|
transform: 'scale(0)',
|
|
285
|
+
transformOrigin: 'center',
|
|
301
286
|
},
|
|
302
287
|
'.cm-collab-selectionInfo': {
|
|
303
288
|
position: 'absolute',
|
|
304
|
-
|
|
305
|
-
|
|
289
|
+
transform: 'translate(-50%, 0)',
|
|
290
|
+
top: '-20px',
|
|
291
|
+
left: 0,
|
|
306
292
|
fontSize: '.75em',
|
|
307
|
-
fontFamily: 'serif',
|
|
293
|
+
fontFamily: 'sans-serif',
|
|
308
294
|
fontStyle: 'normal',
|
|
309
295
|
fontWeight: 'normal',
|
|
310
296
|
lineHeight: 'normal',
|
|
311
297
|
userSelect: 'none',
|
|
312
298
|
color: 'white',
|
|
313
|
-
|
|
314
|
-
paddingRight: '2px',
|
|
299
|
+
padding: '2px',
|
|
315
300
|
zIndex: 101,
|
|
316
301
|
transition: 'opacity .3s ease-in-out',
|
|
317
302
|
backgroundColor: 'inherit',
|
|
303
|
+
borderRadius: '2px',
|
|
318
304
|
// These should be separate.
|
|
319
305
|
opacity: 0,
|
|
320
306
|
transitionDelay: '0s',
|
package/src/extensions/basic.ts
CHANGED
|
@@ -10,28 +10,30 @@ 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
|
|
|
16
17
|
export type BasicBundleOptions = {
|
|
17
18
|
themeMode?: ThemeMode;
|
|
18
|
-
} & Pick<TextEditorProps, 'placeholder' | '
|
|
19
|
+
} & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
|
|
19
20
|
|
|
20
|
-
export const
|
|
21
|
+
export const createBasicBundle = ({
|
|
21
22
|
themeMode,
|
|
22
23
|
placeholder: _placeholder,
|
|
23
|
-
|
|
24
|
-
}: BasicBundleOptions): Extension[] =>
|
|
25
|
-
|
|
24
|
+
lineWrapping = true,
|
|
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);
|
package/src/extensions/blast.ts
CHANGED
|
@@ -125,7 +125,10 @@ class Blaster {
|
|
|
125
125
|
|
|
126
126
|
_lastPoint = { x: 0, y: 0 };
|
|
127
127
|
|
|
128
|
-
constructor(
|
|
128
|
+
constructor(
|
|
129
|
+
private readonly _node: HTMLElement,
|
|
130
|
+
private readonly _options: BlastOptions,
|
|
131
|
+
) {
|
|
129
132
|
this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
|
|
130
133
|
}
|
|
131
134
|
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { invertedEffects } from '@codemirror/commands';
|
|
6
|
+
import { type Extension, Facet, StateEffect, StateField, type Text, type ChangeDesc } from '@codemirror/state';
|
|
6
7
|
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
7
8
|
import sortBy from 'lodash.sortby';
|
|
8
9
|
import { useEffect } from 'react';
|
|
9
10
|
|
|
10
11
|
import { debounce } from '@dxos/async';
|
|
11
|
-
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { nonNullable } from '@dxos/util';
|
|
13
13
|
|
|
14
14
|
import { Cursor } from './cursor';
|
|
@@ -21,15 +21,13 @@ const styles = EditorView.baseTheme({
|
|
|
21
21
|
'& .cm-comment': {
|
|
22
22
|
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
23
23
|
},
|
|
24
|
-
'& .cm-comment-
|
|
24
|
+
'& .cm-comment-current': {
|
|
25
25
|
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
26
26
|
},
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
highlightActive: Decoration.mark({ class: 'cm-comment-active' }),
|
|
32
|
-
};
|
|
29
|
+
const commentMark = Decoration.mark({ class: 'cm-comment' });
|
|
30
|
+
const commentCurrentMark = Decoration.mark({ class: 'cm-comment-current' });
|
|
33
31
|
|
|
34
32
|
type CommentState = {
|
|
35
33
|
comment: Comment;
|
|
@@ -38,7 +36,7 @@ type CommentState = {
|
|
|
38
36
|
};
|
|
39
37
|
|
|
40
38
|
type SelectionState = {
|
|
41
|
-
|
|
39
|
+
current?: string;
|
|
42
40
|
closest?: string;
|
|
43
41
|
};
|
|
44
42
|
|
|
@@ -47,7 +45,7 @@ export type CommentsState = {
|
|
|
47
45
|
selection: SelectionState;
|
|
48
46
|
};
|
|
49
47
|
|
|
50
|
-
export const
|
|
48
|
+
export const focusComment = (view: EditorView, id: string, center = true) => {
|
|
51
49
|
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
|
|
52
50
|
if (!comment?.comment.cursor) {
|
|
53
51
|
return;
|
|
@@ -60,7 +58,7 @@ export const setFocus = (view: EditorView, id: string, center = true) => {
|
|
|
60
58
|
effects: [
|
|
61
59
|
//
|
|
62
60
|
EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
|
|
63
|
-
setSelection.of({
|
|
61
|
+
setSelection.of({ current: id }),
|
|
64
62
|
],
|
|
65
63
|
});
|
|
66
64
|
}
|
|
@@ -115,9 +113,9 @@ const commentsState = StateField.define<CommentsState>({
|
|
|
115
113
|
/**
|
|
116
114
|
* Decorate ranges.
|
|
117
115
|
*/
|
|
118
|
-
const
|
|
116
|
+
const commentsDecorations = EditorView.decorations.compute([commentsState], (state) => {
|
|
119
117
|
const {
|
|
120
|
-
selection: {
|
|
118
|
+
selection: { current },
|
|
121
119
|
comments,
|
|
122
120
|
} = state.field(commentsState);
|
|
123
121
|
|
|
@@ -129,10 +127,10 @@ const highlightDecorations = EditorView.decorations.compute([commentsState], (st
|
|
|
129
127
|
return undefined;
|
|
130
128
|
}
|
|
131
129
|
|
|
132
|
-
if (comment.comment.id ===
|
|
133
|
-
return
|
|
130
|
+
if (comment.comment.id === current) {
|
|
131
|
+
return commentCurrentMark.range(range.from, range.to);
|
|
134
132
|
} else {
|
|
135
|
-
return
|
|
133
|
+
return commentMark.range(range.from, range.to);
|
|
136
134
|
}
|
|
137
135
|
})
|
|
138
136
|
.filter(nonNullable);
|
|
@@ -167,11 +165,11 @@ export type CommentsOptions = {
|
|
|
167
165
|
onHover?: (el: Element, shortcut: string) => void;
|
|
168
166
|
};
|
|
169
167
|
|
|
170
|
-
|
|
168
|
+
type TrackedComment = { id: string; from: number; to: number };
|
|
169
|
+
|
|
171
170
|
const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
|
|
172
171
|
// Tracks indexed selections within text.
|
|
173
|
-
|
|
174
|
-
let tracked: { text: Text; comments: { id: string; from: number; to: number }[] } | null = null;
|
|
172
|
+
let tracked: { text: Text; comments: TrackedComment[] } | null = null;
|
|
175
173
|
|
|
176
174
|
// Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
|
|
177
175
|
const handleTrack = (event: Event, view: EditorView) => {
|
|
@@ -201,16 +199,48 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
201
199
|
copy: handleTrack,
|
|
202
200
|
}),
|
|
203
201
|
|
|
204
|
-
//
|
|
205
|
-
|
|
202
|
+
// Track deleted comments.
|
|
203
|
+
invertedEffects.of((tr) => {
|
|
204
|
+
const { comments } = tr.startState.field(commentsState);
|
|
205
|
+
const effects: StateEffect<any>[] = [];
|
|
206
|
+
tr.changes.iterChangedRanges((fromA, toA) => {
|
|
207
|
+
for (const {
|
|
208
|
+
comment: { id },
|
|
209
|
+
range: { from, to },
|
|
210
|
+
} of comments) {
|
|
211
|
+
if (from < to && from >= fromA && to <= toA) {
|
|
212
|
+
effects.push(restoreCommentEffect.of({ id, from, to }));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return effects;
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
// Handle paste or the undo of comment deletion.
|
|
221
|
+
EditorView.updateListener.of((update) => {
|
|
222
|
+
const restore: TrackedComment[] = [];
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < update.transactions.length; i++) {
|
|
225
|
+
const tr = update.transactions[i];
|
|
226
|
+
for (let j = 0; j < restore.length; j++) {
|
|
227
|
+
restore[j] = mapTrackedComment(restore[j], tr.changes);
|
|
228
|
+
}
|
|
229
|
+
for (const effect of tr.effects) {
|
|
230
|
+
if (effect.is(restoreCommentEffect)) {
|
|
231
|
+
restore.push(effect.value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
206
236
|
if (tracked) {
|
|
207
|
-
const paste = transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
237
|
+
const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
208
238
|
if (paste) {
|
|
209
239
|
let found = -1;
|
|
210
240
|
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
211
241
|
if (text.eq(tracked!.text)) {
|
|
212
|
-
for (let i = transactions.indexOf(paste!) + 1; i < transactions.length; i++) {
|
|
213
|
-
fromB = transactions[i].changes.mapPos(fromB);
|
|
242
|
+
for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
|
|
243
|
+
fromB = update.transactions[i].changes.mapPos(fromB);
|
|
214
244
|
}
|
|
215
245
|
|
|
216
246
|
found = fromB;
|
|
@@ -219,23 +249,78 @@ const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>)
|
|
|
219
249
|
|
|
220
250
|
if (found > -1) {
|
|
221
251
|
for (const moved of tracked.comments) {
|
|
222
|
-
|
|
223
|
-
from: found + moved.from,
|
|
224
|
-
to: found + moved.to,
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
const cursor = Cursor.getCursorFromRange(state, range);
|
|
228
|
-
onUpdate(moved.id, cursor);
|
|
252
|
+
restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
|
|
229
253
|
}
|
|
230
254
|
}
|
|
231
255
|
|
|
232
256
|
tracked = null;
|
|
233
257
|
}
|
|
234
258
|
}
|
|
259
|
+
|
|
260
|
+
for (const comment of restore) {
|
|
261
|
+
const { comments } = update.startState.field(commentsState);
|
|
262
|
+
const exists = comments.some((c) => c.comment.id === comment.id && c.range.from < c.range.to);
|
|
263
|
+
if (!exists) {
|
|
264
|
+
const cursor = Cursor.getCursorFromRange(update.state, comment);
|
|
265
|
+
onUpdate(comment.id, cursor);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
235
268
|
}),
|
|
236
269
|
];
|
|
237
270
|
};
|
|
238
271
|
|
|
272
|
+
const mapTrackedComment = (comment: TrackedComment, changes: ChangeDesc) => ({
|
|
273
|
+
id: comment.id,
|
|
274
|
+
from: changes.mapPos(comment.from, 1),
|
|
275
|
+
to: changes.mapPos(comment.to, 1),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// These are attached to undone/redone transactions in the editor for the purpose of restoring comments
|
|
279
|
+
// that were deleted by the original changes.
|
|
280
|
+
const restoreCommentEffect = StateEffect.define<TrackedComment>({ map: mapTrackedComment });
|
|
281
|
+
|
|
282
|
+
const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({
|
|
283
|
+
combine: (providers) => providers[0],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create comment thread action.
|
|
288
|
+
*/
|
|
289
|
+
export const createComment: Command = (view) => {
|
|
290
|
+
const options = view.state.facet(optionsFacet);
|
|
291
|
+
const { from, to } = view.state.selection.main;
|
|
292
|
+
if (from === to) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Don't allow selection at end of document.
|
|
297
|
+
if (to === view.state.doc.length) {
|
|
298
|
+
view.dispatch({
|
|
299
|
+
changes: {
|
|
300
|
+
from: to,
|
|
301
|
+
insert: '\n',
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
307
|
+
if (cursor) {
|
|
308
|
+
// Create thread via callback.
|
|
309
|
+
const id = options.onCreate?.(cursor, view.coordsAtPos(from));
|
|
310
|
+
if (id) {
|
|
311
|
+
// Update range.
|
|
312
|
+
view.dispatch({
|
|
313
|
+
effects: setSelection.of({ current: id }),
|
|
314
|
+
selection: { anchor: to },
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return false;
|
|
322
|
+
};
|
|
323
|
+
|
|
239
324
|
/**
|
|
240
325
|
* Comment threads.
|
|
241
326
|
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
@@ -252,47 +337,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
252
337
|
|
|
253
338
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
254
339
|
|
|
255
|
-
/**
|
|
256
|
-
* Create comment thread action.
|
|
257
|
-
*/
|
|
258
|
-
const createCommentThread: Command = (view) => {
|
|
259
|
-
invariant(options.onCreate);
|
|
260
|
-
const { from, to } = view.state.selection.main;
|
|
261
|
-
if (from === to) {
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Don't allow selection at end of document.
|
|
266
|
-
if (to === view.state.doc.length) {
|
|
267
|
-
view.dispatch({
|
|
268
|
-
changes: {
|
|
269
|
-
from: to,
|
|
270
|
-
insert: '\n',
|
|
271
|
-
},
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const cursor = Cursor.getCursorFromRange(view.state, { from, to });
|
|
276
|
-
if (cursor) {
|
|
277
|
-
// Create thread via callback.
|
|
278
|
-
const id = options.onCreate?.(cursor, view.coordsAtPos(from));
|
|
279
|
-
if (id) {
|
|
280
|
-
// Update range.
|
|
281
|
-
view.dispatch({
|
|
282
|
-
effects: setSelection.of({ active: id }),
|
|
283
|
-
selection: { anchor: from },
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return false;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
340
|
return [
|
|
341
|
+
optionsFacet.of(options),
|
|
294
342
|
commentsState,
|
|
295
|
-
|
|
343
|
+
commentsDecorations,
|
|
296
344
|
styles,
|
|
297
345
|
|
|
298
346
|
//
|
|
@@ -302,7 +350,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
302
350
|
? keymap.of([
|
|
303
351
|
{
|
|
304
352
|
key: shortcut,
|
|
305
|
-
run: callbackWrapper(
|
|
353
|
+
run: callbackWrapper(createComment),
|
|
306
354
|
},
|
|
307
355
|
])
|
|
308
356
|
: [],
|
|
@@ -371,10 +419,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
371
419
|
//
|
|
372
420
|
// Track selection/proximity.
|
|
373
421
|
//
|
|
374
|
-
EditorView.updateListener.of(({ view, state
|
|
422
|
+
EditorView.updateListener.of(({ view, state }) => {
|
|
375
423
|
let min = Infinity;
|
|
376
424
|
const {
|
|
377
|
-
selection: {
|
|
425
|
+
selection: { current, closest },
|
|
378
426
|
comments,
|
|
379
427
|
} = state.field(commentsState);
|
|
380
428
|
|
|
@@ -382,11 +430,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
382
430
|
const selection: SelectionState = {};
|
|
383
431
|
comments.forEach(({ comment, range }) => {
|
|
384
432
|
if (head >= range.from && head <= range.to) {
|
|
385
|
-
selection.
|
|
433
|
+
selection.current = comment.id;
|
|
386
434
|
selection.closest = undefined;
|
|
387
435
|
}
|
|
388
436
|
|
|
389
|
-
if (!selection.
|
|
437
|
+
if (!selection.current) {
|
|
390
438
|
const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
|
|
391
439
|
if (d < min) {
|
|
392
440
|
selection.closest = comment.id;
|
|
@@ -395,7 +443,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
395
443
|
}
|
|
396
444
|
});
|
|
397
445
|
|
|
398
|
-
if (selection.
|
|
446
|
+
if (selection.current !== current || selection.closest !== closest) {
|
|
399
447
|
view.dispatch({ effects: setSelection.of(selection) });
|
|
400
448
|
|
|
401
449
|
// Update callback.
|
|
@@ -415,11 +463,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
415
463
|
};
|
|
416
464
|
|
|
417
465
|
/**
|
|
418
|
-
* Update state field.
|
|
466
|
+
* Update comments state field.
|
|
419
467
|
*/
|
|
420
|
-
export const useComments = (view
|
|
468
|
+
export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
|
|
421
469
|
useEffect(() => {
|
|
422
|
-
if (view
|
|
470
|
+
if (view) {
|
|
423
471
|
view.dispatch({
|
|
424
472
|
effects: setComments.of(comments),
|
|
425
473
|
});
|
package/src/extensions/index.ts
CHANGED
|
@@ -7,17 +7,10 @@ export * from './automerge';
|
|
|
7
7
|
export * from './awareness';
|
|
8
8
|
export * from './basic';
|
|
9
9
|
export * from './blast';
|
|
10
|
-
export * from './code';
|
|
11
10
|
export * from './comments';
|
|
12
11
|
export * from './cursor';
|
|
13
|
-
export * from './hr';
|
|
14
|
-
export * from './image';
|
|
15
|
-
export * from './link';
|
|
16
12
|
export * from './listener';
|
|
17
13
|
export * from './markdown';
|
|
18
14
|
export * from './mention';
|
|
19
15
|
export * from './outliner';
|
|
20
|
-
export * from './table';
|
|
21
|
-
export * from './tasklist';
|
|
22
16
|
export * from './typewriter';
|
|
23
|
-
export * from './yjs';
|
|
@@ -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, '
|
|
31
|
+
} & Pick<TextEditorProps, 'placeholder' | 'lineWrapping'>;
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Markdown bundle.
|
|
@@ -36,15 +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
|
-
readonly,
|
|
40
|
+
export const createMarkdownExtensions = ({
|
|
41
41
|
themeMode,
|
|
42
42
|
placeholder: _placeholder,
|
|
43
|
-
|
|
43
|
+
lineWrapping = true,
|
|
44
|
+
}: MarkdownBundleOptions = {}): Extension[] => {
|
|
44
45
|
return [
|
|
46
|
+
lineWrapping && EditorView.lineWrapping,
|
|
45
47
|
EditorState.allowMultipleSelections.of(true),
|
|
46
48
|
EditorState.tabSize.of(2),
|
|
47
|
-
EditorView.lineWrapping,
|
|
48
49
|
|
|
49
50
|
// https://github.com/codemirror/basic-setup
|
|
50
51
|
bracketMatching(),
|
|
@@ -55,7 +56,7 @@ export const markdownBundle = ({
|
|
|
55
56
|
highlightActiveLine(),
|
|
56
57
|
history(),
|
|
57
58
|
indentOnInput(),
|
|
58
|
-
_placeholder
|
|
59
|
+
_placeholder && placeholder(_placeholder),
|
|
59
60
|
|
|
60
61
|
// Main extension.
|
|
61
62
|
// https://github.com/codemirror/lang-markdown
|
|
@@ -82,7 +83,7 @@ export const markdownBundle = ({
|
|
|
82
83
|
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
83
84
|
|
|
84
85
|
// Custom styles.
|
|
85
|
-
syntaxHighlighting(markdownHighlightStyle(
|
|
86
|
+
syntaxHighlighting(markdownHighlightStyle()),
|
|
86
87
|
|
|
87
88
|
keymap.of([
|
|
88
89
|
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
@@ -96,5 +97,5 @@ export const markdownBundle = ({
|
|
|
96
97
|
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
97
98
|
...standardKeymap,
|
|
98
99
|
]),
|
|
99
|
-
];
|
|
100
|
+
].filter(isNotFalsy);
|
|
100
101
|
};
|