@dxos/react-ui-editor 0.3.11-main.6fccc32 → 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 +1259 -1060
- 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/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/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/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 +35 -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/index.d.ts +1 -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 +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 -27
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +2 -0
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +34 -37
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +72 -80
- package/src/components/TextEditor/TextEditor.tsx +96 -70
- package/src/components/TextEditor/comments.stories.tsx +113 -71
- package/src/components/Toolbar/Toolbar.stories.tsx +86 -0
- package/src/components/Toolbar/Toolbar.tsx +115 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.ts +39 -46
- 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 +14 -8
- package/src/extensions/awareness.ts +15 -14
- package/src/extensions/basic.ts +22 -17
- package/src/extensions/comments.ts +269 -162
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/index.ts +1 -7
- 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 +9 -15
- package/src/extensions/markdown/hr.ts +71 -0
- package/src/extensions/markdown/index.ts +8 -0
- package/src/extensions/{link.ts → markdown/link.ts} +8 -5
- 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 +47 -56
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +1 -11
- 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/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +0 -4
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/index.d.ts +0 -2
- package/dist/types/src/extensions/yjs/index.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.d.ts +0 -4
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +0 -1
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +0 -2
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge.d.ts +0 -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/code.ts +0 -106
- package/src/extensions/hr.ts +0 -73
- package/src/extensions/tasklist.ts +0 -119
- 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
- /package/dist/types/src/extensions/{code.d.ts → markdown/code.d.ts} +0 -0
- /package/dist/types/src/extensions/{hr.d.ts → markdown/hr.d.ts} +0 -0
- /package/dist/types/src/extensions/{image.d.ts → markdown/image.d.ts} +0 -0
- /package/dist/types/src/extensions/{link.d.ts → markdown/link.d.ts} +0 -0
- /package/dist/types/src/extensions/{table.d.ts → markdown/table.d.ts} +0 -0
- /package/dist/types/src/extensions/{tasklist.d.ts → markdown/tasklist.d.ts} +0 -0
- /package/src/extensions/{image.ts → markdown/image.ts} +0 -0
- /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
|
@@ -2,101 +2,103 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
5
|
+
import { type Extension, Facet, 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
|
-
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import { log } from '@dxos/log';
|
|
12
11
|
import { nonNullable } from '@dxos/util';
|
|
13
12
|
|
|
14
13
|
import { Cursor } from './cursor';
|
|
15
|
-
import { type
|
|
14
|
+
import { type Comment, type Range } from '../hooks';
|
|
16
15
|
import { getToken } from '../styles';
|
|
17
16
|
import { callbackWrapper } from '../util';
|
|
18
17
|
|
|
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
18
|
// TODO(burdon): Reconcile with theme.
|
|
23
19
|
const styles = EditorView.baseTheme({
|
|
24
20
|
'& .cm-comment': {
|
|
25
21
|
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
26
22
|
},
|
|
27
|
-
'& .cm-comment-
|
|
23
|
+
'& .cm-comment-current': {
|
|
28
24
|
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
29
25
|
},
|
|
30
26
|
});
|
|
31
27
|
|
|
32
28
|
const marks = {
|
|
33
29
|
highlight: Decoration.mark({ class: 'cm-comment' }),
|
|
34
|
-
highlightActive: Decoration.mark({ class: 'cm-comment-
|
|
30
|
+
highlightActive: Decoration.mark({ class: 'cm-comment-current' }),
|
|
35
31
|
};
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
type CommentState = {
|
|
34
|
+
comment: Comment;
|
|
35
|
+
range: Range;
|
|
36
|
+
location?: Rect | null;
|
|
41
37
|
};
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
location?: Rect | null;
|
|
48
|
-
};
|
|
39
|
+
type SelectionState = {
|
|
40
|
+
current?: string;
|
|
41
|
+
closest?: string;
|
|
42
|
+
};
|
|
49
43
|
|
|
50
|
-
export type CommentsState =
|
|
51
|
-
|
|
44
|
+
export type CommentsState = {
|
|
45
|
+
comments: CommentState[];
|
|
46
|
+
selection: SelectionState;
|
|
52
47
|
};
|
|
53
48
|
|
|
54
|
-
export const setFocus = (view: EditorView,
|
|
55
|
-
const
|
|
56
|
-
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) {
|
|
57
52
|
return;
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
view.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
|
65
66
|
};
|
|
66
67
|
|
|
67
|
-
export const
|
|
68
|
+
export const setComments = StateEffect.define<Comment[]>();
|
|
68
69
|
|
|
69
|
-
export const setSelection = StateEffect.define<
|
|
70
|
+
export const setSelection = StateEffect.define<SelectionState>();
|
|
70
71
|
|
|
71
72
|
const setCommentState = StateEffect.define<CommentsState>();
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
75
|
* State field (reducer) that tracks comment ranges.
|
|
75
|
-
* The ranges are tracked as
|
|
76
|
-
* 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.
|
|
77
77
|
*/
|
|
78
|
-
const
|
|
79
|
-
create: () => ({
|
|
78
|
+
const commentsState = StateField.define<CommentsState>({
|
|
79
|
+
create: () => ({ comments: [], selection: {} }),
|
|
80
80
|
update: (value, tr) => {
|
|
81
|
-
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
82
|
-
|
|
83
81
|
for (const effect of tr.effects) {
|
|
84
82
|
// Update selection.
|
|
85
83
|
if (effect.is(setSelection)) {
|
|
86
|
-
return { ...value,
|
|
84
|
+
return { ...value, selection: effect.value };
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
// Update range from store.
|
|
90
|
-
if (effect.is(
|
|
91
|
-
const
|
|
92
|
-
const ranges: ExtendedCommentRange[] = comments
|
|
88
|
+
if (effect.is(setComments)) {
|
|
89
|
+
const comments: CommentState[] = effect.value
|
|
93
90
|
.map((comment) => {
|
|
94
|
-
|
|
95
|
-
|
|
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 };
|
|
96
98
|
})
|
|
97
99
|
.filter(nonNullable);
|
|
98
100
|
|
|
99
|
-
return { ...value,
|
|
101
|
+
return { ...value, comments };
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
// Update entire state.
|
|
@@ -112,35 +114,48 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
112
114
|
/**
|
|
113
115
|
* Decorate ranges.
|
|
114
116
|
*/
|
|
115
|
-
const highlightDecorations = EditorView.decorations.compute([
|
|
116
|
-
const {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
}
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.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);
|
|
133
138
|
|
|
134
139
|
return Decoration.set(decorations);
|
|
135
140
|
});
|
|
136
141
|
|
|
137
142
|
export type CommentsOptions = {
|
|
143
|
+
/**
|
|
144
|
+
* Key shortcut to create a new thread.
|
|
145
|
+
*/
|
|
138
146
|
key?: string;
|
|
139
|
-
footnote?: boolean;
|
|
140
147
|
/**
|
|
141
148
|
* Called to create a new thread and return the thread id.
|
|
142
149
|
*/
|
|
143
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;
|
|
144
159
|
/**
|
|
145
160
|
* Called to notify which thread is currently closest to the cursor.
|
|
146
161
|
*/
|
|
@@ -151,6 +166,117 @@ export type CommentsOptions = {
|
|
|
151
166
|
onHover?: (el: Element, shortcut: string) => void;
|
|
152
167
|
};
|
|
153
168
|
|
|
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
|
+
};
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
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
|
+
}
|
|
214
|
+
|
|
215
|
+
found = fromB;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
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
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
];
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const optionsFacet = Facet.define<CommentsOptions, CommentsOptions>({
|
|
239
|
+
combine: (providers) => providers[0],
|
|
240
|
+
});
|
|
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) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
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 },
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return false;
|
|
278
|
+
};
|
|
279
|
+
|
|
154
280
|
/**
|
|
155
281
|
* Comment threads.
|
|
156
282
|
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
@@ -160,55 +286,16 @@ export type CommentsOptions = {
|
|
|
160
286
|
* 4). Tracks the current cursor position to:
|
|
161
287
|
* a). Update the decoration to show if the cursor is within a current selection.
|
|
162
288
|
* b). Calls a handler to indicate which is the closest selection (e.g., to update the thread sidebar).
|
|
163
|
-
* 5). Optionally,
|
|
164
|
-
* 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
289
|
+
* 5). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
165
290
|
*/
|
|
166
291
|
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
167
292
|
const { key: shortcut = "meta-'" } = options;
|
|
168
293
|
|
|
169
294
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
170
295
|
|
|
171
|
-
/**
|
|
172
|
-
* Create comment thread action.
|
|
173
|
-
*/
|
|
174
|
-
const createCommentThread: Command = (view) => {
|
|
175
|
-
const cursorConverter = view.state.facet(Cursor.converter);
|
|
176
|
-
|
|
177
|
-
invariant(options.onCreate);
|
|
178
|
-
const { head, from, to } = view.state.selection.main;
|
|
179
|
-
if (from === to) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
184
|
-
if (cursor) {
|
|
185
|
-
// Create thread via callback.
|
|
186
|
-
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
187
|
-
if (id) {
|
|
188
|
-
// Update range.
|
|
189
|
-
view.dispatch({
|
|
190
|
-
effects: setSelection.of({ active: id }),
|
|
191
|
-
selection: { anchor: from },
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// Insert footnote.
|
|
195
|
-
if (options.footnote) {
|
|
196
|
-
const tag = `[^${id}]`;
|
|
197
|
-
view.dispatch({
|
|
198
|
-
changes: { from: head, insert: tag },
|
|
199
|
-
selection: { anchor: head + tag.length },
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return false;
|
|
208
|
-
};
|
|
209
|
-
|
|
210
296
|
return [
|
|
211
|
-
|
|
297
|
+
optionsFacet.of(options),
|
|
298
|
+
commentsState,
|
|
212
299
|
highlightDecorations,
|
|
213
300
|
styles,
|
|
214
301
|
|
|
@@ -219,7 +306,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
219
306
|
? keymap.of([
|
|
220
307
|
{
|
|
221
308
|
key: shortcut,
|
|
222
|
-
run: callbackWrapper(
|
|
309
|
+
run: callbackWrapper(createComment),
|
|
223
310
|
},
|
|
224
311
|
])
|
|
225
312
|
: [],
|
|
@@ -238,9 +325,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
238
325
|
end: selection.to,
|
|
239
326
|
above: true,
|
|
240
327
|
create: () => {
|
|
241
|
-
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
242
328
|
const el = document.createElement('div');
|
|
243
|
-
options.onHover
|
|
329
|
+
options.onHover!(el, shortcut);
|
|
244
330
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
245
331
|
},
|
|
246
332
|
};
|
|
@@ -248,78 +334,99 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
248
334
|
|
|
249
335
|
return null;
|
|
250
336
|
},
|
|
251
|
-
{
|
|
337
|
+
{
|
|
338
|
+
// TODO(burdon): Hide on change triggered immediately?
|
|
339
|
+
// hideOnChange: true,
|
|
340
|
+
hoverTime: 1_000,
|
|
341
|
+
},
|
|
252
342
|
)
|
|
253
343
|
: [],
|
|
254
344
|
|
|
255
345
|
//
|
|
256
|
-
//
|
|
257
|
-
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
346
|
+
// Track deleted ranges and update ranges for decorations.
|
|
258
347
|
//
|
|
259
348
|
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
ranges.forEach((range) => {
|
|
270
|
-
if (from2 === to2) {
|
|
271
|
-
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
272
|
-
if (!newRange || newRange.to - newRange.from === 0) {
|
|
273
|
-
// TODO(burdon): Delete range if empty.
|
|
274
|
-
log.info('deleted comment', { thread: range.id });
|
|
275
|
-
}
|
|
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);
|
|
276
358
|
}
|
|
359
|
+
}
|
|
277
360
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
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
|
+
}
|
|
284
367
|
});
|
|
368
|
+
});
|
|
285
369
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
370
|
+
if (mod) {
|
|
371
|
+
view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
|
|
289
372
|
}
|
|
373
|
+
}),
|
|
290
374
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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));
|
|
305
395
|
if (d < min) {
|
|
306
|
-
|
|
396
|
+
selection.closest = comment.id;
|
|
307
397
|
min = d;
|
|
308
398
|
}
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// TODO(burdon): Fire debounced callback if range selected (to show hint).
|
|
312
|
-
|
|
313
|
-
if (selected.active !== active || selected.closest !== closest) {
|
|
314
|
-
view.dispatch({ effects: setSelection.of(selected) });
|
|
315
|
-
|
|
316
|
-
// Update callback.
|
|
317
|
-
handleSelect({
|
|
318
|
-
...selected,
|
|
319
|
-
ranges: ranges.map((range) => ({ ...range, location: view.coordsAtPos(range.from) })),
|
|
320
|
-
});
|
|
321
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
|
+
});
|
|
322
414
|
}
|
|
323
415
|
}),
|
|
416
|
+
|
|
417
|
+
options.onUpdate ? trackPastedComments(options.onUpdate) : [],
|
|
324
418
|
];
|
|
325
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]);
|
|
432
|
+
};
|
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]);
|
package/src/extensions/index.ts
CHANGED
|
@@ -7,16 +7,10 @@ export * from './automerge';
|
|
|
7
7
|
export * from './awareness';
|
|
8
8
|
export * from './basic';
|
|
9
9
|
export * from './blast';
|
|
10
|
-
export * from './code';
|
|
11
10
|
export * from './comments';
|
|
12
11
|
export * from './cursor';
|
|
13
|
-
export * from './hr';
|
|
14
|
-
export * from './image';
|
|
15
|
-
export * from './link';
|
|
16
12
|
export * from './listener';
|
|
17
13
|
export * from './markdown';
|
|
18
14
|
export * from './mention';
|
|
19
|
-
export * from './
|
|
20
|
-
export * from './tasklist';
|
|
15
|
+
export * from './outliner';
|
|
21
16
|
export * from './typewriter';
|
|
22
|
-
export * from './yjs';
|