@dxos/react-ui-editor 0.3.11-main.870164f → 0.3.11-main.8a9dd1e
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 +1707 -1320
- 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 -5
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +12 -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/{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/{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 -1
- 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 +4 -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/code.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +6 -2
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- 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 -1
- package/dist/types/src/extensions/image.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +4 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/link.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/tasklist.d.ts +5 -1
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
- 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 +3 -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/themes/default.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +24 -22
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +25 -23
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +69 -65
- 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/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
- package/src/extensions/autocomplete.ts +18 -17
- package/src/{hooks/automerge/plugin.ts → extensions/automerge/automerge.ts} +20 -58
- package/src/extensions/automerge/cursor.ts +52 -0
- package/src/extensions/automerge/defs.ts +62 -0
- package/src/extensions/automerge/index.ts +7 -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/extensions/code.ts +14 -7
- package/src/extensions/comments.ts +95 -110
- package/src/extensions/cursor.ts +46 -0
- package/src/extensions/hr.ts +4 -5
- package/src/extensions/image.ts +40 -22
- package/src/extensions/index.ts +4 -1
- package/src/extensions/link.ts +86 -88
- package/src/extensions/markdown/highlight.ts +3 -12
- package/src/extensions/table.ts +10 -4
- package/src/extensions/tasklist.ts +53 -79
- 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 -97
- package/src/hooks/{yjs/space-provider.ts → yjs.ts} +49 -19
- package/src/index.ts +3 -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/themes/default.ts +51 -78
- 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/util.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/extensions/mermaid.ts +0 -11
- package/src/hooks/automerge/handle.ts +0 -14
- package/src/hooks/automerge/index.ts +0 -6
- package/src/hooks/yjs/index.ts +0 -6
- /package/dist/types/src/{hooks → extensions}/automerge/update-codemirror.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/{extensions/util.d.ts → util.d.ts} +0 -0
- /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
- /package/src/{extensions/util.ts → util.ts} +0 -0
|
@@ -2,21 +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';
|
|
19
|
-
import get from 'lodash.get';
|
|
5
|
+
import { type Extension, StateEffect, StateField, type Text } from '@codemirror/state';
|
|
6
|
+
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
20
7
|
import sortBy from 'lodash.sortby';
|
|
21
8
|
|
|
22
9
|
import { debounce } from '@dxos/async';
|
|
@@ -24,29 +11,21 @@ import { invariant } from '@dxos/invariant';
|
|
|
24
11
|
import { log } from '@dxos/log';
|
|
25
12
|
import { nonNullable } from '@dxos/util';
|
|
26
13
|
|
|
27
|
-
import {
|
|
28
|
-
import { type CommentRange, type EditorModel,
|
|
29
|
-
import {
|
|
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';
|
|
30
18
|
|
|
31
19
|
// TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
|
|
32
20
|
// TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
|
|
33
21
|
|
|
34
22
|
// TODO(burdon): Reconcile with theme.
|
|
35
23
|
const styles = EditorView.baseTheme({
|
|
36
|
-
'& .cm-bookmark': {
|
|
37
|
-
cursor: 'pointer',
|
|
38
|
-
margin: '4px',
|
|
39
|
-
padding: '4px',
|
|
40
|
-
backgroundColor: 'yellow',
|
|
41
|
-
},
|
|
42
|
-
'& .cm-bookmark-selected': {
|
|
43
|
-
backgroundColor: 'orange',
|
|
44
|
-
},
|
|
45
24
|
'& .cm-comment': {
|
|
46
|
-
backgroundColor:
|
|
25
|
+
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
47
26
|
},
|
|
48
27
|
'& .cm-comment-active': {
|
|
49
|
-
backgroundColor:
|
|
28
|
+
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
50
29
|
},
|
|
51
30
|
});
|
|
52
31
|
|
|
@@ -99,6 +78,8 @@ const setCommentState = StateEffect.define<CommentsState>();
|
|
|
99
78
|
const commentsStateField = StateField.define<CommentsState>({
|
|
100
79
|
create: () => ({ ranges: [] }),
|
|
101
80
|
update: (value, tr) => {
|
|
81
|
+
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
82
|
+
|
|
102
83
|
for (const effect of tr.effects) {
|
|
103
84
|
// Update selection.
|
|
104
85
|
if (effect.is(setSelection)) {
|
|
@@ -107,10 +88,10 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
107
88
|
|
|
108
89
|
// Update range from store.
|
|
109
90
|
if (effect.is(setCommentRange)) {
|
|
110
|
-
const {
|
|
91
|
+
const { comments } = effect.value;
|
|
111
92
|
const ranges: ExtendedCommentRange[] = comments
|
|
112
93
|
.map((comment) => {
|
|
113
|
-
const range =
|
|
94
|
+
const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
|
|
114
95
|
return range && { ...comment, ...range };
|
|
115
96
|
})
|
|
116
97
|
.filter(nonNullable);
|
|
@@ -153,42 +134,17 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
|
|
|
153
134
|
return Decoration.set(decorations);
|
|
154
135
|
});
|
|
155
136
|
|
|
156
|
-
/**
|
|
157
|
-
* Optional bookmark widget at the start/end of the selection.
|
|
158
|
-
* May correspond to a markdown bookmark.
|
|
159
|
-
*/
|
|
160
|
-
class BookmarkWidget extends WidgetType {
|
|
161
|
-
constructor(
|
|
162
|
-
private readonly _anchor: number,
|
|
163
|
-
private readonly _id: string,
|
|
164
|
-
private readonly _handleClick?: () => void,
|
|
165
|
-
) {
|
|
166
|
-
super();
|
|
167
|
-
invariant(this._id);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
override eq(other: BookmarkWidget) {
|
|
171
|
-
return this._anchor === other._anchor && this._id === other._id;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
override toDOM() {
|
|
175
|
-
const span = document.createElement('span');
|
|
176
|
-
span.className = 'cm-bookmark';
|
|
177
|
-
span.textContent = '※';
|
|
178
|
-
if (this._handleClick) {
|
|
179
|
-
span.onclick = () => this._handleClick!();
|
|
180
|
-
}
|
|
181
|
-
return span;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
137
|
export type CommentsOptions = {
|
|
186
138
|
key?: string;
|
|
187
139
|
footnote?: boolean;
|
|
188
140
|
/**
|
|
189
141
|
* Called to create a new thread and return the thread id.
|
|
190
142
|
*/
|
|
191
|
-
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;
|
|
192
148
|
/**
|
|
193
149
|
* Called to notify which thread is currently closest to the cursor.
|
|
194
150
|
*/
|
|
@@ -196,7 +152,66 @@ export type CommentsOptions = {
|
|
|
196
152
|
/**
|
|
197
153
|
* Called to render tooltip.
|
|
198
154
|
*/
|
|
199
|
-
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
|
+
];
|
|
200
215
|
};
|
|
201
216
|
|
|
202
217
|
/**
|
|
@@ -212,23 +227,26 @@ export type CommentsOptions = {
|
|
|
212
227
|
* 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
213
228
|
*/
|
|
214
229
|
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
230
|
+
const { key: shortcut = "meta-'" } = options;
|
|
231
|
+
|
|
215
232
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
216
233
|
|
|
217
234
|
/**
|
|
218
235
|
* Create comment thread action.
|
|
219
236
|
*/
|
|
220
237
|
const createCommentThread: Command = (view) => {
|
|
238
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
239
|
+
|
|
221
240
|
invariant(options.onCreate);
|
|
222
241
|
const { head, from, to } = view.state.selection.main;
|
|
223
242
|
if (from === to) {
|
|
224
243
|
return false;
|
|
225
244
|
}
|
|
226
245
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
if (relPos) {
|
|
246
|
+
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
247
|
+
if (cursor) {
|
|
230
248
|
// Create thread via callback.
|
|
231
|
-
const id = callbackWrapper(options.onCreate)(
|
|
249
|
+
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
232
250
|
if (id) {
|
|
233
251
|
// Update range.
|
|
234
252
|
view.dispatch({
|
|
@@ -252,44 +270,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
252
270
|
return false;
|
|
253
271
|
};
|
|
254
272
|
|
|
255
|
-
/**
|
|
256
|
-
* Bookmark widget decoration.
|
|
257
|
-
*/
|
|
258
|
-
const bookmarksViewPlugin = ViewPlugin.fromClass(
|
|
259
|
-
class BookmarkViewPlugin {
|
|
260
|
-
// Match markdown footnotes (option).
|
|
261
|
-
// https://www.markdownguide.org/extended-syntax/#footnotes
|
|
262
|
-
static bookmarkMatcher = new MatchDecorator({
|
|
263
|
-
regexp: /\[\^(\w+)\]/g,
|
|
264
|
-
decoration: (match, view, pos) => {
|
|
265
|
-
const id = match[1];
|
|
266
|
-
return Decoration.replace({
|
|
267
|
-
id,
|
|
268
|
-
widget: new BookmarkWidget(pos, id),
|
|
269
|
-
});
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
bookmarks: DecorationSet;
|
|
274
|
-
constructor(view: EditorView) {
|
|
275
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.createDeco(view);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
update(update: ViewUpdate) {
|
|
279
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.updateDeco(update, this.bookmarks);
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
decorations: (instance) => instance.bookmarks,
|
|
284
|
-
provide: (plugin) =>
|
|
285
|
-
EditorView.atomicRanges.of((view) => {
|
|
286
|
-
return view.plugin(plugin)?.bookmarks || Decoration.none;
|
|
287
|
-
}),
|
|
288
|
-
},
|
|
289
|
-
);
|
|
290
|
-
|
|
291
273
|
return [
|
|
292
|
-
bookmarksViewPlugin,
|
|
293
274
|
commentsStateField,
|
|
294
275
|
highlightDecorations,
|
|
295
276
|
styles,
|
|
@@ -300,7 +281,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
300
281
|
options.onCreate
|
|
301
282
|
? keymap.of([
|
|
302
283
|
{
|
|
303
|
-
key:
|
|
284
|
+
key: shortcut,
|
|
304
285
|
run: callbackWrapper(createCommentThread),
|
|
305
286
|
},
|
|
306
287
|
])
|
|
@@ -322,7 +303,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
322
303
|
create: () => {
|
|
323
304
|
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
324
305
|
const el = document.createElement('div');
|
|
325
|
-
options.onHover?.(el);
|
|
306
|
+
options.onHover?.(el, shortcut);
|
|
326
307
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
327
308
|
},
|
|
328
309
|
};
|
|
@@ -338,19 +319,19 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
338
319
|
// Monitor cursor movement and text updates.
|
|
339
320
|
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
340
321
|
//
|
|
341
|
-
EditorView.updateListener.of(({ view, state,
|
|
322
|
+
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
323
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
324
|
+
|
|
342
325
|
//
|
|
343
326
|
// Test if need to recompute indexed range if document changes before the end of the range.
|
|
344
327
|
//
|
|
345
328
|
{
|
|
346
329
|
let mod = false;
|
|
347
|
-
const model = state.field(modelState);
|
|
348
330
|
const { active, ranges } = state.field(commentsStateField);
|
|
349
331
|
changes.iterChanges((from, to, from2, to2) => {
|
|
350
|
-
invariant(model);
|
|
351
332
|
ranges.forEach((range) => {
|
|
352
333
|
if (from2 === to2) {
|
|
353
|
-
const newRange =
|
|
334
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
354
335
|
if (!newRange || newRange.to - newRange.from === 0) {
|
|
355
336
|
// TODO(burdon): Delete range if empty.
|
|
356
337
|
log.info('deleted comment', { thread: range.id });
|
|
@@ -358,7 +339,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
358
339
|
}
|
|
359
340
|
|
|
360
341
|
if (from <= range.to) {
|
|
361
|
-
const newRange =
|
|
342
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
362
343
|
Object.assign(range, newRange);
|
|
363
344
|
mod = true;
|
|
364
345
|
}
|
|
@@ -390,6 +371,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
390
371
|
}
|
|
391
372
|
});
|
|
392
373
|
|
|
374
|
+
// TODO(burdon): Fire debounced callback if range selected (to show hint).
|
|
375
|
+
|
|
393
376
|
if (selected.active !== active || selected.closest !== closest) {
|
|
394
377
|
view.dispatch({ effects: setSelection.of(selected) });
|
|
395
378
|
|
|
@@ -401,5 +384,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
401
384
|
}
|
|
402
385
|
}
|
|
403
386
|
}),
|
|
387
|
+
|
|
388
|
+
options.onMove ? trackPastedComments(options.onMove) : [],
|
|
404
389
|
];
|
|
405
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
|
+
}
|
package/src/extensions/hr.ts
CHANGED
|
@@ -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';
|
package/src/extensions/image.ts
CHANGED
|
@@ -3,28 +3,45 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { syntaxTree } from '@codemirror/language';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
type Extension,
|
|
9
|
-
type RangeSet,
|
|
10
|
-
RangeSetBuilder,
|
|
11
|
-
StateField,
|
|
12
|
-
type Transaction,
|
|
13
|
-
} from '@codemirror/state';
|
|
14
|
-
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
6
|
+
import { type EditorState, type Extension, StateField, type Transaction, type Range } from '@codemirror/state';
|
|
7
|
+
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
|
15
8
|
|
|
16
9
|
export type ImageOptions = {};
|
|
17
10
|
|
|
18
11
|
export const image = (options: ImageOptions = {}): Extension => {
|
|
19
|
-
return StateField.define<
|
|
20
|
-
create: (state) =>
|
|
21
|
-
|
|
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
|
+
},
|
|
22
39
|
provide: (field) => EditorView.decorations.from(field),
|
|
23
40
|
});
|
|
24
41
|
};
|
|
25
42
|
|
|
26
|
-
const
|
|
27
|
-
const
|
|
43
|
+
const buildDecorations = (from: number, to: number, state: EditorState) => {
|
|
44
|
+
const decorations: Range<Decoration>[] = [];
|
|
28
45
|
const cursor = state.selection.main.head;
|
|
29
46
|
|
|
30
47
|
syntaxTree(state).iterate({
|
|
@@ -34,20 +51,20 @@ const update = (state: EditorState, options: ImageOptions) => {
|
|
|
34
51
|
if (urlNode) {
|
|
35
52
|
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
|
36
53
|
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
37
|
-
|
|
38
|
-
hide ? node.from : node.to,
|
|
39
|
-
node.to,
|
|
54
|
+
decorations.push(
|
|
40
55
|
Decoration.replace({
|
|
41
56
|
block: true, // Prevent cursor from entering.
|
|
42
57
|
widget: new ImageWidget(url),
|
|
43
|
-
}),
|
|
58
|
+
}).range(hide ? node.from : node.to, node.to),
|
|
44
59
|
);
|
|
45
60
|
}
|
|
46
61
|
}
|
|
47
62
|
},
|
|
63
|
+
from,
|
|
64
|
+
to,
|
|
48
65
|
});
|
|
49
66
|
|
|
50
|
-
return
|
|
67
|
+
return decorations;
|
|
51
68
|
};
|
|
52
69
|
|
|
53
70
|
class ImageWidget extends WidgetType {
|
|
@@ -55,15 +72,16 @@ class ImageWidget extends WidgetType {
|
|
|
55
72
|
super();
|
|
56
73
|
}
|
|
57
74
|
|
|
58
|
-
|
|
59
|
-
override eq(other: WidgetType) {
|
|
75
|
+
override eq(other: this) {
|
|
60
76
|
return this._url === (other as any as ImageWidget)._url;
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
toDOM(view: EditorView) {
|
|
79
|
+
override toDOM(view: EditorView) {
|
|
64
80
|
const img = document.createElement('img');
|
|
65
81
|
img.setAttribute('src', this._url);
|
|
66
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');
|
|
67
85
|
return img;
|
|
68
86
|
}
|
|
69
87
|
}
|
package/src/extensions/index.ts
CHANGED
|
@@ -3,17 +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
21
|
export * from './typewriter';
|
|
22
|
+
export * from './yjs';
|