@dxos/react-ui-editor 0.3.11-main.0b0bfb1 → 0.3.11-main.0e02eb0
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 +1652 -1062
- 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 -15
- 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/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/{yjs.stories.d.ts → hooks.stories.d.ts} +13 -2
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +25 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +23 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +2 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.spec.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts +2 -0
- package/dist/types/src/extensions/automerge/automerge.test.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +9 -11
- 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 +8 -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 +2 -2
- 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 +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 +3 -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 +4 -2
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +4 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useEditorView.d.ts +17 -0
- package/dist/types/src/hooks/useEditorView.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextEditor.d.ts +39 -0
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +14 -27
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +2 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +2 -0
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +47 -37
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +73 -82
- package/src/components/TextEditor/TextEditor.tsx +111 -75
- package/src/components/TextEditor/automerge.stories.tsx +1 -2
- package/src/components/TextEditor/hooks.stories.tsx +106 -0
- package/src/components/Toolbar/Toolbar.stories.tsx +90 -0
- package/src/components/Toolbar/Toolbar.tsx +166 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +1 -0
- package/src/extensions/autocomplete.ts +6 -3
- package/src/extensions/automerge/automerge.spec.tsx +76 -0
- package/src/extensions/automerge/automerge.test.ts +13 -0
- package/src/extensions/automerge/automerge.ts +54 -45
- package/src/extensions/automerge/cursor.ts +3 -3
- package/src/extensions/automerge/defs.ts +13 -34
- package/src/extensions/automerge/index.ts +0 -1
- package/src/extensions/automerge/semaphore.ts +33 -32
- package/src/extensions/automerge/update-automerge.ts +14 -8
- package/src/extensions/automerge/update-codemirror.ts +5 -5
- package/src/extensions/awareness.ts +22 -18
- package/src/extensions/basic.ts +17 -10
- package/src/extensions/blast.ts +4 -1
- package/src/extensions/comments.ts +316 -165
- package/src/extensions/cursor.ts +7 -3
- package/src/extensions/index.ts +1 -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 +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} +16 -7
- package/src/extensions/{tasklist.ts → markdown/tasklist.ts} +48 -33
- 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 +5 -2
- package/src/hooks/useActionHandler.ts +62 -0
- package/src/hooks/useEditorView.ts +29 -0
- package/src/hooks/useTextEditor.ts +153 -0
- package/src/hooks/useTextModel.ts +66 -56
- package/src/index.ts +1 -1
- package/src/styles/markdown.ts +9 -10
- package/src/themes/default.ts +9 -6
- 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/comments.stories.d.ts +0 -26
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/code.d.ts.map +0 -1
- package/dist/types/src/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/extensions/image.d.ts.map +0 -1
- package/dist/types/src/extensions/link.d.ts.map +0 -1
- package/dist/types/src/extensions/table.d.ts.map +0 -1
- package/dist/types/src/extensions/tasklist.d.ts +0 -7
- 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/comments.stories.tsx +0 -315
- 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/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/src/extensions/{image.ts → markdown/image.ts} +0 -0
- /package/src/extensions/{table.ts → markdown/table.ts} +0 -0
|
@@ -2,101 +2,102 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { invertedEffects } from '@codemirror/commands';
|
|
6
|
+
import { type Extension, Facet, StateEffect, StateField, type Text, type ChangeDesc } from '@codemirror/state';
|
|
6
7
|
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
7
8
|
import sortBy from 'lodash.sortby';
|
|
9
|
+
import { useEffect } from 'react';
|
|
8
10
|
|
|
9
11
|
import { debounce } from '@dxos/async';
|
|
10
|
-
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
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
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;
|
|
35
36
|
};
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
active?: string;
|
|
38
|
+
type SelectionState = {
|
|
39
|
+
current?: string;
|
|
40
40
|
closest?: string;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// TODO(burdon): Not part of state; just required for callback.
|
|
47
|
-
location?: Rect | null;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export type CommentsState = CommentSelected & {
|
|
51
|
-
ranges: ExtendedCommentRange[];
|
|
43
|
+
export type CommentsState = {
|
|
44
|
+
comments: CommentState[];
|
|
45
|
+
selection: SelectionState;
|
|
52
46
|
};
|
|
53
47
|
|
|
54
|
-
export const
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
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) {
|
|
57
51
|
return;
|
|
58
52
|
}
|
|
59
53
|
|
|
60
|
-
view.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
export const
|
|
67
|
+
export const setComments = StateEffect.define<Comment[]>();
|
|
68
68
|
|
|
69
|
-
export const setSelection = StateEffect.define<
|
|
69
|
+
export const setSelection = StateEffect.define<SelectionState>();
|
|
70
70
|
|
|
71
71
|
const setCommentState = StateEffect.define<CommentsState>();
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* State field (reducer) that tracks comment ranges.
|
|
75
|
-
* The ranges are tracked as
|
|
76
|
-
* 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.
|
|
77
76
|
*/
|
|
78
|
-
const
|
|
79
|
-
create: () => ({
|
|
77
|
+
const commentsState = StateField.define<CommentsState>({
|
|
78
|
+
create: () => ({ comments: [], selection: {} }),
|
|
80
79
|
update: (value, tr) => {
|
|
81
|
-
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
82
|
-
|
|
83
80
|
for (const effect of tr.effects) {
|
|
84
81
|
// Update selection.
|
|
85
82
|
if (effect.is(setSelection)) {
|
|
86
|
-
return { ...value,
|
|
83
|
+
return { ...value, selection: effect.value };
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
// Update range from store.
|
|
90
|
-
if (effect.is(
|
|
91
|
-
const
|
|
92
|
-
const ranges: ExtendedCommentRange[] = comments
|
|
87
|
+
if (effect.is(setComments)) {
|
|
88
|
+
const comments: CommentState[] = effect.value
|
|
93
89
|
.map((comment) => {
|
|
94
|
-
|
|
95
|
-
|
|
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 };
|
|
96
97
|
})
|
|
97
98
|
.filter(nonNullable);
|
|
98
99
|
|
|
99
|
-
return { ...value,
|
|
100
|
+
return { ...value, comments };
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
// Update entire state.
|
|
@@ -112,35 +113,48 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
112
113
|
/**
|
|
113
114
|
* Decorate ranges.
|
|
114
115
|
*/
|
|
115
|
-
const
|
|
116
|
-
const {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
}
|
|
124
129
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.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);
|
|
133
137
|
|
|
134
138
|
return Decoration.set(decorations);
|
|
135
139
|
});
|
|
136
140
|
|
|
137
141
|
export type CommentsOptions = {
|
|
142
|
+
/**
|
|
143
|
+
* Key shortcut to create a new thread.
|
|
144
|
+
*/
|
|
138
145
|
key?: string;
|
|
139
|
-
footnote?: boolean;
|
|
140
146
|
/**
|
|
141
147
|
* Called to create a new thread and return the thread id.
|
|
142
148
|
*/
|
|
143
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;
|
|
144
158
|
/**
|
|
145
159
|
* Called to notify which thread is currently closest to the cursor.
|
|
146
160
|
*/
|
|
@@ -151,6 +165,162 @@ export type CommentsOptions = {
|
|
|
151
165
|
onHover?: (el: Element, shortcut: string) => void;
|
|
152
166
|
};
|
|
153
167
|
|
|
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
|
+
};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
return [
|
|
197
|
+
EditorView.domEventHandlers({
|
|
198
|
+
cut: handleTrack,
|
|
199
|
+
copy: handleTrack,
|
|
200
|
+
}),
|
|
201
|
+
|
|
202
|
+
// Track deleted comments.
|
|
203
|
+
invertedEffects.of((tr) => {
|
|
204
|
+
const { comments } = tr.startState.field(commentsState);
|
|
205
|
+
const effects: StateEffect<any>[] = [];
|
|
206
|
+
tr.changes.iterChangedRanges((fromA, toA) => {
|
|
207
|
+
for (const {
|
|
208
|
+
comment: { id },
|
|
209
|
+
range: { from, to },
|
|
210
|
+
} of comments) {
|
|
211
|
+
if (from < to && from >= fromA && to <= toA) {
|
|
212
|
+
effects.push(restoreCommentEffect.of({ id, from, to }));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return effects;
|
|
218
|
+
}),
|
|
219
|
+
|
|
220
|
+
// Handle paste or the undo of comment deletion.
|
|
221
|
+
EditorView.updateListener.of((update) => {
|
|
222
|
+
const restore: TrackedComment[] = [];
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < update.transactions.length; i++) {
|
|
225
|
+
const tr = update.transactions[i];
|
|
226
|
+
for (let j = 0; j < restore.length; j++) {
|
|
227
|
+
restore[j] = mapTrackedComment(restore[j], tr.changes);
|
|
228
|
+
}
|
|
229
|
+
for (const effect of tr.effects) {
|
|
230
|
+
if (effect.is(restoreCommentEffect)) {
|
|
231
|
+
restore.push(effect.value);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
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
|
+
}
|
|
245
|
+
|
|
246
|
+
found = fromB;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
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
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const comment of restore) {
|
|
261
|
+
const { comments } = update.startState.field(commentsState);
|
|
262
|
+
const exists = comments.some((c) => c.comment.id === comment.id && c.range.from < c.range.to);
|
|
263
|
+
if (!exists) {
|
|
264
|
+
const cursor = Cursor.getCursorFromRange(update.state, comment);
|
|
265
|
+
onUpdate(comment.id, cursor);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
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
|
+
|
|
154
324
|
/**
|
|
155
325
|
* Comment threads.
|
|
156
326
|
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
@@ -160,56 +330,17 @@ export type CommentsOptions = {
|
|
|
160
330
|
* 4). Tracks the current cursor position to:
|
|
161
331
|
* a). Update the decoration to show if the cursor is within a current selection.
|
|
162
332
|
* 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.
|
|
333
|
+
* 5). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
165
334
|
*/
|
|
166
335
|
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
167
336
|
const { key: shortcut = "meta-'" } = options;
|
|
168
337
|
|
|
169
338
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
170
339
|
|
|
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
340
|
return [
|
|
211
|
-
|
|
212
|
-
|
|
341
|
+
optionsFacet.of(options),
|
|
342
|
+
commentsState,
|
|
343
|
+
commentsDecorations,
|
|
213
344
|
styles,
|
|
214
345
|
|
|
215
346
|
//
|
|
@@ -219,7 +350,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
219
350
|
? keymap.of([
|
|
220
351
|
{
|
|
221
352
|
key: shortcut,
|
|
222
|
-
run: callbackWrapper(
|
|
353
|
+
run: callbackWrapper(createComment),
|
|
223
354
|
},
|
|
224
355
|
])
|
|
225
356
|
: [],
|
|
@@ -238,9 +369,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
238
369
|
end: selection.to,
|
|
239
370
|
above: true,
|
|
240
371
|
create: () => {
|
|
241
|
-
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
242
372
|
const el = document.createElement('div');
|
|
243
|
-
options.onHover
|
|
373
|
+
options.onHover!(el, shortcut);
|
|
244
374
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
245
375
|
},
|
|
246
376
|
};
|
|
@@ -248,78 +378,99 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
248
378
|
|
|
249
379
|
return null;
|
|
250
380
|
},
|
|
251
|
-
{
|
|
381
|
+
{
|
|
382
|
+
// TODO(burdon): Hide on change triggered immediately?
|
|
383
|
+
// hideOnChange: true,
|
|
384
|
+
hoverTime: 1_000,
|
|
385
|
+
},
|
|
252
386
|
)
|
|
253
387
|
: [],
|
|
254
388
|
|
|
255
389
|
//
|
|
256
|
-
//
|
|
257
|
-
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
390
|
+
// Track deleted ranges and update ranges for decorations.
|
|
258
391
|
//
|
|
259
392
|
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
|
-
}
|
|
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);
|
|
276
402
|
}
|
|
403
|
+
}
|
|
277
404
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
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
|
+
}
|
|
284
411
|
});
|
|
412
|
+
});
|
|
285
413
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
414
|
+
if (mod) {
|
|
415
|
+
view.dispatch({ effects: setCommentState.of({ comments, ...value }) });
|
|
289
416
|
}
|
|
417
|
+
}),
|
|
290
418
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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));
|
|
305
439
|
if (d < min) {
|
|
306
|
-
|
|
440
|
+
selection.closest = comment.id;
|
|
307
441
|
min = d;
|
|
308
442
|
}
|
|
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
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
|
+
});
|
|
322
458
|
}
|
|
323
459
|
}),
|
|
460
|
+
|
|
461
|
+
options.onUpdate ? trackPastedComments(options.onUpdate) : [],
|
|
324
462
|
];
|
|
325
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
|
+
};
|
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';
|