@dxos/react-ui-editor 0.3.11-main.3f845e1 → 0.3.11-main.47875f3
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 +1796 -1376
- 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 +9 -8
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +14 -19
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -3
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/{hooks/automerge → components/TextEditor}/automerge.stories.d.ts +6 -1
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/index.d.ts +0 -2
- package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
- package/dist/types/src/{hooks/yjs → components/TextEditor}/yjs.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/cursor.d.ts +5 -0
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +26 -0
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/index.d.ts +3 -0
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
- package/dist/types/src/{hooks → extensions}/automerge/semaphore.d.ts +4 -4
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/update-automerge.d.ts +5 -0
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness.d.ts +46 -0
- package/dist/types/src/extensions/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/basic.d.ts.map +1 -0
- package/dist/types/src/extensions/blast.d.ts.map +1 -0
- package/dist/types/src/extensions/code.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions → extensions}/comments.d.ts +10 -4
- package/dist/types/src/extensions/comments.d.ts.map +1 -0
- package/dist/types/src/extensions/cursor.d.ts +25 -0
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/image.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions → extensions}/index.d.ts +4 -2
- package/dist/types/src/extensions/index.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions → extensions}/link.d.ts +1 -0
- package/dist/types/src/extensions/link.d.ts.map +1 -0
- package/dist/types/src/extensions/listener.d.ts +10 -0
- package/dist/types/src/extensions/listener.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -0
- package/dist/types/src/extensions/table.d.ts.map +1 -0
- package/dist/types/src/extensions/tasklist.d.ts +7 -0
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -0
- package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
- package/dist/types/src/extensions/yjs/cursor.d.ts +4 -0
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/yjs/index.d.ts +2 -0
- package/dist/types/src/extensions/yjs/index.d.ts.map +1 -0
- package/dist/types/src/extensions/yjs/yjs.d.ts +4 -0
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +1 -0
- package/dist/types/src/extensions/yjs/yjs.test.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge.d.ts +3 -0
- package/dist/types/src/hooks/automerge.d.ts.map +1 -0
- package/dist/types/src/hooks/awareness-provider.d.ts +28 -0
- package/dist/types/src/hooks/awareness-provider.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +4 -8
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/{yjs/space-provider.d.ts → yjs.d.ts} +8 -8
- package/dist/types/src/hooks/yjs.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +5 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/cursors.d.ts.map +1 -0
- package/dist/types/src/styles/index.d.ts +1 -0
- package/dist/types/src/styles/index.d.ts.map +1 -1
- 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/testing/replicator.d.ts +2 -2
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor/themes → themes}/default.d.ts +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -0
- package/dist/types/src/themes/index.d.ts.map +1 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +25 -23
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +36 -36
- package/src/components/TextEditor/TextEditor.stories.tsx +13 -34
- package/src/components/TextEditor/TextEditor.tsx +74 -70
- package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +66 -6
- package/src/components/TextEditor/codemirror.stories.ts +3 -2
- package/src/components/TextEditor/comments.stories.tsx +329 -0
- package/src/components/TextEditor/index.ts +0 -3
- package/src/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
- package/src/{components/TextEditor/extensions → extensions}/autocomplete.ts +18 -17
- package/src/extensions/automerge/automerge.ts +94 -0
- package/src/extensions/automerge/cursor.ts +52 -0
- package/src/extensions/automerge/defs.ts +62 -0
- package/src/extensions/automerge/index.ts +6 -0
- package/src/{hooks → extensions}/automerge/semaphore.ts +14 -12
- package/src/{hooks → extensions}/automerge/update-automerge.ts +5 -6
- package/src/{hooks → extensions}/automerge/update-codemirror.ts +13 -5
- package/src/extensions/awareness.ts +309 -0
- package/src/{components/TextEditor/extensions → extensions}/code.ts +14 -7
- package/src/{components/TextEditor/extensions → extensions}/comments.ts +116 -114
- package/src/extensions/cursor.ts +46 -0
- package/src/{components/TextEditor/extensions → extensions}/hr.ts +4 -5
- package/src/extensions/image.ts +87 -0
- package/src/{components/TextEditor/extensions → extensions}/index.ts +4 -2
- package/src/{components/TextEditor/extensions → extensions}/link.ts +116 -62
- package/src/extensions/listener.ts +35 -0
- package/src/{components/TextEditor/extensions → extensions}/markdown/highlight.ts +4 -13
- package/src/{components/TextEditor/extensions → extensions}/table.ts +10 -4
- package/src/extensions/tasklist.ts +95 -0
- package/src/{components/TextEditor/extensions → extensions}/typewriter.ts +1 -1
- package/src/extensions/yjs/cursor.ts +22 -0
- package/src/extensions/yjs/index.ts +5 -0
- package/src/{hooks → extensions}/yjs/yjs.test.ts +2 -26
- package/src/extensions/yjs/yjs.ts +16 -0
- package/src/hooks/automerge.ts +52 -0
- package/src/hooks/awareness-provider.ts +120 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useTextModel.ts +20 -96
- package/src/hooks/{yjs/space-provider.ts → yjs.ts} +49 -19
- package/src/index.ts +5 -0
- package/src/styles/index.ts +1 -0
- package/src/styles/markdown.ts +0 -2
- package/src/styles/tokens.ts +3 -0
- package/src/testing/replicator.ts +10 -15
- package/src/{components/TextEditor/themes → themes}/default.ts +52 -79
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hr.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts +0 -9
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts +0 -3
- package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +0 -3
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/typewriter.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/util.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/handle.d.ts +0 -9
- package/dist/types/src/hooks/automerge/handle.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/index.d.ts +0 -3
- package/dist/types/src/hooks/automerge/index.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/plugin.d.ts +0 -26
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/semaphore.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/update-automerge.d.ts +0 -6
- package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/cursors.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/index.d.ts +0 -3
- package/dist/types/src/hooks/yjs/index.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/space-provider.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +0 -1
- package/src/components/TextEditor/extensions/image.ts +0 -69
- package/src/components/TextEditor/extensions/listener.ts +0 -23
- package/src/components/TextEditor/extensions/mermaid.ts +0 -11
- package/src/components/TextEditor/extensions/tasklist.ts +0 -121
- package/src/components/TextEditor/extensions/tooltip.ts +0 -61
- package/src/hooks/automerge/handle.ts +0 -14
- package/src/hooks/automerge/index.ts +0 -6
- package/src/hooks/automerge/plugin.ts +0 -134
- package/src/hooks/yjs/index.ts +0 -6
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/autocomplete.d.ts +0 -0
- /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/basic.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/blast.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/code.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/hr.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/image.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/bundle.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/highlight.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/index.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/mention.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/table.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/typewriter.d.ts +0 -0
- /package/dist/types/src/{hooks → extensions}/yjs/yjs.test.d.ts +0 -0
- /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/themes → themes}/index.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions/util.d.ts → util.d.ts} +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/basic.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/blast.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/markdown/bundle.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/markdown/index.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/mention.ts +0 -0
- /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
- /package/src/{components/TextEditor/themes → themes}/index.ts +0 -0
- /package/src/{components/TextEditor/extensions/util.ts → util.ts} +0 -0
|
@@ -2,20 +2,8 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
-
import {
|
|
7
|
-
hoverTooltip,
|
|
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 { type Extension, StateEffect, StateField, type Text } from '@codemirror/state';
|
|
6
|
+
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
19
7
|
import sortBy from 'lodash.sortby';
|
|
20
8
|
|
|
21
9
|
import { debounce } from '@dxos/async';
|
|
@@ -23,28 +11,21 @@ import { invariant } from '@dxos/invariant';
|
|
|
23
11
|
import { log } from '@dxos/log';
|
|
24
12
|
import { nonNullable } from '@dxos/util';
|
|
25
13
|
|
|
26
|
-
import {
|
|
27
|
-
import { type CommentRange, type EditorModel,
|
|
14
|
+
import { Cursor } from './cursor';
|
|
15
|
+
import { type CommentRange, type EditorModel, type Range } from '../hooks';
|
|
16
|
+
import { getToken } from '../styles';
|
|
17
|
+
import { callbackWrapper } from '../util';
|
|
28
18
|
|
|
29
19
|
// TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
|
|
30
20
|
// TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
|
|
31
21
|
|
|
32
22
|
// TODO(burdon): Reconcile with theme.
|
|
33
23
|
const styles = EditorView.baseTheme({
|
|
34
|
-
'& .cm-bookmark': {
|
|
35
|
-
cursor: 'pointer',
|
|
36
|
-
margin: '4px',
|
|
37
|
-
padding: '4px',
|
|
38
|
-
backgroundColor: 'yellow',
|
|
39
|
-
},
|
|
40
|
-
'& .cm-bookmark-selected': {
|
|
41
|
-
backgroundColor: 'orange',
|
|
42
|
-
},
|
|
43
24
|
'& .cm-comment': {
|
|
44
|
-
backgroundColor: 'yellow',
|
|
25
|
+
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
45
26
|
},
|
|
46
27
|
'& .cm-comment-active': {
|
|
47
|
-
backgroundColor: '
|
|
28
|
+
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
48
29
|
},
|
|
49
30
|
});
|
|
50
31
|
|
|
@@ -70,9 +51,22 @@ export type CommentsState = CommentSelected & {
|
|
|
70
51
|
ranges: ExtendedCommentRange[];
|
|
71
52
|
};
|
|
72
53
|
|
|
54
|
+
export const setFocus = (view: EditorView, thread: string) => {
|
|
55
|
+
const range = view.state.field(commentsStateField).ranges.find((range) => range.id === thread);
|
|
56
|
+
if (!range) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
view.dispatch({
|
|
61
|
+
effects: setSelection.of({ active: thread }),
|
|
62
|
+
selection: { anchor: range.from },
|
|
63
|
+
scrollIntoView: true,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
73
67
|
export const setCommentRange = StateEffect.define<{ model: EditorModel; comments: CommentRange[] }>();
|
|
74
68
|
|
|
75
|
-
const setSelection = StateEffect.define<CommentSelected>();
|
|
69
|
+
export const setSelection = StateEffect.define<CommentSelected>();
|
|
76
70
|
|
|
77
71
|
const setCommentState = StateEffect.define<CommentsState>();
|
|
78
72
|
|
|
@@ -84,6 +78,8 @@ const setCommentState = StateEffect.define<CommentsState>();
|
|
|
84
78
|
const commentsStateField = StateField.define<CommentsState>({
|
|
85
79
|
create: () => ({ ranges: [] }),
|
|
86
80
|
update: (value, tr) => {
|
|
81
|
+
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
82
|
+
|
|
87
83
|
for (const effect of tr.effects) {
|
|
88
84
|
// Update selection.
|
|
89
85
|
if (effect.is(setSelection)) {
|
|
@@ -92,11 +88,13 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
92
88
|
|
|
93
89
|
// Update range from store.
|
|
94
90
|
if (effect.is(setCommentRange)) {
|
|
95
|
-
const {
|
|
96
|
-
const ranges: ExtendedCommentRange[] = comments
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
91
|
+
const { comments } = effect.value;
|
|
92
|
+
const ranges: ExtendedCommentRange[] = comments
|
|
93
|
+
.map((comment) => {
|
|
94
|
+
const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
|
|
95
|
+
return range && { ...comment, ...range };
|
|
96
|
+
})
|
|
97
|
+
.filter(nonNullable);
|
|
100
98
|
|
|
101
99
|
return { ...value, ranges };
|
|
102
100
|
}
|
|
@@ -136,42 +134,17 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
|
|
|
136
134
|
return Decoration.set(decorations);
|
|
137
135
|
});
|
|
138
136
|
|
|
139
|
-
/**
|
|
140
|
-
* Optional bookmark widget at the start/end of the selection.
|
|
141
|
-
* May correspond to a markdown bookmark.
|
|
142
|
-
*/
|
|
143
|
-
class BookmarkWidget extends WidgetType {
|
|
144
|
-
constructor(
|
|
145
|
-
private readonly _anchor: number,
|
|
146
|
-
private readonly _id: string,
|
|
147
|
-
private readonly _handleClick?: () => void,
|
|
148
|
-
) {
|
|
149
|
-
super();
|
|
150
|
-
invariant(this._id);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
override eq(other: BookmarkWidget) {
|
|
154
|
-
return this._anchor === other._anchor && this._id === other._id;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
override toDOM() {
|
|
158
|
-
const span = document.createElement('span');
|
|
159
|
-
span.className = 'cm-bookmark';
|
|
160
|
-
span.textContent = '※';
|
|
161
|
-
if (this._handleClick) {
|
|
162
|
-
span.onclick = () => this._handleClick!();
|
|
163
|
-
}
|
|
164
|
-
return span;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
137
|
export type CommentsOptions = {
|
|
169
138
|
key?: string;
|
|
170
139
|
footnote?: boolean;
|
|
171
140
|
/**
|
|
172
141
|
* Called to create a new thread and return the thread id.
|
|
173
142
|
*/
|
|
174
|
-
onCreate?: (
|
|
143
|
+
onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* Called when a comment is moved.
|
|
146
|
+
*/
|
|
147
|
+
onMove?: (threadID: string, cursor: string) => void;
|
|
175
148
|
/**
|
|
176
149
|
* Called to notify which thread is currently closest to the cursor.
|
|
177
150
|
*/
|
|
@@ -179,7 +152,66 @@ export type CommentsOptions = {
|
|
|
179
152
|
/**
|
|
180
153
|
* Called to render tooltip.
|
|
181
154
|
*/
|
|
182
|
-
onHover?: (el: Element) => void;
|
|
155
|
+
onHover?: (el: Element, shortcut: string) => void;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const trackPastedComments = (onMove: (threadID: string, cursor: string) => void) => {
|
|
159
|
+
let tracked: { text: Text; comments: { from: number; to: number; id: string }[] } | null = null;
|
|
160
|
+
|
|
161
|
+
const registerCopy = (event: Event, view: EditorView) => {
|
|
162
|
+
const comments = view.state.field(commentsStateField);
|
|
163
|
+
const { main } = view.state.selection;
|
|
164
|
+
const inSel = comments.ranges.filter(
|
|
165
|
+
(range) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
166
|
+
);
|
|
167
|
+
if (!inSel.length) {
|
|
168
|
+
tracked = null;
|
|
169
|
+
} else {
|
|
170
|
+
tracked = {
|
|
171
|
+
text: view.state.doc.slice(main.from, main.to),
|
|
172
|
+
comments: inSel.map((range) => ({ from: range.from - main.from, to: range.to - main.from, id: range.id })),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return [
|
|
178
|
+
EditorView.domEventHandlers({
|
|
179
|
+
cut: registerCopy,
|
|
180
|
+
copy: registerCopy,
|
|
181
|
+
}),
|
|
182
|
+
|
|
183
|
+
EditorView.updateListener.of((update) => {
|
|
184
|
+
if (tracked) {
|
|
185
|
+
const paste = update.transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
186
|
+
if (paste) {
|
|
187
|
+
let found = -1;
|
|
188
|
+
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
189
|
+
if (text.eq(tracked!.text)) {
|
|
190
|
+
for (let i = update.transactions.indexOf(paste!) + 1; i < update.transactions.length; i++) {
|
|
191
|
+
fromB = update.transactions[i].changes.mapPos(fromB);
|
|
192
|
+
}
|
|
193
|
+
found = fromB;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
if (found > -1) {
|
|
197
|
+
const active = update.view.state.field(commentsStateField).ranges;
|
|
198
|
+
for (const moved of tracked.comments) {
|
|
199
|
+
if (active.some((range) => range.id === moved.id && range.from === range.to)) {
|
|
200
|
+
onMove(
|
|
201
|
+
moved.id,
|
|
202
|
+
Cursor.getCursorFromRange(update.view.state.facet(Cursor.converter), {
|
|
203
|
+
from: found + moved.from,
|
|
204
|
+
to: found + moved.to,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
tracked = null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}),
|
|
214
|
+
];
|
|
183
215
|
};
|
|
184
216
|
|
|
185
217
|
/**
|
|
@@ -195,23 +227,26 @@ export type CommentsOptions = {
|
|
|
195
227
|
* 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
196
228
|
*/
|
|
197
229
|
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
230
|
+
const { key: shortcut = "meta-'" } = options;
|
|
231
|
+
|
|
198
232
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
199
233
|
|
|
200
234
|
/**
|
|
201
235
|
* Create comment thread action.
|
|
202
236
|
*/
|
|
203
237
|
const createCommentThread: Command = (view) => {
|
|
238
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
239
|
+
|
|
204
240
|
invariant(options.onCreate);
|
|
205
241
|
const { head, from, to } = view.state.selection.main;
|
|
206
242
|
if (from === to) {
|
|
207
243
|
return false;
|
|
208
244
|
}
|
|
209
245
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
if (relPos) {
|
|
246
|
+
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
247
|
+
if (cursor) {
|
|
213
248
|
// Create thread via callback.
|
|
214
|
-
const id = callbackWrapper(options.onCreate)(
|
|
249
|
+
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
215
250
|
if (id) {
|
|
216
251
|
// Update range.
|
|
217
252
|
view.dispatch({
|
|
@@ -235,44 +270,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
235
270
|
return false;
|
|
236
271
|
};
|
|
237
272
|
|
|
238
|
-
/**
|
|
239
|
-
* Bookmark widget decoration.
|
|
240
|
-
*/
|
|
241
|
-
const bookmarksViewPlugin = ViewPlugin.fromClass(
|
|
242
|
-
class BookmarkViewPlugin {
|
|
243
|
-
// Match markdown footnotes (option).
|
|
244
|
-
// https://www.markdownguide.org/extended-syntax/#footnotes
|
|
245
|
-
static bookmarkMatcher = new MatchDecorator({
|
|
246
|
-
regexp: /\[\^(\w+)\]/g,
|
|
247
|
-
decoration: (match, view, pos) => {
|
|
248
|
-
const id = match[1];
|
|
249
|
-
return Decoration.replace({
|
|
250
|
-
id,
|
|
251
|
-
widget: new BookmarkWidget(pos, id),
|
|
252
|
-
});
|
|
253
|
-
},
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
bookmarks: DecorationSet;
|
|
257
|
-
constructor(view: EditorView) {
|
|
258
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.createDeco(view);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
update(update: ViewUpdate) {
|
|
262
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.updateDeco(update, this.bookmarks);
|
|
263
|
-
}
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
decorations: (instance) => instance.bookmarks,
|
|
267
|
-
provide: (plugin) =>
|
|
268
|
-
EditorView.atomicRanges.of((view) => {
|
|
269
|
-
return view.plugin(plugin)?.bookmarks || Decoration.none;
|
|
270
|
-
}),
|
|
271
|
-
},
|
|
272
|
-
);
|
|
273
|
-
|
|
274
273
|
return [
|
|
275
|
-
bookmarksViewPlugin,
|
|
276
274
|
commentsStateField,
|
|
277
275
|
highlightDecorations,
|
|
278
276
|
styles,
|
|
@@ -283,8 +281,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
283
281
|
options.onCreate
|
|
284
282
|
? keymap.of([
|
|
285
283
|
{
|
|
286
|
-
key:
|
|
287
|
-
run: createCommentThread,
|
|
284
|
+
key: shortcut,
|
|
285
|
+
run: callbackWrapper(createCommentThread),
|
|
288
286
|
},
|
|
289
287
|
])
|
|
290
288
|
: [],
|
|
@@ -305,7 +303,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
305
303
|
create: () => {
|
|
306
304
|
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
307
305
|
const el = document.createElement('div');
|
|
308
|
-
options.onHover?.(el);
|
|
306
|
+
options.onHover?.(el, shortcut);
|
|
309
307
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
310
308
|
},
|
|
311
309
|
};
|
|
@@ -321,27 +319,27 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
321
319
|
// Monitor cursor movement and text updates.
|
|
322
320
|
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
323
321
|
//
|
|
324
|
-
EditorView.updateListener.of(({ view, state,
|
|
322
|
+
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
323
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
324
|
+
|
|
325
325
|
//
|
|
326
326
|
// Test if need to recompute indexed range if document changes before the end of the range.
|
|
327
327
|
//
|
|
328
328
|
{
|
|
329
329
|
let mod = false;
|
|
330
|
-
const model = state.field(modelState);
|
|
331
330
|
const { active, ranges } = state.field(commentsStateField);
|
|
332
331
|
changes.iterChanges((from, to, from2, to2) => {
|
|
333
|
-
invariant(model);
|
|
334
332
|
ranges.forEach((range) => {
|
|
335
333
|
if (from2 === to2) {
|
|
336
|
-
const newRange =
|
|
337
|
-
if (newRange.to - newRange.from === 0) {
|
|
334
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
335
|
+
if (!newRange || newRange.to - newRange.from === 0) {
|
|
338
336
|
// TODO(burdon): Delete range if empty.
|
|
339
337
|
log.info('deleted comment', { thread: range.id });
|
|
340
338
|
}
|
|
341
339
|
}
|
|
342
340
|
|
|
343
341
|
if (from <= range.to) {
|
|
344
|
-
const newRange =
|
|
342
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
345
343
|
Object.assign(range, newRange);
|
|
346
344
|
mod = true;
|
|
347
345
|
}
|
|
@@ -373,6 +371,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
373
371
|
}
|
|
374
372
|
});
|
|
375
373
|
|
|
374
|
+
// TODO(burdon): Fire debounced callback if range selected (to show hint).
|
|
375
|
+
|
|
376
376
|
if (selected.active !== active || selected.closest !== closest) {
|
|
377
377
|
view.dispatch({ effects: setSelection.of(selected) });
|
|
378
378
|
|
|
@@ -384,5 +384,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
}),
|
|
387
|
+
|
|
388
|
+
options.onMove ? trackPastedComments(options.onMove) : [],
|
|
387
389
|
];
|
|
388
390
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Facet } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
import type { Range } from '../hooks';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts indexes into the text document into stable peer-independent cursors.
|
|
11
|
+
*
|
|
12
|
+
* See:
|
|
13
|
+
* - https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
|
|
14
|
+
* - https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
15
|
+
*
|
|
16
|
+
* @param {assoc} number Negative values will associate the cursor with the previous character
|
|
17
|
+
* while positive - with the next one.
|
|
18
|
+
*/
|
|
19
|
+
export interface CursorConverter {
|
|
20
|
+
toCursor(position: number, assoc?: -1 | 1 | undefined): string;
|
|
21
|
+
fromCursor(cursor: string): number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const defaultCursorConverter: CursorConverter = {
|
|
25
|
+
toCursor: (position) => position.toString(),
|
|
26
|
+
fromCursor: (cursor) => parseInt(cursor),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class Cursor {
|
|
30
|
+
static readonly converter = Facet.define<CursorConverter, CursorConverter>({
|
|
31
|
+
combine: (providers) => providers[0] ?? defaultCursorConverter,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
static readonly getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
|
|
35
|
+
const from = cursorConverter.toCursor(range.from);
|
|
36
|
+
const to = cursorConverter.toCursor(range.to, -1);
|
|
37
|
+
return [from, to].join(':');
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
static readonly getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
|
|
41
|
+
const parts = cursor.split(':');
|
|
42
|
+
const from = cursorConverter.fromCursor(parts[0]);
|
|
43
|
+
const to = cursorConverter.fromCursor(parts[1]);
|
|
44
|
+
return from !== undefined && to !== undefined ? { from, to } : undefined;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -11,16 +11,15 @@ import {
|
|
|
11
11
|
type ViewUpdate,
|
|
12
12
|
WidgetType,
|
|
13
13
|
} from '@codemirror/view';
|
|
14
|
-
import get from 'lodash.get';
|
|
15
14
|
|
|
16
|
-
import {
|
|
15
|
+
import { getToken } from '../styles';
|
|
17
16
|
|
|
18
17
|
// TODO(burdon): Reconcile with theme.
|
|
19
18
|
const styles = EditorView.baseTheme({
|
|
20
19
|
'& .cm-hr': {
|
|
21
20
|
// TODO(burdon): ???
|
|
22
21
|
// Note that block-level decorations should not have vertical margins,
|
|
23
|
-
borderBottom: `1px solid ${
|
|
22
|
+
borderBottom: `1px solid ${getToken('extend.colors.neutral.200')}`,
|
|
24
23
|
},
|
|
25
24
|
});
|
|
26
25
|
|
|
@@ -29,11 +28,11 @@ class HorizontalRuleWidget extends WidgetType {
|
|
|
29
28
|
super();
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
override eq(other:
|
|
31
|
+
override eq(other: this) {
|
|
33
32
|
return this._pos === (other as any as HorizontalRuleWidget)._pos;
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
toDOM(view: EditorView) {
|
|
35
|
+
override toDOM(view: EditorView) {
|
|
37
36
|
// TODO(burdon): Create <hr> element? Does this clash with markdown parser?
|
|
38
37
|
const el = document.createElement('div');
|
|
39
38
|
el.className = 'cm-hr';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type EditorState, type Extension, StateField, type Transaction, type Range } from '@codemirror/state';
|
|
7
|
+
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
export type ImageOptions = {};
|
|
10
|
+
|
|
11
|
+
export const image = (options: ImageOptions = {}): Extension => {
|
|
12
|
+
return StateField.define<DecorationSet>({
|
|
13
|
+
create: (state) => {
|
|
14
|
+
return Decoration.set(buildDecorations(0, state.doc.length, state));
|
|
15
|
+
},
|
|
16
|
+
update: (value: DecorationSet, tr: Transaction) => {
|
|
17
|
+
if (!tr.docChanged && !tr.selection) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
// Find range of changes and cursor changes.
|
|
21
|
+
const cursor = tr.state.selection.main.head;
|
|
22
|
+
const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
|
|
23
|
+
let from = Math.min(cursor, oldCursor);
|
|
24
|
+
let to = Math.max(cursor, oldCursor);
|
|
25
|
+
tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
|
|
26
|
+
from = Math.min(from, fromB);
|
|
27
|
+
to = Math.max(to, toB);
|
|
28
|
+
});
|
|
29
|
+
// Expand to cover lines
|
|
30
|
+
from = tr.state.doc.lineAt(from).from;
|
|
31
|
+
to = tr.state.doc.lineAt(to).to;
|
|
32
|
+
return value.map(tr.changes).update({
|
|
33
|
+
filterFrom: from,
|
|
34
|
+
filterTo: to,
|
|
35
|
+
filter: () => false,
|
|
36
|
+
add: buildDecorations(from, to, tr.state),
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const buildDecorations = (from: number, to: number, state: EditorState) => {
|
|
44
|
+
const decorations: Range<Decoration>[] = [];
|
|
45
|
+
const cursor = state.selection.main.head;
|
|
46
|
+
|
|
47
|
+
syntaxTree(state).iterate({
|
|
48
|
+
enter: (node) => {
|
|
49
|
+
if (node.name === 'Image') {
|
|
50
|
+
const urlNode = node.node.getChild('URL');
|
|
51
|
+
if (urlNode) {
|
|
52
|
+
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
|
53
|
+
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
54
|
+
decorations.push(
|
|
55
|
+
Decoration.replace({
|
|
56
|
+
block: true, // Prevent cursor from entering.
|
|
57
|
+
widget: new ImageWidget(url),
|
|
58
|
+
}).range(hide ? node.from : node.to, node.to),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
from,
|
|
64
|
+
to,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return decorations;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
class ImageWidget extends WidgetType {
|
|
71
|
+
constructor(readonly _url: string) {
|
|
72
|
+
super();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override eq(other: this) {
|
|
76
|
+
return this._url === (other as any as ImageWidget)._url;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override toDOM(view: EditorView) {
|
|
80
|
+
const img = document.createElement('img');
|
|
81
|
+
img.setAttribute('src', this._url);
|
|
82
|
+
img.setAttribute('class', 'cm-image');
|
|
83
|
+
// Images are hidden until successfully loaded to avoid flickering effects.
|
|
84
|
+
img.onload = () => img.classList.add('cm-loaded-image');
|
|
85
|
+
return img;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
export * from './autocomplete';
|
|
6
|
+
export * from './automerge';
|
|
7
|
+
export * from './awareness';
|
|
6
8
|
export * from './basic';
|
|
7
9
|
export * from './blast';
|
|
8
10
|
export * from './code';
|
|
9
11
|
export * from './comments';
|
|
12
|
+
export * from './cursor';
|
|
10
13
|
export * from './hr';
|
|
11
14
|
export * from './image';
|
|
12
15
|
export * from './link';
|
|
13
16
|
export * from './listener';
|
|
14
17
|
export * from './markdown';
|
|
15
18
|
export * from './mention';
|
|
16
|
-
export * from './mermaid';
|
|
17
19
|
export * from './table';
|
|
18
20
|
export * from './tasklist';
|
|
19
|
-
export * from './tooltip';
|
|
20
21
|
export * from './typewriter';
|
|
22
|
+
export * from './yjs';
|