@dxos/react-ui-editor 0.3.11-main.0e40963 → 0.3.11-main.101b0f0
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 +2483 -1557
- 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 +16 -14
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +20 -17
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -3
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/{hooks/automerge → components/TextEditor}/automerge.stories.d.ts +6 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts +23 -0
- 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/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
- 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/cursor.d.ts +5 -0
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +24 -0
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/index.d.ts +3 -0
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/semaphore.d.ts +15 -0
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness.d.ts +46 -0
- package/dist/types/src/extensions/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/basic.d.ts +4 -5
- package/dist/types/src/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +37 -18
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/cursor.d.ts +25 -0
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +4 -7
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/bundle.d.ts +3 -6
- 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/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- package/dist/types/src/hooks/awareness-provider.d.ts +31 -0
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
- package/dist/types/src/hooks/defs.d.ts +25 -0
- package/dist/types/src/hooks/defs.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +5 -1
- 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 +2 -31
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/cursors.d.ts.map +1 -0
- package/dist/types/src/styles/index.d.ts +1 -0
- package/dist/types/src/styles/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/styles/tokens.d.ts +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +7 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +48 -36
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +84 -91
- package/src/components/TextEditor/TextEditor.stories.tsx +13 -34
- package/src/components/TextEditor/TextEditor.tsx +136 -95
- package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +66 -6
- package/src/components/TextEditor/comments.stories.tsx +357 -0
- 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/autocomplete.ts +23 -19
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +103 -0
- package/src/extensions/automerge/cursor.ts +52 -0
- package/src/extensions/automerge/defs.ts +41 -0
- package/src/extensions/automerge/index.ts +6 -0
- package/src/extensions/automerge/semaphore.ts +81 -0
- package/src/extensions/automerge/update-automerge.ts +49 -0
- package/src/{hooks → extensions}/automerge/update-codemirror.ts +17 -9
- package/src/extensions/awareness.ts +310 -0
- package/src/extensions/basic.ts +17 -10
- package/src/extensions/comments.ts +331 -246
- package/src/extensions/cursor.ts +50 -0
- package/src/extensions/index.ts +4 -7
- package/src/extensions/markdown/bundle.ts +23 -28
- 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 +11 -26
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/markdown/image.ts +87 -0
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +87 -86
- package/src/extensions/{table.ts → markdown/table.ts} +10 -4
- package/src/extensions/markdown/tasklist.ts +110 -0
- package/src/extensions/mention.ts +9 -2
- package/src/extensions/outliner.ts +12 -0
- package/src/hooks/awareness-provider.ts +124 -0
- package/src/hooks/defs.ts +42 -0
- package/src/hooks/index.ts +6 -1
- 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 +33 -116
- package/src/index.ts +3 -0
- package/src/styles/index.ts +1 -0
- package/src/styles/markdown.ts +9 -12
- package/src/styles/tokens.ts +3 -0
- package/src/themes/default.ts +53 -80
- package/src/util.ts +41 -0
- 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/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/mermaid.d.ts +0 -3
- package/dist/types/src/extensions/mermaid.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/util.d.ts +0 -5
- package/dist/types/src/extensions/util.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
- package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/index.d.ts +0 -3
- package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/semaphore.d.ts +0 -19
- package/dist/types/src/hooks/automerge/semaphore.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/update-automerge.d.ts +0 -6
- package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/index.d.ts +0 -3
- package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/space-provider.d.ts +0 -27
- package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts +0 -12
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/hooks/yjs/yjs.test.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 -114
- package/src/extensions/code.ts +0 -99
- package/src/extensions/hr.ts +0 -74
- package/src/extensions/image.ts +0 -69
- package/src/extensions/mermaid.ts +0 -11
- package/src/extensions/tasklist.ts +0 -121
- package/src/extensions/util.ts +0 -18
- package/src/hooks/automerge/handle.ts +0 -14
- package/src/hooks/automerge/index.ts +0 -6
- package/src/hooks/automerge/plugin.ts +0 -134
- package/src/hooks/automerge/semaphore.ts +0 -78
- package/src/hooks/automerge/update-automerge.ts +0 -44
- package/src/hooks/yjs/index.ts +0 -6
- package/src/hooks/yjs/space-provider.ts +0 -126
- package/src/hooks/yjs/yjs.stories.tsx +0 -56
- package/src/hooks/yjs/yjs.test.ts +0 -59
- 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 -189
- /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.d.ts +0 -0
- /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/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
- /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
|
@@ -2,106 +2,102 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
keymap,
|
|
9
|
-
type Command,
|
|
10
|
-
type DecorationSet,
|
|
11
|
-
type ViewUpdate,
|
|
12
|
-
Decoration,
|
|
13
|
-
EditorView,
|
|
14
|
-
MatchDecorator,
|
|
15
|
-
ViewPlugin,
|
|
16
|
-
WidgetType,
|
|
17
|
-
type Rect,
|
|
18
|
-
} from '@codemirror/view';
|
|
5
|
+
import { invertedEffects } from '@codemirror/commands';
|
|
6
|
+
import { type Extension, Facet, StateEffect, StateField, type Text, type ChangeDesc } from '@codemirror/state';
|
|
7
|
+
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
19
8
|
import sortBy from 'lodash.sortby';
|
|
9
|
+
import { useEffect } from 'react';
|
|
20
10
|
|
|
21
11
|
import { debounce } from '@dxos/async';
|
|
22
|
-
import { invariant } from '@dxos/invariant';
|
|
23
|
-
import { log } from '@dxos/log';
|
|
24
12
|
import { nonNullable } from '@dxos/util';
|
|
25
13
|
|
|
26
|
-
import {
|
|
27
|
-
import { type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
|
|
31
|
-
// TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
|
|
14
|
+
import { Cursor } from './cursor';
|
|
15
|
+
import { type Comment, type Range } from '../hooks';
|
|
16
|
+
import { getToken } from '../styles';
|
|
17
|
+
import { callbackWrapper } from '../util';
|
|
32
18
|
|
|
33
19
|
// TODO(burdon): Reconcile with theme.
|
|
34
20
|
const styles = EditorView.baseTheme({
|
|
35
|
-
'& .cm-bookmark': {
|
|
36
|
-
cursor: 'pointer',
|
|
37
|
-
margin: '4px',
|
|
38
|
-
padding: '4px',
|
|
39
|
-
backgroundColor: 'yellow',
|
|
40
|
-
},
|
|
41
|
-
'& .cm-bookmark-selected': {
|
|
42
|
-
backgroundColor: 'orange',
|
|
43
|
-
},
|
|
44
21
|
'& .cm-comment': {
|
|
45
|
-
backgroundColor: 'yellow',
|
|
22
|
+
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
46
23
|
},
|
|
47
|
-
'& .cm-comment-
|
|
48
|
-
backgroundColor: '
|
|
24
|
+
'& .cm-comment-current': {
|
|
25
|
+
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
49
26
|
},
|
|
50
27
|
});
|
|
51
28
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
29
|
+
const commentMark = Decoration.mark({ class: 'cm-comment' });
|
|
30
|
+
const commentCurrentMark = Decoration.mark({ class: 'cm-comment-current' });
|
|
31
|
+
|
|
32
|
+
type CommentState = {
|
|
33
|
+
comment: Comment;
|
|
34
|
+
range: Range;
|
|
35
|
+
location?: Rect | null;
|
|
55
36
|
};
|
|
56
37
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
active?: string;
|
|
38
|
+
type SelectionState = {
|
|
39
|
+
current?: string;
|
|
60
40
|
closest?: string;
|
|
61
41
|
};
|
|
62
42
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
export type CommentsState = {
|
|
44
|
+
comments: CommentState[];
|
|
45
|
+
selection: SelectionState;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const focusComment = (view: EditorView, id: string, center = true) => {
|
|
49
|
+
const comment = view.state.field(commentsState).comments.find((range) => range.comment.id === id);
|
|
50
|
+
if (!comment?.comment.cursor) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
69
53
|
|
|
70
|
-
|
|
71
|
-
|
|
54
|
+
const range = Cursor.getRangeFromCursor(view.state, comment.comment.cursor);
|
|
55
|
+
if (range) {
|
|
56
|
+
view.dispatch({
|
|
57
|
+
selection: { anchor: range.from },
|
|
58
|
+
effects: [
|
|
59
|
+
//
|
|
60
|
+
EditorView.scrollIntoView(range.from, center ? { y: 'center' } : undefined),
|
|
61
|
+
setSelection.of({ current: id }),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
72
65
|
};
|
|
73
66
|
|
|
74
|
-
export const
|
|
67
|
+
export const setComments = StateEffect.define<Comment[]>();
|
|
75
68
|
|
|
76
|
-
const setSelection = StateEffect.define<
|
|
69
|
+
export const setSelection = StateEffect.define<SelectionState>();
|
|
77
70
|
|
|
78
71
|
const setCommentState = StateEffect.define<CommentsState>();
|
|
79
72
|
|
|
80
73
|
/**
|
|
81
74
|
* State field (reducer) that tracks comment ranges.
|
|
82
|
-
* The ranges are tracked as
|
|
83
|
-
* therefore must be updated when the document changes.
|
|
75
|
+
* The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
|
|
84
76
|
*/
|
|
85
|
-
const
|
|
86
|
-
create: () => ({
|
|
77
|
+
const commentsState = StateField.define<CommentsState>({
|
|
78
|
+
create: () => ({ comments: [], selection: {} }),
|
|
87
79
|
update: (value, tr) => {
|
|
88
80
|
for (const effect of tr.effects) {
|
|
89
81
|
// Update selection.
|
|
90
82
|
if (effect.is(setSelection)) {
|
|
91
|
-
return { ...value,
|
|
83
|
+
return { ...value, selection: effect.value };
|
|
92
84
|
}
|
|
93
85
|
|
|
94
86
|
// Update range from store.
|
|
95
|
-
if (effect.is(
|
|
96
|
-
const
|
|
97
|
-
const ranges: ExtendedCommentRange[] = comments
|
|
87
|
+
if (effect.is(setComments)) {
|
|
88
|
+
const comments: CommentState[] = effect.value
|
|
98
89
|
.map((comment) => {
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
// Skip cut/deleted comments.
|
|
91
|
+
if (!comment.cursor) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const range = Cursor.getRangeFromCursor(tr.state, comment.cursor);
|
|
96
|
+
return range && { comment, range };
|
|
101
97
|
})
|
|
102
98
|
.filter(nonNullable);
|
|
103
99
|
|
|
104
|
-
return { ...value,
|
|
100
|
+
return { ...value, comments };
|
|
105
101
|
}
|
|
106
102
|
|
|
107
103
|
// Update entire state.
|
|
@@ -117,64 +113,48 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
117
113
|
/**
|
|
118
114
|
* Decorate ranges.
|
|
119
115
|
*/
|
|
120
|
-
const
|
|
121
|
-
const {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
116
|
+
const commentsDecorations = EditorView.decorations.compute([commentsState], (state) => {
|
|
117
|
+
const {
|
|
118
|
+
selection: { current },
|
|
119
|
+
comments,
|
|
120
|
+
} = state.field(commentsState);
|
|
121
|
+
|
|
122
|
+
const decorations = sortBy(comments ?? [], (range) => range.range.from)
|
|
123
|
+
?.flatMap((comment) => {
|
|
124
|
+
const range = comment.range;
|
|
125
|
+
if (!range || range.from === range.to) {
|
|
126
|
+
console.warn('Invalid range:', range);
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.filter(nonNullable) ?? [];
|
|
130
|
+
if (comment.comment.id === current) {
|
|
131
|
+
return commentCurrentMark.range(range.from, range.to);
|
|
132
|
+
} else {
|
|
133
|
+
return commentMark.range(range.from, range.to);
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.filter(nonNullable);
|
|
138
137
|
|
|
139
138
|
return Decoration.set(decorations);
|
|
140
139
|
});
|
|
141
140
|
|
|
142
|
-
/**
|
|
143
|
-
* Optional bookmark widget at the start/end of the selection.
|
|
144
|
-
* May correspond to a markdown bookmark.
|
|
145
|
-
*/
|
|
146
|
-
class BookmarkWidget extends WidgetType {
|
|
147
|
-
constructor(
|
|
148
|
-
private readonly _anchor: number,
|
|
149
|
-
private readonly _id: string,
|
|
150
|
-
private readonly _handleClick?: () => void,
|
|
151
|
-
) {
|
|
152
|
-
super();
|
|
153
|
-
invariant(this._id);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
override eq(other: BookmarkWidget) {
|
|
157
|
-
return this._anchor === other._anchor && this._id === other._id;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
override toDOM() {
|
|
161
|
-
const span = document.createElement('span');
|
|
162
|
-
span.className = 'cm-bookmark';
|
|
163
|
-
span.textContent = '※';
|
|
164
|
-
if (this._handleClick) {
|
|
165
|
-
span.onclick = () => this._handleClick!();
|
|
166
|
-
}
|
|
167
|
-
return span;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
141
|
export type CommentsOptions = {
|
|
142
|
+
/**
|
|
143
|
+
* Key shortcut to create a new thread.
|
|
144
|
+
*/
|
|
172
145
|
key?: string;
|
|
173
|
-
footnote?: boolean;
|
|
174
146
|
/**
|
|
175
147
|
* Called to create a new thread and return the thread id.
|
|
176
148
|
*/
|
|
177
|
-
onCreate?: (
|
|
149
|
+
onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* Selection cut/deleted.
|
|
152
|
+
*/
|
|
153
|
+
onDelete?: (thread: string) => void;
|
|
154
|
+
/**
|
|
155
|
+
* Called when a comment is moved.
|
|
156
|
+
*/
|
|
157
|
+
onUpdate?: (thread: string, cursor: string) => void;
|
|
178
158
|
/**
|
|
179
159
|
* Called to notify which thread is currently closest to the cursor.
|
|
180
160
|
*/
|
|
@@ -182,102 +162,185 @@ export type CommentsOptions = {
|
|
|
182
162
|
/**
|
|
183
163
|
* Called to render tooltip.
|
|
184
164
|
*/
|
|
185
|
-
onHover?: (el: Element) => void;
|
|
165
|
+
onHover?: (el: Element, shortcut: string) => void;
|
|
186
166
|
};
|
|
187
167
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
168
|
+
type TrackedComment = { id: string; from: number; to: number };
|
|
169
|
+
|
|
170
|
+
const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
|
|
171
|
+
// Tracks indexed selections within text.
|
|
172
|
+
let tracked: { text: Text; comments: TrackedComment[] } | null = null;
|
|
173
|
+
|
|
174
|
+
// Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
|
|
175
|
+
const handleTrack = (event: Event, view: EditorView) => {
|
|
176
|
+
const comments = view.state.field(commentsState);
|
|
177
|
+
const { main } = view.state.selection;
|
|
178
|
+
const selectedRanges = comments.comments.filter(
|
|
179
|
+
({ range }) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (!selectedRanges.length) {
|
|
183
|
+
tracked = null;
|
|
184
|
+
} else {
|
|
185
|
+
tracked = {
|
|
186
|
+
text: view.state.doc.slice(main.from, main.to),
|
|
187
|
+
comments: selectedRanges.map(({ comment, range }) => ({
|
|
188
|
+
id: comment.id,
|
|
189
|
+
from: range.from - main.from,
|
|
190
|
+
to: range.to - main.from,
|
|
191
|
+
})),
|
|
192
|
+
};
|
|
211
193
|
}
|
|
194
|
+
};
|
|
212
195
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (id) {
|
|
219
|
-
// Update range.
|
|
220
|
-
view.dispatch({
|
|
221
|
-
effects: setSelection.of({ active: id }),
|
|
222
|
-
selection: { anchor: from },
|
|
223
|
-
});
|
|
196
|
+
return [
|
|
197
|
+
EditorView.domEventHandlers({
|
|
198
|
+
cut: handleTrack,
|
|
199
|
+
copy: handleTrack,
|
|
200
|
+
}),
|
|
224
201
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
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
|
+
}
|
|
232
214
|
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return effects;
|
|
218
|
+
}),
|
|
233
219
|
|
|
234
|
-
|
|
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
|
+
}
|
|
235
234
|
}
|
|
236
|
-
}
|
|
237
235
|
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
if (tracked) {
|
|
237
|
+
const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
238
|
+
if (paste) {
|
|
239
|
+
let found = -1;
|
|
240
|
+
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
241
|
+
if (text.eq(tracked!.text)) {
|
|
242
|
+
for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
|
|
243
|
+
fromB = update.transactions[i].changes.mapPos(fromB);
|
|
244
|
+
}
|
|
240
245
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
*/
|
|
244
|
-
const bookmarksViewPlugin = ViewPlugin.fromClass(
|
|
245
|
-
class BookmarkViewPlugin {
|
|
246
|
-
// Match markdown footnotes (option).
|
|
247
|
-
// https://www.markdownguide.org/extended-syntax/#footnotes
|
|
248
|
-
static bookmarkMatcher = new MatchDecorator({
|
|
249
|
-
regexp: /\[\^(\w+)\]/g,
|
|
250
|
-
decoration: (match, view, pos) => {
|
|
251
|
-
const id = match[1];
|
|
252
|
-
return Decoration.replace({
|
|
253
|
-
id,
|
|
254
|
-
widget: new BookmarkWidget(pos, id),
|
|
246
|
+
found = fromB;
|
|
247
|
+
}
|
|
255
248
|
});
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
249
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
250
|
+
if (found > -1) {
|
|
251
|
+
for (const moved of tracked.comments) {
|
|
252
|
+
restore.push({ id: moved.id, from: found + moved.from, to: found + moved.to });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
tracked = null;
|
|
257
|
+
}
|
|
262
258
|
}
|
|
263
259
|
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
}
|
|
266
267
|
}
|
|
267
|
-
},
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
268
|
+
}),
|
|
269
|
+
];
|
|
270
|
+
};
|
|
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
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Comment threads.
|
|
326
|
+
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
327
|
+
* Since the selections are relative, they do not need to be updated when the document is edited.
|
|
328
|
+
* 2). Implements a StateField to track absolute selections corresponding to the comments (i.e., when the document is edited).
|
|
329
|
+
* 3). Creates decoration marks to apply classes to each selection.
|
|
330
|
+
* 4). Tracks the current cursor position to:
|
|
331
|
+
* a). Update the decoration to show if the cursor is within a current selection.
|
|
332
|
+
* b). Calls a handler to indicate which is the closest selection (e.g., to update the thread sidebar).
|
|
333
|
+
* 5). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
334
|
+
*/
|
|
335
|
+
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
336
|
+
const { key: shortcut = "meta-'" } = options;
|
|
337
|
+
|
|
338
|
+
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
276
339
|
|
|
277
340
|
return [
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
341
|
+
optionsFacet.of(options),
|
|
342
|
+
commentsState,
|
|
343
|
+
commentsDecorations,
|
|
281
344
|
styles,
|
|
282
345
|
|
|
283
346
|
//
|
|
@@ -286,8 +349,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
286
349
|
options.onCreate
|
|
287
350
|
? keymap.of([
|
|
288
351
|
{
|
|
289
|
-
key:
|
|
290
|
-
run: callbackWrapper(
|
|
352
|
+
key: shortcut,
|
|
353
|
+
run: callbackWrapper(createComment),
|
|
291
354
|
},
|
|
292
355
|
])
|
|
293
356
|
: [],
|
|
@@ -306,9 +369,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
306
369
|
end: selection.to,
|
|
307
370
|
above: true,
|
|
308
371
|
create: () => {
|
|
309
|
-
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
310
372
|
const el = document.createElement('div');
|
|
311
|
-
options.onHover
|
|
373
|
+
options.onHover!(el, shortcut);
|
|
312
374
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
313
375
|
},
|
|
314
376
|
};
|
|
@@ -316,76 +378,99 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
316
378
|
|
|
317
379
|
return null;
|
|
318
380
|
},
|
|
319
|
-
{
|
|
381
|
+
{
|
|
382
|
+
// TODO(burdon): Hide on change triggered immediately?
|
|
383
|
+
// hideOnChange: true,
|
|
384
|
+
hoverTime: 1_000,
|
|
385
|
+
},
|
|
320
386
|
)
|
|
321
387
|
: [],
|
|
322
388
|
|
|
323
389
|
//
|
|
324
|
-
//
|
|
325
|
-
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
390
|
+
// Track deleted ranges and update ranges for decorations.
|
|
326
391
|
//
|
|
327
|
-
EditorView.updateListener.of(({ view, state,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
ranges.forEach((range) => {
|
|
338
|
-
if (from2 === to2) {
|
|
339
|
-
const newRange = model.getRangeFromCursor!(range.cursor);
|
|
340
|
-
if (!newRange || newRange.to - newRange.from === 0) {
|
|
341
|
-
// TODO(burdon): Delete range if empty.
|
|
342
|
-
log.info('deleted comment', { thread: range.id });
|
|
343
|
-
}
|
|
392
|
+
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
393
|
+
let mod = false;
|
|
394
|
+
const { comments, ...value } = state.field(commentsState);
|
|
395
|
+
changes.iterChanges((from, to, from2, to2) => {
|
|
396
|
+
comments.forEach(({ comment, range }) => {
|
|
397
|
+
// Test if range deleted.
|
|
398
|
+
if (from2 === to2) {
|
|
399
|
+
const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
|
|
400
|
+
if (!newRange || newRange.to - newRange.from === 0) {
|
|
401
|
+
options.onDelete?.(comment.id);
|
|
344
402
|
}
|
|
403
|
+
}
|
|
345
404
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
405
|
+
// Update range.
|
|
406
|
+
if (from <= range.to) {
|
|
407
|
+
const newRange = Cursor.getRangeFromCursor(view.state, comment.cursor!);
|
|
408
|
+
Object.assign(range, newRange);
|
|
409
|
+
mod = true;
|
|
410
|
+
}
|
|
352
411
|
});
|
|
412
|
+
});
|
|
353
413
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
414
|
+
if (mod) {
|
|
415
|
+
view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
|
|
357
416
|
}
|
|
417
|
+
}),
|
|
358
418
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
419
|
+
//
|
|
420
|
+
// Track selection/proximity.
|
|
421
|
+
//
|
|
422
|
+
EditorView.updateListener.of(({ view, state }) => {
|
|
423
|
+
let min = Infinity;
|
|
424
|
+
const {
|
|
425
|
+
selection: { current, closest },
|
|
426
|
+
comments,
|
|
427
|
+
} = state.field(commentsState);
|
|
428
|
+
|
|
429
|
+
const { head } = state.selection.main;
|
|
430
|
+
const selection: SelectionState = {};
|
|
431
|
+
comments.forEach(({ comment, range }) => {
|
|
432
|
+
if (head >= range.from && head <= range.to) {
|
|
433
|
+
selection.current = comment.id;
|
|
434
|
+
selection.closest = undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!selection.current) {
|
|
438
|
+
const d = Math.min(Math.abs(head - range.from), Math.abs(head - range.to));
|
|
373
439
|
if (d < min) {
|
|
374
|
-
|
|
440
|
+
selection.closest = comment.id;
|
|
375
441
|
min = d;
|
|
376
442
|
}
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
if (selected.active !== active || selected.closest !== closest) {
|
|
380
|
-
view.dispatch({ effects: setSelection.of(selected) });
|
|
381
|
-
|
|
382
|
-
// Update callback.
|
|
383
|
-
handleSelect({
|
|
384
|
-
...selected,
|
|
385
|
-
ranges: ranges.map((range) => ({ ...range, location: view.coordsAtPos(range.from) })),
|
|
386
|
-
});
|
|
387
443
|
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
if (selection.current !== current || selection.closest !== closest) {
|
|
447
|
+
view.dispatch({ effects: setSelection.of(selection) });
|
|
448
|
+
|
|
449
|
+
// Update callback.
|
|
450
|
+
handleSelect({
|
|
451
|
+
selection,
|
|
452
|
+
comments: comments.map(({ comment, range }) => ({
|
|
453
|
+
comment,
|
|
454
|
+
range,
|
|
455
|
+
location: view.coordsAtPos(range.from),
|
|
456
|
+
})),
|
|
457
|
+
});
|
|
388
458
|
}
|
|
389
459
|
}),
|
|
460
|
+
|
|
461
|
+
options.onUpdate ? trackPastedComments(options.onUpdate) : [],
|
|
390
462
|
];
|
|
391
463
|
};
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Update comments state field.
|
|
467
|
+
*/
|
|
468
|
+
export const useComments = (view: EditorView | null, comments: Comment[] = []) => {
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
if (view) {
|
|
471
|
+
view.dispatch({
|
|
472
|
+
effects: setComments.of(comments),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}, [view, comments]);
|
|
476
|
+
};
|