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