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