@dxos/react-ui-editor 0.3.11-main.3f845e1 → 0.3.11-main.42059c4
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 +1804 -1399
- 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/extensions/automerge/semaphore.d.ts +14 -0
- 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 +18 -7
- 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 +3 -1
- 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 +355 -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 +66 -0
- package/src/extensions/automerge/index.ts +6 -0
- package/src/{hooks → extensions}/automerge/semaphore.ts +23 -23
- 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 +151 -137
- 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 +4 -1
- 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 +0 -19
- 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,49 +2,27 @@
|
|
|
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';
|
|
22
10
|
import { invariant } from '@dxos/invariant';
|
|
23
|
-
import { log } from '@dxos/log';
|
|
24
11
|
import { nonNullable } from '@dxos/util';
|
|
25
12
|
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
import { semaphoreFacet } from './automerge/defs';
|
|
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';
|
|
31
18
|
|
|
32
19
|
// TODO(burdon): Reconcile with theme.
|
|
33
20
|
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
21
|
'& .cm-comment': {
|
|
44
|
-
backgroundColor: 'yellow',
|
|
22
|
+
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
45
23
|
},
|
|
46
24
|
'& .cm-comment-active': {
|
|
47
|
-
backgroundColor: '
|
|
25
|
+
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
48
26
|
},
|
|
49
27
|
});
|
|
50
28
|
|
|
@@ -70,20 +48,34 @@ export type CommentsState = CommentSelected & {
|
|
|
70
48
|
ranges: ExtendedCommentRange[];
|
|
71
49
|
};
|
|
72
50
|
|
|
51
|
+
export const setFocus = (view: EditorView, thread: string) => {
|
|
52
|
+
const range = view.state.field(commentsStateField).ranges.find((range) => range.id === thread);
|
|
53
|
+
if (!range) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
view.dispatch({
|
|
58
|
+
effects: setSelection.of({ active: thread }),
|
|
59
|
+
selection: { anchor: range.from },
|
|
60
|
+
scrollIntoView: true, // TODO(burdon): Scroll to y-position (or center of screen?)
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
73
64
|
export const setCommentRange = StateEffect.define<{ model: EditorModel; comments: CommentRange[] }>();
|
|
74
65
|
|
|
75
|
-
const setSelection = StateEffect.define<CommentSelected>();
|
|
66
|
+
export const setSelection = StateEffect.define<CommentSelected>();
|
|
76
67
|
|
|
77
68
|
const setCommentState = StateEffect.define<CommentsState>();
|
|
78
69
|
|
|
79
70
|
/**
|
|
80
71
|
* State field (reducer) that tracks comment ranges.
|
|
81
|
-
* The ranges are tracked as
|
|
82
|
-
* therefore must be updated when the document changes.
|
|
72
|
+
* The ranges are tracked as Automerge cursors from which the absolute indexed ranges can be computed.
|
|
83
73
|
*/
|
|
84
74
|
const commentsStateField = StateField.define<CommentsState>({
|
|
85
75
|
create: () => ({ ranges: [] }),
|
|
86
76
|
update: (value, tr) => {
|
|
77
|
+
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
78
|
+
|
|
87
79
|
for (const effect of tr.effects) {
|
|
88
80
|
// Update selection.
|
|
89
81
|
if (effect.is(setSelection)) {
|
|
@@ -91,12 +83,15 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
91
83
|
}
|
|
92
84
|
|
|
93
85
|
// Update range from store.
|
|
94
|
-
if (effect.is(setCommentRange)) {
|
|
95
|
-
const {
|
|
96
|
-
const ranges: ExtendedCommentRange[] = comments
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
if (effect.is(setCommentRange) && effect.value.comments.length > 0) {
|
|
87
|
+
const { comments } = effect.value;
|
|
88
|
+
const ranges: ExtendedCommentRange[] = comments
|
|
89
|
+
.map((comment) => {
|
|
90
|
+
const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
|
|
91
|
+
// console.log('update', JSON.stringify({ range, cursor: comment.cursor }, undefined, 2));
|
|
92
|
+
return range && { ...comment, ...range };
|
|
93
|
+
})
|
|
94
|
+
.filter(nonNullable);
|
|
100
95
|
|
|
101
96
|
return { ...value, ranges };
|
|
102
97
|
}
|
|
@@ -136,42 +131,23 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
|
|
|
136
131
|
return Decoration.set(decorations);
|
|
137
132
|
});
|
|
138
133
|
|
|
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
134
|
export type CommentsOptions = {
|
|
135
|
+
/**
|
|
136
|
+
* Key shortcut to create a new thread.
|
|
137
|
+
*/
|
|
169
138
|
key?: string;
|
|
170
|
-
footnote?: boolean;
|
|
171
139
|
/**
|
|
172
140
|
* Called to create a new thread and return the thread id.
|
|
173
141
|
*/
|
|
174
|
-
onCreate?: (
|
|
142
|
+
onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
|
|
143
|
+
/**
|
|
144
|
+
* Selection cut/deleted.
|
|
145
|
+
*/
|
|
146
|
+
onDelete?: (thread: string) => void;
|
|
147
|
+
/**
|
|
148
|
+
* Called when a comment is moved.
|
|
149
|
+
*/
|
|
150
|
+
onUpdate?: (thread: string, cursor: string) => void;
|
|
175
151
|
/**
|
|
176
152
|
* Called to notify which thread is currently closest to the cursor.
|
|
177
153
|
*/
|
|
@@ -179,7 +155,86 @@ export type CommentsOptions = {
|
|
|
179
155
|
/**
|
|
180
156
|
* Called to render tooltip.
|
|
181
157
|
*/
|
|
182
|
-
onHover?: (el: Element) => void;
|
|
158
|
+
onHover?: (el: Element, shortcut: string) => void;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// TODO(burdon): Handle cut/restore via undo (need to integrate with history?)
|
|
162
|
+
const trackPastedComments = (onUpdate: NonNullable<CommentsOptions['onUpdate']>) => {
|
|
163
|
+
// Cut/deleted comments.
|
|
164
|
+
// Tracks indexed selections within text.
|
|
165
|
+
let tracked: { text: Text; comments: { id: string; from: number; to: number }[] } | null = null;
|
|
166
|
+
|
|
167
|
+
// Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
|
|
168
|
+
const handleTrack = (event: Event, view: EditorView) => {
|
|
169
|
+
const comments = view.state.field(commentsStateField);
|
|
170
|
+
const { main } = view.state.selection;
|
|
171
|
+
const selectedRanges = comments.ranges.filter(
|
|
172
|
+
(range) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
if (!selectedRanges.length) {
|
|
176
|
+
tracked = null;
|
|
177
|
+
} else {
|
|
178
|
+
tracked = {
|
|
179
|
+
text: view.state.doc.slice(main.from, main.to),
|
|
180
|
+
comments: selectedRanges.map((range) => ({
|
|
181
|
+
id: range.id,
|
|
182
|
+
from: range.from - main.from,
|
|
183
|
+
to: range.to - main.from,
|
|
184
|
+
})),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return [
|
|
190
|
+
EditorView.domEventHandlers({
|
|
191
|
+
cut: handleTrack,
|
|
192
|
+
copy: handleTrack,
|
|
193
|
+
}),
|
|
194
|
+
|
|
195
|
+
// Handle paste.
|
|
196
|
+
EditorView.updateListener.of(({ view, state, transactions }) => {
|
|
197
|
+
if (tracked) {
|
|
198
|
+
const paste = transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
199
|
+
if (paste) {
|
|
200
|
+
let found = -1;
|
|
201
|
+
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
202
|
+
if (text.eq(tracked!.text)) {
|
|
203
|
+
for (let i = transactions.indexOf(paste!) + 1; i < transactions.length; i++) {
|
|
204
|
+
fromB = transactions[i].changes.mapPos(fromB);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
found = fromB;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (found > -1) {
|
|
212
|
+
const comments = tracked.comments;
|
|
213
|
+
|
|
214
|
+
// Sync before recomputing cursor/range.
|
|
215
|
+
// TODO(burdon): Decouple from automerge?
|
|
216
|
+
view.state.facet(semaphoreFacet).reconcile(view);
|
|
217
|
+
|
|
218
|
+
const active = state.field(commentsStateField).ranges;
|
|
219
|
+
for (const moved of comments) {
|
|
220
|
+
if (active.some((range) => range.id === moved.id && range.from === range.to)) {
|
|
221
|
+
const range = {
|
|
222
|
+
from: found + moved.from,
|
|
223
|
+
to: found + moved.to,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const cursor = Cursor.getCursorFromRange(state.facet(Cursor.converter), range);
|
|
227
|
+
// console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
|
|
228
|
+
onUpdate(moved.id, cursor);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
tracked = null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}),
|
|
237
|
+
];
|
|
183
238
|
};
|
|
184
239
|
|
|
185
240
|
/**
|
|
@@ -191,27 +246,29 @@ export type CommentsOptions = {
|
|
|
191
246
|
* 4). Tracks the current cursor position to:
|
|
192
247
|
* a). Update the decoration to show if the cursor is within a current selection.
|
|
193
248
|
* b). Calls a handler to indicate which is the closest selection (e.g., to update the thread sidebar).
|
|
194
|
-
* 5). Optionally,
|
|
195
|
-
* 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
249
|
+
* 5). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
196
250
|
*/
|
|
197
251
|
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
252
|
+
const { key: shortcut = "meta-'" } = options;
|
|
253
|
+
|
|
198
254
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
199
255
|
|
|
200
256
|
/**
|
|
201
257
|
* Create comment thread action.
|
|
202
258
|
*/
|
|
203
259
|
const createCommentThread: Command = (view) => {
|
|
260
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
261
|
+
|
|
204
262
|
invariant(options.onCreate);
|
|
205
|
-
const {
|
|
263
|
+
const { from, to } = view.state.selection.main;
|
|
206
264
|
if (from === to) {
|
|
207
265
|
return false;
|
|
208
266
|
}
|
|
209
267
|
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
if (relPos) {
|
|
268
|
+
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
269
|
+
if (cursor) {
|
|
213
270
|
// Create thread via callback.
|
|
214
|
-
const id = callbackWrapper(options.onCreate)(
|
|
271
|
+
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
215
272
|
if (id) {
|
|
216
273
|
// Update range.
|
|
217
274
|
view.dispatch({
|
|
@@ -219,15 +276,6 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
219
276
|
selection: { anchor: from },
|
|
220
277
|
});
|
|
221
278
|
|
|
222
|
-
// Insert footnote.
|
|
223
|
-
if (options.footnote) {
|
|
224
|
-
const tag = `[^${id}]`;
|
|
225
|
-
view.dispatch({
|
|
226
|
-
changes: { from: head, insert: tag },
|
|
227
|
-
selection: { anchor: head + tag.length },
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
279
|
return true;
|
|
232
280
|
}
|
|
233
281
|
}
|
|
@@ -235,44 +283,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
235
283
|
return false;
|
|
236
284
|
};
|
|
237
285
|
|
|
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
286
|
return [
|
|
275
|
-
bookmarksViewPlugin,
|
|
276
287
|
commentsStateField,
|
|
277
288
|
highlightDecorations,
|
|
278
289
|
styles,
|
|
@@ -283,8 +294,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
283
294
|
options.onCreate
|
|
284
295
|
? keymap.of([
|
|
285
296
|
{
|
|
286
|
-
key:
|
|
287
|
-
run: createCommentThread,
|
|
297
|
+
key: shortcut,
|
|
298
|
+
run: callbackWrapper(createCommentThread),
|
|
288
299
|
},
|
|
289
300
|
])
|
|
290
301
|
: [],
|
|
@@ -303,9 +314,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
303
314
|
end: selection.to,
|
|
304
315
|
above: true,
|
|
305
316
|
create: () => {
|
|
306
|
-
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
307
317
|
const el = document.createElement('div');
|
|
308
|
-
options.onHover?.(el);
|
|
318
|
+
options.onHover?.(el, shortcut);
|
|
309
319
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
310
320
|
},
|
|
311
321
|
};
|
|
@@ -321,27 +331,27 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
321
331
|
// Monitor cursor movement and text updates.
|
|
322
332
|
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
323
333
|
//
|
|
324
|
-
EditorView.updateListener.of(({ view, state,
|
|
334
|
+
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
335
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
336
|
+
|
|
325
337
|
//
|
|
326
338
|
// Test if need to recompute indexed range if document changes before the end of the range.
|
|
327
339
|
//
|
|
328
340
|
{
|
|
329
341
|
let mod = false;
|
|
330
|
-
const model = state.field(modelState);
|
|
331
342
|
const { active, ranges } = state.field(commentsStateField);
|
|
332
343
|
changes.iterChanges((from, to, from2, to2) => {
|
|
333
|
-
|
|
344
|
+
// TODO(burdon): Skip deleted (tracked) ranges.
|
|
334
345
|
ranges.forEach((range) => {
|
|
335
346
|
if (from2 === to2) {
|
|
336
|
-
const newRange =
|
|
337
|
-
if (newRange.to - newRange.from === 0) {
|
|
338
|
-
|
|
339
|
-
log.info('deleted comment', { thread: range.id });
|
|
347
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
348
|
+
if (!newRange || newRange.to - newRange.from === 0) {
|
|
349
|
+
options.onDelete?.(range.id);
|
|
340
350
|
}
|
|
341
351
|
}
|
|
342
352
|
|
|
343
353
|
if (from <= range.to) {
|
|
344
|
-
const newRange =
|
|
354
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
345
355
|
Object.assign(range, newRange);
|
|
346
356
|
mod = true;
|
|
347
357
|
}
|
|
@@ -362,6 +372,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
362
372
|
let min = Infinity;
|
|
363
373
|
const { active, closest, ranges } = state.field(commentsStateField);
|
|
364
374
|
const selected: CommentSelected = { active: undefined, closest: undefined };
|
|
375
|
+
|
|
376
|
+
// TODO(burdon): Skip deleted (tracked) ranges.
|
|
365
377
|
ranges.forEach((comment) => {
|
|
366
378
|
const d = Math.min(Math.abs(head - comment.from), Math.abs(head - comment.to));
|
|
367
379
|
if (head >= comment.from && head <= comment.to) {
|
|
@@ -384,5 +396,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
384
396
|
}
|
|
385
397
|
}
|
|
386
398
|
}),
|
|
399
|
+
|
|
400
|
+
options.onUpdate ? trackPastedComments(options.onUpdate) : [],
|
|
387
401
|
];
|
|
388
402
|
};
|
|
@@ -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';
|