@dxos/react-ui-editor 0.3.11-main.c86e105 → 0.3.11-main.cd64fe0
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 +84 -96
- 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 +3 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts +4 -4
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/semaphore.d.ts +4 -9
- package/dist/types/src/extensions/automerge/semaphore.d.ts.map +1 -1
- package/dist/types/src/extensions/code.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +9 -4
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/hr.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/outliner.d.ts +4 -0
- package/dist/types/src/extensions/outliner.d.ts.map +1 -0
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/package.json +23 -23
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +16 -14
- package/src/components/TextEditor/TextEditor.tsx +12 -12
- package/src/components/TextEditor/comments.stories.tsx +74 -48
- package/src/extensions/automerge/automerge.ts +10 -12
- package/src/extensions/automerge/defs.ts +13 -9
- package/src/extensions/automerge/semaphore.ts +10 -12
- package/src/extensions/awareness.ts +2 -1
- package/src/extensions/code.ts +15 -17
- package/src/extensions/comments.ts +68 -50
- package/src/extensions/hr.ts +6 -7
- package/src/extensions/index.ts +1 -0
- package/src/extensions/link.ts +3 -3
- package/src/extensions/outliner.ts +12 -0
- package/src/extensions/tasklist.ts +1 -0
- package/src/hooks/index.ts +2 -1
|
@@ -8,17 +8,14 @@ import sortBy from 'lodash.sortby';
|
|
|
8
8
|
|
|
9
9
|
import { debounce } from '@dxos/async';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import { log } from '@dxos/log';
|
|
12
11
|
import { nonNullable } from '@dxos/util';
|
|
13
12
|
|
|
13
|
+
import { semaphoreFacet } from './automerge/defs';
|
|
14
14
|
import { Cursor } from './cursor';
|
|
15
15
|
import { type CommentRange, type EditorModel, type Range } from '../hooks';
|
|
16
16
|
import { getToken } from '../styles';
|
|
17
17
|
import { callbackWrapper } from '../util';
|
|
18
18
|
|
|
19
|
-
// TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
|
|
20
|
-
// TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
|
|
21
|
-
|
|
22
19
|
// TODO(burdon): Reconcile with theme.
|
|
23
20
|
const styles = EditorView.baseTheme({
|
|
24
21
|
'& .cm-comment': {
|
|
@@ -60,7 +57,7 @@ export const setFocus = (view: EditorView, thread: string) => {
|
|
|
60
57
|
view.dispatch({
|
|
61
58
|
effects: setSelection.of({ active: thread }),
|
|
62
59
|
selection: { anchor: range.from },
|
|
63
|
-
scrollIntoView: true,
|
|
60
|
+
scrollIntoView: true, // TODO(burdon): Scroll to y-position (or center of screen?)
|
|
64
61
|
});
|
|
65
62
|
};
|
|
66
63
|
|
|
@@ -72,8 +69,7 @@ const setCommentState = StateEffect.define<CommentsState>();
|
|
|
72
69
|
|
|
73
70
|
/**
|
|
74
71
|
* State field (reducer) that tracks comment ranges.
|
|
75
|
-
* The ranges are tracked as
|
|
76
|
-
* 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.
|
|
77
73
|
*/
|
|
78
74
|
const commentsStateField = StateField.define<CommentsState>({
|
|
79
75
|
create: () => ({ ranges: [] }),
|
|
@@ -92,6 +88,7 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
92
88
|
const ranges: ExtendedCommentRange[] = comments
|
|
93
89
|
.map((comment) => {
|
|
94
90
|
const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
|
|
91
|
+
// console.log('update', JSON.stringify({ range, cursor: comment.cursor }, undefined, 2));
|
|
95
92
|
return range && { ...comment, ...range };
|
|
96
93
|
})
|
|
97
94
|
.filter(nonNullable);
|
|
@@ -135,16 +132,22 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
|
|
|
135
132
|
});
|
|
136
133
|
|
|
137
134
|
export type CommentsOptions = {
|
|
135
|
+
/**
|
|
136
|
+
* Key shortcut to create a new thread.
|
|
137
|
+
*/
|
|
138
138
|
key?: string;
|
|
139
|
-
footnote?: boolean;
|
|
140
139
|
/**
|
|
141
140
|
* Called to create a new thread and return the thread id.
|
|
142
141
|
*/
|
|
143
142
|
onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
|
|
143
|
+
/**
|
|
144
|
+
* Selection cut/deleted.
|
|
145
|
+
*/
|
|
146
|
+
onDelete?: (thread: string) => void;
|
|
144
147
|
/**
|
|
145
148
|
* Called when a comment is moved.
|
|
146
149
|
*/
|
|
147
|
-
|
|
150
|
+
onUpdate?: (thread: string, cursor: string) => void;
|
|
148
151
|
/**
|
|
149
152
|
* Called to notify which thread is currently closest to the cursor.
|
|
150
153
|
*/
|
|
@@ -155,58 +158,80 @@ export type CommentsOptions = {
|
|
|
155
158
|
onHover?: (el: Element, shortcut: string) => void;
|
|
156
159
|
};
|
|
157
160
|
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
160
166
|
|
|
161
|
-
|
|
167
|
+
// Track cut or copy (enables cut-and-paste and copy-delete-paste to restore comment selection).
|
|
168
|
+
const handleTrack = (event: Event, view: EditorView) => {
|
|
162
169
|
const comments = view.state.field(commentsStateField);
|
|
163
170
|
const { main } = view.state.selection;
|
|
164
|
-
const
|
|
171
|
+
const selectedRanges = comments.ranges.filter(
|
|
165
172
|
(range) => range.from >= main.from && range.to <= main.to && range.from < range.to,
|
|
166
173
|
);
|
|
167
|
-
|
|
174
|
+
|
|
175
|
+
if (!selectedRanges.length) {
|
|
168
176
|
tracked = null;
|
|
169
177
|
} else {
|
|
170
178
|
tracked = {
|
|
171
179
|
text: view.state.doc.slice(main.from, main.to),
|
|
172
|
-
comments:
|
|
180
|
+
comments: selectedRanges.map((range) => ({
|
|
181
|
+
id: range.id,
|
|
182
|
+
from: range.from - main.from,
|
|
183
|
+
to: range.to - main.from,
|
|
184
|
+
})),
|
|
173
185
|
};
|
|
174
186
|
}
|
|
187
|
+
|
|
188
|
+
// console.log('track', JSON.stringify({ tracked }, undefined, 2));
|
|
175
189
|
};
|
|
176
190
|
|
|
177
191
|
return [
|
|
178
192
|
EditorView.domEventHandlers({
|
|
179
|
-
cut:
|
|
180
|
-
copy:
|
|
193
|
+
cut: handleTrack,
|
|
194
|
+
copy: handleTrack,
|
|
181
195
|
}),
|
|
182
196
|
|
|
183
|
-
|
|
197
|
+
// Handle paste.
|
|
198
|
+
EditorView.updateListener.of(({ view, state, transactions }) => {
|
|
184
199
|
if (tracked) {
|
|
185
|
-
const paste =
|
|
200
|
+
const paste = transactions.find((tr) => tr.isUserEvent('input.paste'));
|
|
186
201
|
if (paste) {
|
|
187
202
|
let found = -1;
|
|
188
203
|
paste.changes.iterChanges((fromA, toA, fromB, toB, text) => {
|
|
189
204
|
if (text.eq(tracked!.text)) {
|
|
190
|
-
for (let i =
|
|
191
|
-
fromB =
|
|
205
|
+
for (let i = transactions.indexOf(paste!) + 1; i < transactions.length; i++) {
|
|
206
|
+
fromB = transactions[i].changes.mapPos(fromB);
|
|
192
207
|
}
|
|
208
|
+
|
|
193
209
|
found = fromB;
|
|
194
210
|
}
|
|
195
211
|
});
|
|
212
|
+
|
|
196
213
|
if (found > -1) {
|
|
197
|
-
const
|
|
198
|
-
|
|
214
|
+
const comments = tracked.comments;
|
|
215
|
+
|
|
216
|
+
// Sync before recomputing cursor/range.
|
|
217
|
+
// TODO(burdon): Generalize sync facet to decouple from automerge?
|
|
218
|
+
view.state.facet(semaphoreFacet).reconcile(view);
|
|
219
|
+
|
|
220
|
+
const active = state.field(commentsStateField).ranges;
|
|
221
|
+
for (const moved of comments) {
|
|
199
222
|
if (active.some((range) => range.id === moved.id && range.from === range.to)) {
|
|
200
|
-
|
|
201
|
-
moved.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
);
|
|
223
|
+
const range = {
|
|
224
|
+
from: found + moved.from,
|
|
225
|
+
to: found + moved.to,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const cursor = Cursor.getCursorFromRange(state.facet(Cursor.converter), range);
|
|
229
|
+
// console.log('paste', JSON.stringify({ moved, range, cursor }, undefined, 2));
|
|
230
|
+
onUpdate(moved.id, cursor);
|
|
207
231
|
}
|
|
208
232
|
}
|
|
209
233
|
}
|
|
234
|
+
|
|
210
235
|
tracked = null;
|
|
211
236
|
}
|
|
212
237
|
}
|
|
@@ -223,8 +248,7 @@ const trackPastedComments = (onMove: (threadID: string, cursor: string) => void)
|
|
|
223
248
|
* 4). Tracks the current cursor position to:
|
|
224
249
|
* a). Update the decoration to show if the cursor is within a current selection.
|
|
225
250
|
* b). Calls a handler to indicate which is the closest selection (e.g., to update the thread sidebar).
|
|
226
|
-
* 5). Optionally,
|
|
227
|
-
* 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
251
|
+
* 5). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
228
252
|
*/
|
|
229
253
|
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
230
254
|
const { key: shortcut = "meta-'" } = options;
|
|
@@ -238,7 +262,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
238
262
|
const cursorConverter = view.state.facet(Cursor.converter);
|
|
239
263
|
|
|
240
264
|
invariant(options.onCreate);
|
|
241
|
-
const {
|
|
265
|
+
const { from, to } = view.state.selection.main;
|
|
242
266
|
if (from === to) {
|
|
243
267
|
return false;
|
|
244
268
|
}
|
|
@@ -254,15 +278,6 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
254
278
|
selection: { anchor: from },
|
|
255
279
|
});
|
|
256
280
|
|
|
257
|
-
// Insert footnote.
|
|
258
|
-
if (options.footnote) {
|
|
259
|
-
const tag = `[^${id}]`;
|
|
260
|
-
view.dispatch({
|
|
261
|
-
changes: { from: head, insert: tag },
|
|
262
|
-
selection: { anchor: head + tag.length },
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
281
|
return true;
|
|
267
282
|
}
|
|
268
283
|
}
|
|
@@ -301,9 +316,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
301
316
|
end: selection.to,
|
|
302
317
|
above: true,
|
|
303
318
|
create: () => {
|
|
304
|
-
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
305
319
|
const el = document.createElement('div');
|
|
306
|
-
options.onHover
|
|
320
|
+
options.onHover!(el, shortcut);
|
|
307
321
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
308
322
|
},
|
|
309
323
|
};
|
|
@@ -311,7 +325,11 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
311
325
|
|
|
312
326
|
return null;
|
|
313
327
|
},
|
|
314
|
-
{
|
|
328
|
+
{
|
|
329
|
+
// TODO(burdon): Hide on change triggered immediately?
|
|
330
|
+
// hideOnChange: true,
|
|
331
|
+
hoverTime: 1_000,
|
|
332
|
+
},
|
|
315
333
|
)
|
|
316
334
|
: [],
|
|
317
335
|
|
|
@@ -329,12 +347,12 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
329
347
|
let mod = false;
|
|
330
348
|
const { active, ranges } = state.field(commentsStateField);
|
|
331
349
|
changes.iterChanges((from, to, from2, to2) => {
|
|
350
|
+
// TODO(burdon): Skip deleted (tracked) ranges.
|
|
332
351
|
ranges.forEach((range) => {
|
|
333
352
|
if (from2 === to2) {
|
|
334
353
|
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
335
354
|
if (!newRange || newRange.to - newRange.from === 0) {
|
|
336
|
-
|
|
337
|
-
log.info('deleted comment', { thread: range.id });
|
|
355
|
+
options.onDelete?.(range.id);
|
|
338
356
|
}
|
|
339
357
|
}
|
|
340
358
|
|
|
@@ -360,6 +378,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
360
378
|
let min = Infinity;
|
|
361
379
|
const { active, closest, ranges } = state.field(commentsStateField);
|
|
362
380
|
const selected: CommentSelected = { active: undefined, closest: undefined };
|
|
381
|
+
|
|
382
|
+
// TODO(burdon): Skip deleted (tracked) ranges.
|
|
363
383
|
ranges.forEach((comment) => {
|
|
364
384
|
const d = Math.min(Math.abs(head - comment.from), Math.abs(head - comment.to));
|
|
365
385
|
if (head >= comment.from && head <= comment.to) {
|
|
@@ -371,8 +391,6 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
371
391
|
}
|
|
372
392
|
});
|
|
373
393
|
|
|
374
|
-
// TODO(burdon): Fire debounced callback if range selected (to show hint).
|
|
375
|
-
|
|
376
394
|
if (selected.active !== active || selected.closest !== closest) {
|
|
377
395
|
view.dispatch({ effects: setSelection.of(selected) });
|
|
378
396
|
|
|
@@ -385,6 +403,6 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
385
403
|
}
|
|
386
404
|
}),
|
|
387
405
|
|
|
388
|
-
options.
|
|
406
|
+
options.onUpdate ? trackPastedComments(options.onUpdate) : [],
|
|
389
407
|
];
|
|
390
408
|
};
|
package/src/extensions/hr.ts
CHANGED
|
@@ -33,15 +33,19 @@ class HorizontalRuleWidget extends WidgetType {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
override toDOM(view: EditorView) {
|
|
36
|
-
// TODO(burdon): Create <hr> element? Does this clash with markdown parser?
|
|
37
36
|
const el = document.createElement('div');
|
|
38
37
|
el.className = 'cm-hr';
|
|
39
38
|
return el;
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
|
|
42
|
+
// TODO(burdon): Like Tasklist, allow cursor to move into range.
|
|
43
|
+
|
|
44
|
+
// NOTE: Without a blank line before this markup will treat the previous line as a heading.
|
|
45
|
+
// https://www.markdownguide.org/basic-syntax/#horizontal-rules
|
|
43
46
|
const placeholderMatcher = new MatchDecorator({
|
|
44
|
-
regexp:
|
|
47
|
+
// regexp: /(?<=\n\n)---/gs,
|
|
48
|
+
regexp: /^---$/gs,
|
|
45
49
|
decoration: (match, view, pos) =>
|
|
46
50
|
Decoration.replace({
|
|
47
51
|
widget: new HorizontalRuleWidget(pos),
|
|
@@ -63,11 +67,6 @@ export const hr = () => [
|
|
|
63
67
|
},
|
|
64
68
|
{
|
|
65
69
|
decorations: (instance) => instance.rules,
|
|
66
|
-
// TODO(burdon): Is this really atomic (cursor can move into ---).
|
|
67
|
-
provide: (plugin) =>
|
|
68
|
-
EditorView.atomicRanges.of((view) => {
|
|
69
|
-
return view.plugin(plugin)?.rules || Decoration.none;
|
|
70
|
-
}),
|
|
71
70
|
},
|
|
72
71
|
),
|
|
73
72
|
];
|
package/src/extensions/index.ts
CHANGED
package/src/extensions/link.ts
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
import { syntaxTree } from '@codemirror/language';
|
|
7
7
|
import { type Extension, RangeSetBuilder } from '@codemirror/state';
|
|
8
8
|
import {
|
|
9
|
+
hoverTooltip,
|
|
9
10
|
Decoration,
|
|
11
|
+
type DecorationSet,
|
|
10
12
|
type EditorView,
|
|
11
|
-
WidgetType,
|
|
12
|
-
hoverTooltip,
|
|
13
13
|
ViewPlugin,
|
|
14
14
|
type ViewUpdate,
|
|
15
|
-
|
|
15
|
+
WidgetType,
|
|
16
16
|
} from '@codemirror/view';
|
|
17
17
|
import { type SyntaxNode } from '@lezer/common';
|
|
18
18
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
|
|
7
|
+
export type OutlinerOptions = {};
|
|
8
|
+
|
|
9
|
+
// TODO(burdon): Experimental component: multiple hierarchical draggable editor sections.
|
|
10
|
+
export const outliner = (options: OutlinerOptions = {}): Extension => {
|
|
11
|
+
return [];
|
|
12
|
+
};
|
|
@@ -64,6 +64,7 @@ export const tasklist = (options: TasklistOptions = {}) => {
|
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
decorations: (v) => v.decorations,
|
|
67
|
+
// TODO(burdon): Is this still required?
|
|
67
68
|
provide: (plugin) =>
|
|
68
69
|
EditorView.atomicRanges.of((view) => {
|
|
69
70
|
return view.plugin(plugin)?.decorations || Decoration.none;
|