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