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