@dxos/react-ui-editor 0.3.11-main.690ca41 → 0.3.11-main.6ec1cd2
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 +201 -327
- 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 +6 -2
- 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/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/yjs.stories.d.ts +1 -1
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/index.d.ts +1 -0
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -1
- package/dist/types/src/extensions/code.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts +2 -2
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/{util → extensions}/cursor.d.ts +9 -1
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- 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/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts +1 -1
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +0 -5
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.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 +23 -22
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +19 -16
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -46
- package/src/components/TextEditor/TextEditor.tsx +68 -64
- package/src/components/TextEditor/comments.stories.tsx +315 -0
- package/src/extensions/autocomplete.ts +18 -17
- package/src/extensions/automerge/automerge.ts +2 -2
- package/src/extensions/automerge/cursor.ts +2 -1
- package/src/extensions/automerge/index.ts +1 -0
- package/src/extensions/awareness.ts +25 -32
- package/src/extensions/code.ts +10 -2
- package/src/extensions/comments.ts +20 -115
- package/src/extensions/cursor.ts +46 -0
- package/src/extensions/hr.ts +2 -2
- package/src/extensions/image.ts +2 -2
- package/src/extensions/index.ts +1 -0
- package/src/extensions/link.ts +4 -4
- package/src/extensions/markdown/highlight.ts +0 -8
- package/src/extensions/table.ts +6 -3
- package/src/extensions/tasklist.ts +2 -2
- package/src/extensions/yjs/cursor.ts +2 -1
- package/src/extensions/yjs/yjs.ts +2 -2
- package/src/hooks/useTextModel.ts +4 -7
- package/src/hooks/yjs.ts +1 -23
- package/src/index.ts +2 -0
- package/src/styles/markdown.ts +0 -2
- package/src/themes/default.ts +17 -42
- package/dist/types/src/util/cursor.d.ts.map +0 -1
- package/dist/types/src/util/index.d.ts +0 -3
- package/dist/types/src/util/index.d.ts.map +0 -1
- package/dist/types/src/util/util.d.ts.map +0 -1
- package/src/util/cursor.ts +0 -29
- package/src/util/index.ts +0 -6
- /package/dist/types/src/{util/util.d.ts → util.d.ts} +0 -0
- /package/src/{util/util.ts → util.ts} +0 -0
|
@@ -3,19 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
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';
|
|
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,24 +11,16 @@ import { invariant } from '@dxos/invariant';
|
|
|
23
11
|
import { log } from '@dxos/log';
|
|
24
12
|
import { nonNullable } from '@dxos/util';
|
|
25
13
|
|
|
14
|
+
import { Cursor } from './cursor';
|
|
26
15
|
import { type CommentRange, type EditorModel, type Range } from '../hooks';
|
|
27
16
|
import { getToken } from '../styles';
|
|
28
|
-
import { callbackWrapper
|
|
17
|
+
import { callbackWrapper } from '../util';
|
|
29
18
|
|
|
30
19
|
// TODO(burdon): Handle delete, cut, copy, and paste (separately) text that includes comment range.
|
|
31
20
|
// TODO(burdon): Consider breaking into separate plugin (since not standalone)? Like mermaid?
|
|
32
21
|
|
|
33
22
|
// TODO(burdon): Reconcile with theme.
|
|
34
23
|
const styles = EditorView.baseTheme({
|
|
35
|
-
'& .cm-bookmark': {
|
|
36
|
-
cursor: 'pointer',
|
|
37
|
-
margin: '4px',
|
|
38
|
-
padding: '4px',
|
|
39
|
-
backgroundColor: 'yellow',
|
|
40
|
-
},
|
|
41
|
-
'& .cm-bookmark-selected': {
|
|
42
|
-
backgroundColor: 'orange',
|
|
43
|
-
},
|
|
44
24
|
'& .cm-comment': {
|
|
45
25
|
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
46
26
|
},
|
|
@@ -98,7 +78,7 @@ const setCommentState = StateEffect.define<CommentsState>();
|
|
|
98
78
|
const commentsStateField = StateField.define<CommentsState>({
|
|
99
79
|
create: () => ({ ranges: [] }),
|
|
100
80
|
update: (value, tr) => {
|
|
101
|
-
const cursorConverter = tr.state.facet(
|
|
81
|
+
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
102
82
|
|
|
103
83
|
for (const effect of tr.effects) {
|
|
104
84
|
// Update selection.
|
|
@@ -111,7 +91,7 @@ const commentsStateField = StateField.define<CommentsState>({
|
|
|
111
91
|
const { comments } = effect.value;
|
|
112
92
|
const ranges: ExtendedCommentRange[] = comments
|
|
113
93
|
.map((comment) => {
|
|
114
|
-
const range = getRangeFromCursor(cursorConverter, comment.cursor);
|
|
94
|
+
const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
|
|
115
95
|
return range && { ...comment, ...range };
|
|
116
96
|
})
|
|
117
97
|
.filter(nonNullable);
|
|
@@ -154,42 +134,13 @@ const highlightDecorations = EditorView.decorations.compute([commentsStateField]
|
|
|
154
134
|
return Decoration.set(decorations);
|
|
155
135
|
});
|
|
156
136
|
|
|
157
|
-
/**
|
|
158
|
-
* Optional bookmark widget at the start/end of the selection.
|
|
159
|
-
* May correspond to a markdown bookmark.
|
|
160
|
-
*/
|
|
161
|
-
class BookmarkWidget extends WidgetType {
|
|
162
|
-
constructor(
|
|
163
|
-
private readonly _anchor: number,
|
|
164
|
-
private readonly _id: string,
|
|
165
|
-
private readonly _handleClick?: () => void,
|
|
166
|
-
) {
|
|
167
|
-
super();
|
|
168
|
-
invariant(this._id);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
override eq(other: BookmarkWidget) {
|
|
172
|
-
return this._anchor === other._anchor && this._id === other._id;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
override toDOM() {
|
|
176
|
-
const span = document.createElement('span');
|
|
177
|
-
span.className = 'cm-bookmark';
|
|
178
|
-
span.textContent = '※';
|
|
179
|
-
if (this._handleClick) {
|
|
180
|
-
span.onclick = () => this._handleClick!();
|
|
181
|
-
}
|
|
182
|
-
return span;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
137
|
export type CommentsOptions = {
|
|
187
138
|
key?: string;
|
|
188
139
|
footnote?: boolean;
|
|
189
140
|
/**
|
|
190
141
|
* Called to create a new thread and return the thread id.
|
|
191
142
|
*/
|
|
192
|
-
onCreate?: (
|
|
143
|
+
onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
|
|
193
144
|
/**
|
|
194
145
|
* Called to notify which thread is currently closest to the cursor.
|
|
195
146
|
*/
|
|
@@ -197,7 +148,7 @@ export type CommentsOptions = {
|
|
|
197
148
|
/**
|
|
198
149
|
* Called to render tooltip.
|
|
199
150
|
*/
|
|
200
|
-
onHover?: (el: Element) => void;
|
|
151
|
+
onHover?: (el: Element, shortcut: string) => void;
|
|
201
152
|
};
|
|
202
153
|
|
|
203
154
|
/**
|
|
@@ -213,13 +164,15 @@ export type CommentsOptions = {
|
|
|
213
164
|
* 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
214
165
|
*/
|
|
215
166
|
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
167
|
+
const { key: shortcut = "meta-'" } = options;
|
|
168
|
+
|
|
216
169
|
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
217
170
|
|
|
218
171
|
/**
|
|
219
172
|
* Create comment thread action.
|
|
220
173
|
*/
|
|
221
174
|
const createCommentThread: Command = (view) => {
|
|
222
|
-
const cursorConverter = view.state.facet(
|
|
175
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
223
176
|
|
|
224
177
|
invariant(options.onCreate);
|
|
225
178
|
const { head, from, to } = view.state.selection.main;
|
|
@@ -227,10 +180,10 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
227
180
|
return false;
|
|
228
181
|
}
|
|
229
182
|
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
183
|
+
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
184
|
+
if (cursor) {
|
|
232
185
|
// Create thread via callback.
|
|
233
|
-
const id = callbackWrapper(options.onCreate)(
|
|
186
|
+
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
234
187
|
if (id) {
|
|
235
188
|
// Update range.
|
|
236
189
|
view.dispatch({
|
|
@@ -254,44 +207,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
254
207
|
return false;
|
|
255
208
|
};
|
|
256
209
|
|
|
257
|
-
/**
|
|
258
|
-
* Bookmark widget decoration.
|
|
259
|
-
*/
|
|
260
|
-
const bookmarksViewPlugin = ViewPlugin.fromClass(
|
|
261
|
-
class BookmarkViewPlugin {
|
|
262
|
-
// Match markdown footnotes (option).
|
|
263
|
-
// https://www.markdownguide.org/extended-syntax/#footnotes
|
|
264
|
-
static bookmarkMatcher = new MatchDecorator({
|
|
265
|
-
regexp: /\[\^(\w+)\]/g,
|
|
266
|
-
decoration: (match, view, pos) => {
|
|
267
|
-
const id = match[1];
|
|
268
|
-
return Decoration.replace({
|
|
269
|
-
id,
|
|
270
|
-
widget: new BookmarkWidget(pos, id),
|
|
271
|
-
});
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
bookmarks: DecorationSet;
|
|
276
|
-
constructor(view: EditorView) {
|
|
277
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.createDeco(view);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
update(update: ViewUpdate) {
|
|
281
|
-
this.bookmarks = BookmarkViewPlugin.bookmarkMatcher.updateDeco(update, this.bookmarks);
|
|
282
|
-
}
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
decorations: (instance) => instance.bookmarks,
|
|
286
|
-
provide: (plugin) =>
|
|
287
|
-
EditorView.atomicRanges.of((view) => {
|
|
288
|
-
return view.plugin(plugin)?.bookmarks || Decoration.none;
|
|
289
|
-
}),
|
|
290
|
-
},
|
|
291
|
-
);
|
|
292
|
-
|
|
293
210
|
return [
|
|
294
|
-
bookmarksViewPlugin,
|
|
295
211
|
commentsStateField,
|
|
296
212
|
highlightDecorations,
|
|
297
213
|
styles,
|
|
@@ -302,7 +218,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
302
218
|
options.onCreate
|
|
303
219
|
? keymap.of([
|
|
304
220
|
{
|
|
305
|
-
key:
|
|
221
|
+
key: shortcut,
|
|
306
222
|
run: callbackWrapper(createCommentThread),
|
|
307
223
|
},
|
|
308
224
|
])
|
|
@@ -324,7 +240,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
324
240
|
create: () => {
|
|
325
241
|
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
326
242
|
const el = document.createElement('div');
|
|
327
|
-
options.onHover?.(el);
|
|
243
|
+
options.onHover?.(el, shortcut);
|
|
328
244
|
return { dom: el, offset: { x: 0, y: 8 } };
|
|
329
245
|
},
|
|
330
246
|
};
|
|
@@ -341,7 +257,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
341
257
|
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
342
258
|
//
|
|
343
259
|
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
344
|
-
const cursorConverter = view.state.facet(
|
|
260
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
345
261
|
|
|
346
262
|
//
|
|
347
263
|
// Test if need to recompute indexed range if document changes before the end of the range.
|
|
@@ -352,7 +268,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
352
268
|
changes.iterChanges((from, to, from2, to2) => {
|
|
353
269
|
ranges.forEach((range) => {
|
|
354
270
|
if (from2 === to2) {
|
|
355
|
-
const newRange = getRangeFromCursor(cursorConverter, range.cursor);
|
|
271
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
356
272
|
if (!newRange || newRange.to - newRange.from === 0) {
|
|
357
273
|
// TODO(burdon): Delete range if empty.
|
|
358
274
|
log.info('deleted comment', { thread: range.id });
|
|
@@ -360,7 +276,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
360
276
|
}
|
|
361
277
|
|
|
362
278
|
if (from <= range.to) {
|
|
363
|
-
const newRange = getRangeFromCursor(cursorConverter, range.cursor);
|
|
279
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
364
280
|
Object.assign(range, newRange);
|
|
365
281
|
mod = true;
|
|
366
282
|
}
|
|
@@ -392,6 +308,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
392
308
|
}
|
|
393
309
|
});
|
|
394
310
|
|
|
311
|
+
// TODO(burdon): Fire debounced callback if range selected (to show hint).
|
|
312
|
+
|
|
395
313
|
if (selected.active !== active || selected.closest !== closest) {
|
|
396
314
|
view.dispatch({ effects: setSelection.of(selected) });
|
|
397
315
|
|
|
@@ -405,16 +323,3 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
405
323
|
}),
|
|
406
324
|
];
|
|
407
325
|
};
|
|
408
|
-
|
|
409
|
-
const getCursorFromRange = (cursorConverter: CursorConverter, range: Range) => {
|
|
410
|
-
const from = cursorConverter.toCursor(range.from);
|
|
411
|
-
const to = cursorConverter.toCursor(range.to, -1);
|
|
412
|
-
return [from, to].join(':');
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
const getRangeFromCursor = (cursorConverter: CursorConverter, cursor: string) => {
|
|
416
|
-
const parts = cursor.split(':');
|
|
417
|
-
const from = cursorConverter.fromCursor(parts[0]);
|
|
418
|
-
const to = cursorConverter.fromCursor(parts[1]);
|
|
419
|
-
return from !== undefined && to !== undefined ? { from, to } : undefined;
|
|
420
|
-
};
|
|
@@ -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
|
@@ -28,11 +28,11 @@ class HorizontalRuleWidget extends WidgetType {
|
|
|
28
28
|
super();
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
override eq(other:
|
|
31
|
+
override eq(other: this) {
|
|
32
32
|
return this._pos === (other as any as HorizontalRuleWidget)._pos;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
toDOM(view: EditorView) {
|
|
35
|
+
override toDOM(view: EditorView) {
|
|
36
36
|
// TODO(burdon): Create <hr> element? Does this clash with markdown parser?
|
|
37
37
|
const el = document.createElement('div');
|
|
38
38
|
el.className = 'cm-hr';
|
package/src/extensions/image.ts
CHANGED
|
@@ -72,11 +72,11 @@ class ImageWidget extends WidgetType {
|
|
|
72
72
|
super();
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
override eq(other:
|
|
75
|
+
override eq(other: this) {
|
|
76
76
|
return this._url === (other as any as ImageWidget)._url;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
toDOM(view: EditorView) {
|
|
79
|
+
override toDOM(view: EditorView) {
|
|
80
80
|
const img = document.createElement('img');
|
|
81
81
|
img.setAttribute('src', this._url);
|
|
82
82
|
img.setAttribute('class', 'cm-image');
|
package/src/extensions/index.ts
CHANGED
package/src/extensions/link.ts
CHANGED
|
@@ -93,11 +93,11 @@ class LinkButton extends WidgetType {
|
|
|
93
93
|
super();
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
override eq(other:
|
|
96
|
+
override eq(other: this) {
|
|
97
97
|
return this._url === other._url;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
toDOM(view: EditorView) {
|
|
100
|
+
override toDOM(view: EditorView) {
|
|
101
101
|
const el = document.createElement('span');
|
|
102
102
|
this._onAttach!(el, this._url);
|
|
103
103
|
return el;
|
|
@@ -109,11 +109,11 @@ class LinkText extends WidgetType {
|
|
|
109
109
|
super();
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
override eq(other:
|
|
112
|
+
override eq(other: this) {
|
|
113
113
|
return this._url === other._url;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
toDOM(view: EditorView) {
|
|
116
|
+
override toDOM(view: EditorView) {
|
|
117
117
|
const link = document.createElement('a');
|
|
118
118
|
link.setAttribute('class', 'cm-link');
|
|
119
119
|
link.textContent = this._text;
|
|
@@ -31,7 +31,6 @@ export const markdownTags = {
|
|
|
31
31
|
CodeText: Tag.define(),
|
|
32
32
|
EmphasisMark: Tag.define(),
|
|
33
33
|
HeaderMark: Tag.define(),
|
|
34
|
-
// HorizontalRule: Tag.define(),
|
|
35
34
|
InlineCode: Tag.define(),
|
|
36
35
|
LinkLabel: Tag.define(),
|
|
37
36
|
LinkReference: Tag.define(),
|
|
@@ -177,13 +176,6 @@ export const markdownHighlightStyle = (readonly?: boolean) => {
|
|
|
177
176
|
class: blockquote,
|
|
178
177
|
},
|
|
179
178
|
|
|
180
|
-
// TODO(burdon): Replace with extension.
|
|
181
|
-
// {
|
|
182
|
-
// tag: [markdownTags.HorizontalRule],
|
|
183
|
-
// class: mx(horizontalRule, readonly && '-indent-[100rem]'),
|
|
184
|
-
// },
|
|
185
|
-
|
|
186
|
-
// TODO(burdon): Only if being edited.
|
|
187
179
|
{
|
|
188
180
|
tag: [markdownTags.TableCell],
|
|
189
181
|
class: 'font-mono',
|
package/src/extensions/table.ts
CHANGED
|
@@ -81,7 +81,6 @@ const update = (state: EditorState, options: TableOptions) => {
|
|
|
81
81
|
table.from,
|
|
82
82
|
table.to,
|
|
83
83
|
Decoration.replace({
|
|
84
|
-
block: true,
|
|
85
84
|
widget: new TableWidget(table),
|
|
86
85
|
}),
|
|
87
86
|
);
|
|
@@ -98,14 +97,14 @@ class TableWidget extends WidgetType {
|
|
|
98
97
|
super();
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
override eq(other:
|
|
100
|
+
override eq(other: this) {
|
|
102
101
|
return (
|
|
103
102
|
this._table.header?.join() === other._table.header?.join() &&
|
|
104
103
|
this._table.rows?.join() === other._table.rows?.join()
|
|
105
104
|
);
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
toDOM(view: EditorView) {
|
|
107
|
+
override toDOM(view: EditorView) {
|
|
109
108
|
const table = document.createElement('table');
|
|
110
109
|
|
|
111
110
|
{
|
|
@@ -132,4 +131,8 @@ class TableWidget extends WidgetType {
|
|
|
132
131
|
|
|
133
132
|
return table;
|
|
134
133
|
}
|
|
134
|
+
|
|
135
|
+
override ignoreEvent(e: Event) {
|
|
136
|
+
return !/^mouse/.test(e.type);
|
|
137
|
+
}
|
|
135
138
|
}
|
|
@@ -29,11 +29,11 @@ class CheckboxWidget extends WidgetType {
|
|
|
29
29
|
super();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
override eq(other:
|
|
32
|
+
override eq(other: this) {
|
|
33
33
|
return this._checked === other._checked && this._indent === other._indent;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
toDOM(view: EditorView) {
|
|
36
|
+
override toDOM(view: EditorView) {
|
|
37
37
|
const wrap = document.createElement('span');
|
|
38
38
|
wrap.className = 'cm-task-item';
|
|
39
39
|
wrap.setAttribute('aria-hidden', 'true');
|
|
@@ -6,12 +6,13 @@ import * as Y from 'yjs';
|
|
|
6
6
|
|
|
7
7
|
import { arrayToString, stringToArray } from '@dxos/util';
|
|
8
8
|
|
|
9
|
-
import { type CursorConverter } from '
|
|
9
|
+
import { type CursorConverter } from '../cursor';
|
|
10
10
|
|
|
11
11
|
export const cursorConverter = (text: Y.Text): CursorConverter => ({
|
|
12
12
|
toCursor: (index, assoc) => {
|
|
13
13
|
return arrayToString(Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, index, assoc)));
|
|
14
14
|
},
|
|
15
|
+
|
|
15
16
|
fromCursor: (cursor) => {
|
|
16
17
|
return (
|
|
17
18
|
Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(stringToArray(cursor)), text.doc!)?.index ??
|
|
@@ -8,9 +8,9 @@ import type * as YP from 'y-protocols/awareness';
|
|
|
8
8
|
import type { YText } from '@dxos/text-model';
|
|
9
9
|
|
|
10
10
|
import { cursorConverter } from './cursor';
|
|
11
|
-
import {
|
|
11
|
+
import { Cursor } from '../cursor';
|
|
12
12
|
|
|
13
13
|
export const yjs = (content: YText, awareness?: YP.Awareness) => [
|
|
14
|
-
|
|
14
|
+
Cursor.converter.of(cursorConverter(content)),
|
|
15
15
|
yCollab(content, awareness),
|
|
16
16
|
];
|
|
@@ -30,7 +30,7 @@ export type Range = {
|
|
|
30
30
|
|
|
31
31
|
export type CommentRange = {
|
|
32
32
|
id: string;
|
|
33
|
-
cursor: string;
|
|
33
|
+
cursor: string; // TODO(burdon): Rename range.
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
@@ -41,13 +41,10 @@ export type EditorModel = {
|
|
|
41
41
|
|
|
42
42
|
// TODO(burdon): Remove.
|
|
43
43
|
content: string | YText | YXmlFragment | DocAccessor;
|
|
44
|
-
|
|
45
|
-
* @deprecated Use CursorConverter.
|
|
46
|
-
*/
|
|
47
|
-
getCursorFromRange?: (value: Range) => string;
|
|
48
|
-
getRangeFromCursor?: (cursor: string) => Range | undefined;
|
|
44
|
+
|
|
49
45
|
// TODO(burdon): Move into extension.
|
|
50
46
|
awareness?: YP.Awareness;
|
|
47
|
+
|
|
51
48
|
// TODO(burdon): Remove.
|
|
52
49
|
peer?: {
|
|
53
50
|
id: string;
|
|
@@ -66,7 +63,7 @@ export type UseTextModelProps = {
|
|
|
66
63
|
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
67
64
|
export const useTextModel = (props: UseTextModelProps): EditorModel | undefined => {
|
|
68
65
|
const { identity, space, text } = props;
|
|
69
|
-
const [model, setModel] = useState<EditorModel | undefined>(
|
|
66
|
+
const [model, setModel] = useState<EditorModel | undefined>();
|
|
70
67
|
useEffect(() => setModel(createModel(props)), [identity, space, text]);
|
|
71
68
|
return model;
|
|
72
69
|
};
|
package/src/hooks/yjs.ts
CHANGED
|
@@ -6,7 +6,6 @@ import * as decoding from 'lib0/decoding';
|
|
|
6
6
|
import * as encoding from 'lib0/encoding';
|
|
7
7
|
import { Observable } from 'lib0/observable';
|
|
8
8
|
import * as YP from 'y-protocols/awareness';
|
|
9
|
-
import * as Y from 'yjs';
|
|
10
9
|
import { type Doc } from 'yjs';
|
|
11
10
|
|
|
12
11
|
import { invariant } from '@dxos/invariant';
|
|
@@ -14,9 +13,8 @@ import { log } from '@dxos/log';
|
|
|
14
13
|
import { type Space } from '@dxos/react-client/echo';
|
|
15
14
|
import { type GossipMessage } from '@dxos/react-client/mesh';
|
|
16
15
|
import type { YText } from '@dxos/text-model';
|
|
17
|
-
import { arrayToString, stringToArray } from '@dxos/util';
|
|
18
16
|
|
|
19
|
-
import { type EditorModel, modelState, type
|
|
17
|
+
import { type EditorModel, modelState, type UseTextModelProps } from './useTextModel';
|
|
20
18
|
import { yjs } from '../extensions';
|
|
21
19
|
|
|
22
20
|
export const createYjsModel = ({ identity, space, text }: UseTextModelProps): EditorModel => {
|
|
@@ -29,26 +27,6 @@ export const createYjsModel = ({ identity, space, text }: UseTextModelProps): Ed
|
|
|
29
27
|
id: text.doc.guid,
|
|
30
28
|
content: text.content,
|
|
31
29
|
text: () => text.content!.toString(),
|
|
32
|
-
// https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
33
|
-
// TODO(dmaretskyi): Refactor as cursor-converter.
|
|
34
|
-
getCursorFromRange: (range: Range) => {
|
|
35
|
-
const from = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.from));
|
|
36
|
-
const to = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.to, -1));
|
|
37
|
-
return [arrayToString(from), arrayToString(to)].join(':');
|
|
38
|
-
},
|
|
39
|
-
getRangeFromCursor: (cursor: string) => {
|
|
40
|
-
invariant(text.doc);
|
|
41
|
-
const parts = cursor.split(':');
|
|
42
|
-
const from = Y.createAbsolutePositionFromRelativePosition(
|
|
43
|
-
Y.decodeRelativePosition(stringToArray(parts[0])),
|
|
44
|
-
text.doc,
|
|
45
|
-
);
|
|
46
|
-
const to = Y.createAbsolutePositionFromRelativePosition(
|
|
47
|
-
Y.decodeRelativePosition(stringToArray(parts[1])),
|
|
48
|
-
text.doc,
|
|
49
|
-
);
|
|
50
|
-
return from && to ? { from: from.index, to: to.index } : undefined;
|
|
51
|
-
},
|
|
52
30
|
extension: [modelState.init(() => model), yjs(text.content as YText, provider?.awareness)],
|
|
53
31
|
awareness: provider?.awareness,
|
|
54
32
|
peer: identity
|
package/src/index.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
|
|
6
6
|
export { Doc, YText, YXmlFragment } from '@dxos/text-model';
|
|
7
7
|
export { type Extension } from '@codemirror/state';
|
|
8
|
+
export { type EditorView } from '@codemirror/view';
|
|
8
9
|
|
|
9
10
|
export * from './components';
|
|
10
11
|
export * from './extensions';
|
|
11
12
|
export * from './hooks';
|
|
12
13
|
export { getToken } from './styles';
|
|
13
14
|
export * from './themes';
|
|
15
|
+
export * from './util';
|
package/src/styles/markdown.ts
CHANGED
|
@@ -39,10 +39,8 @@ export const codeBlock = 'mlb-2 font-mono bg-neutral-500/10 p-3 rounded';
|
|
|
39
39
|
|
|
40
40
|
export const inlineUrl = mx(code, 'px-1');
|
|
41
41
|
|
|
42
|
-
// TODO(burdon): Replace with widget.
|
|
43
42
|
export const blockquote = mx('pl-1 mr-1 border-is-4 border-primary-500/70 dark:border-primary-500/30', light);
|
|
44
43
|
|
|
45
|
-
// TODO(burdon): Replace with widget.
|
|
46
44
|
export const horizontalRule =
|
|
47
45
|
'flex mlb-4 border-b text-neutral-100 dark:text-neutral-900 border-neutral-200 dark:border-neutral-800';
|
|
48
46
|
|
package/src/themes/default.ts
CHANGED
|
@@ -6,8 +6,8 @@ import get from 'lodash.get';
|
|
|
6
6
|
|
|
7
7
|
import { type ThemeStyles, tokens } from '../styles';
|
|
8
8
|
|
|
9
|
-
// TODO(burdon): Can we
|
|
10
|
-
//
|
|
9
|
+
// TODO(burdon): Can we use @apply and import css file?
|
|
10
|
+
// https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply?
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Minimal styles.
|
|
@@ -86,6 +86,8 @@ export const defaultTheme: ThemeStyles = {
|
|
|
86
86
|
//
|
|
87
87
|
'.cm-line': {
|
|
88
88
|
paddingInline: 0,
|
|
89
|
+
// TODO(burdon): Other values leave gaps between lines when highlighted.
|
|
90
|
+
// lineHeight: 'normal',
|
|
89
91
|
},
|
|
90
92
|
'.cm-activeLine': {
|
|
91
93
|
background: 'inherit',
|
|
@@ -119,36 +121,6 @@ export const defaultTheme: ThemeStyles = {
|
|
|
119
121
|
backgroundColor: get(tokens, 'extend.colors.yellow.700'),
|
|
120
122
|
},
|
|
121
123
|
|
|
122
|
-
//
|
|
123
|
-
// collaboration
|
|
124
|
-
// TODO(burdon): Review classnames (YJS dependent?) Note: e2e tests depend on this class.
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
'&light .cm-ySelection, &light .cm-yLineSelection': {
|
|
128
|
-
mixBlendMode: 'multiply',
|
|
129
|
-
},
|
|
130
|
-
'&dark .cm-ySelection, &dark .cm-yLineSelection': {
|
|
131
|
-
mixBlendMode: 'screen',
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
'.cm-ySelectionInfo': {
|
|
135
|
-
padding: '2px 4px',
|
|
136
|
-
marginBlockStart: '-4px',
|
|
137
|
-
},
|
|
138
|
-
'.cm-ySelection, & .cm-selectionMatch': {
|
|
139
|
-
paddingBlockStart: '.15em',
|
|
140
|
-
paddingBlockEnd: '.15em',
|
|
141
|
-
},
|
|
142
|
-
'.cm-ySelectionCaret': {
|
|
143
|
-
display: 'inline-block',
|
|
144
|
-
insetBlockStart: '.1em',
|
|
145
|
-
blockSize: '1.4em',
|
|
146
|
-
verticalAlign: 'top',
|
|
147
|
-
},
|
|
148
|
-
'.cm-yLineSelection': {
|
|
149
|
-
margin: 0,
|
|
150
|
-
},
|
|
151
|
-
|
|
152
124
|
//
|
|
153
125
|
// link
|
|
154
126
|
//
|
|
@@ -229,16 +201,17 @@ export const defaultTheme: ThemeStyles = {
|
|
|
229
201
|
|
|
230
202
|
//
|
|
231
203
|
// font size
|
|
232
|
-
// TODO(thure): This appears to be the best or only way to set selection caret heights,
|
|
204
|
+
// TODO(thure): This appears to be the best or only way to set selection caret heights,
|
|
205
|
+
// but it's far more verbose than it needs to be.
|
|
233
206
|
//
|
|
234
|
-
...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}, {}),
|
|
207
|
+
// ...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
|
|
208
|
+
// const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
|
|
209
|
+
//
|
|
210
|
+
// acc[`& .text-${fontSize} + .cm-ySelectionCaret`] = { height };
|
|
211
|
+
// acc[`& .text-${fontSize} + .cm-ySelection + .cm-ySelectionCaret`] = { height };
|
|
212
|
+
// acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = { height };
|
|
213
|
+
// return acc;
|
|
214
|
+
// }, {}),
|
|
242
215
|
|
|
243
216
|
/**
|
|
244
217
|
* Panels
|
|
@@ -292,7 +265,9 @@ export const textTheme: ThemeStyles = {
|
|
|
292
265
|
};
|
|
293
266
|
|
|
294
267
|
export const markdownTheme: ThemeStyles = {
|
|
295
|
-
|
|
268
|
+
'.cm-scroller': {
|
|
269
|
+
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
270
|
+
},
|
|
296
271
|
'.cm-placeholder': {
|
|
297
272
|
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
298
273
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../../../src/util/cursor.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C;;;;;;;;;GASG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS,GAAG,MAAM,CAAC;IAC/D,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACpC;AAOD,eAAO,MAAM,eAAe,yCAE1B,CAAC"}
|