@dxos/react-ui-editor 0.3.10 → 0.3.11-main.1caa3af
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 +1928 -1717
- 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 +16 -9
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +38 -8
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/hr.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/hr.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +3 -2
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts +3 -0
- package/dist/types/src/components/TextEditor/extensions/mermaid.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/{demo.d.ts → typewriter.d.ts} +2 -3
- package/dist/types/src/components/TextEditor/extensions/typewriter.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/util.d.ts +5 -0
- package/dist/types/src/components/TextEditor/extensions/util.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/themes/default.d.ts +5 -13
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts +2 -3
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/{PatchSemaphore.d.ts → semaphore.d.ts} +1 -1
- package/dist/types/src/hooks/automerge/semaphore.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/update-automerge.d.ts +6 -0
- package/dist/types/src/hooks/automerge/update-automerge.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/update-codemirror.d.ts +5 -0
- package/dist/types/src/hooks/automerge/update-codemirror.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +14 -6
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs/yjs.test.d.ts +2 -0
- package/dist/types/src/hooks/yjs/yjs.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/package.json +24 -20
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +128 -67
- package/src/components/TextEditor/TextEditor.stories.tsx +1 -1
- package/src/components/TextEditor/TextEditor.tsx +26 -22
- package/src/components/TextEditor/extensions/code.ts +12 -12
- package/src/components/TextEditor/extensions/comments.ts +307 -92
- package/src/components/TextEditor/extensions/hr.ts +74 -0
- package/src/components/TextEditor/extensions/image.ts +3 -1
- package/src/components/TextEditor/extensions/index.ts +3 -2
- package/src/components/TextEditor/extensions/markdown/highlight.ts +5 -8
- package/src/components/TextEditor/extensions/mermaid.ts +11 -0
- package/src/components/TextEditor/extensions/table.ts +14 -1
- package/src/components/TextEditor/extensions/{tooltip.tsx → tooltip.ts} +1 -1
- package/src/components/TextEditor/extensions/{demo.ts → typewriter.ts} +1 -2
- package/src/components/TextEditor/extensions/util.ts +18 -0
- package/src/components/TextEditor/themes/default.ts +61 -38
- package/src/hooks/automerge/automerge.stories.tsx +2 -3
- package/src/hooks/automerge/plugin.ts +1 -1
- package/src/hooks/automerge/{PatchSemaphore.ts → semaphore.ts} +4 -3
- package/src/hooks/automerge/{translation/codemirror-to-automerge.ts → update-automerge.ts} +3 -3
- package/src/hooks/automerge/{translation/automerge-to-codemirror.ts → update-codemirror.ts} +2 -7
- package/src/hooks/useTextModel.ts +59 -22
- package/src/hooks/yjs/yjs.test.ts +59 -0
- package/src/index.ts +1 -0
- package/src/testing/replicator.ts +0 -1
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +0 -10
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +0 -24
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +0 -5
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +0 -6
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/translation/index.d.ts +0 -3
- package/dist/types/src/hooks/automerge/translation/index.d.ts.map +0 -1
- package/src/components/TextEditor/extensions/experimental.tsx +0 -114
- package/src/components/TextEditor/extensions/highlight.ts +0 -128
- package/src/hooks/automerge/translation/index.ts +0 -6
|
@@ -9,6 +9,9 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
|
9
9
|
// TODO(burdon): Snippet to create basic table.
|
|
10
10
|
// https://codemirror.net/docs/ref/#autocomplete.snippet
|
|
11
11
|
|
|
12
|
+
// TODO(burdon): Advanced formatting (left/right/center).
|
|
13
|
+
// TODO(burdon): Editor to auto balance columns.
|
|
14
|
+
|
|
12
15
|
export type TableOptions = {};
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -73,7 +76,17 @@ const update = (state: EditorState, options: TableOptions) => {
|
|
|
73
76
|
|
|
74
77
|
tables.forEach((table) => {
|
|
75
78
|
const hide = state.readOnly || cursor < table.from || cursor > table.to;
|
|
76
|
-
hide &&
|
|
79
|
+
hide &&
|
|
80
|
+
builder.add(
|
|
81
|
+
table.from,
|
|
82
|
+
table.to,
|
|
83
|
+
Decoration.replace({
|
|
84
|
+
block: true,
|
|
85
|
+
widget: new TableWidget(table),
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Add class for styling.
|
|
77
90
|
builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
|
|
78
91
|
});
|
|
79
92
|
|
|
@@ -55,7 +55,7 @@ export const tooltip = ({ onHover, regexp = markdownLinkRegexp }: TooltipOptions
|
|
|
55
55
|
const el = document.createElement('div');
|
|
56
56
|
el.className = tooltipContent({}, 'pli-2 plb-1');
|
|
57
57
|
onHover(el, url);
|
|
58
|
-
return { dom: el, offset: { x: 0, y:
|
|
58
|
+
return { dom: el, offset: { x: 0, y: 4 } };
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
61
|
});
|
|
@@ -15,10 +15,9 @@ export type DemoOptions = {
|
|
|
15
15
|
const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https://dxos.org)'];
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Demo script.
|
|
19
18
|
* Configurable plugin that let's user cycle through pre-configured input script.
|
|
20
19
|
*/
|
|
21
|
-
export const
|
|
20
|
+
export const typewriter = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
|
|
22
21
|
let t: any;
|
|
23
22
|
let idx = 0;
|
|
24
23
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { log } from '@dxos/log';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CodeMirror callbacks swallow errors so wrap handlers.
|
|
9
|
+
*/
|
|
10
|
+
// TODO(burdon): Figure out how to systematize this.
|
|
11
|
+
export const callbackWrapper = <T extends Function>(fn: T): T =>
|
|
12
|
+
((...args: any[]) => {
|
|
13
|
+
try {
|
|
14
|
+
return fn(...args);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
log.catch(error);
|
|
17
|
+
}
|
|
18
|
+
}) as unknown as T;
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import get from 'lodash.get';
|
|
6
|
-
import { type StyleSpec } from 'style-mod';
|
|
7
6
|
|
|
8
|
-
import { tokens } from '../../../styles';
|
|
7
|
+
import { type ThemeStyles, tokens } from '../../../styles';
|
|
8
|
+
|
|
9
|
+
// TODO(burdon): Can we erase All of the default CM styles?
|
|
10
|
+
// TODO(burdon): Theme styles seem to be added multiple times?
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Minimal styles.
|
|
@@ -18,19 +20,31 @@ import { tokens } from '../../../styles';
|
|
|
18
20
|
* NOTE: Use one of '&', '&light', and '&dark' prefix to scope instance.
|
|
19
21
|
* NOTE: `light` and `dark` selectors are preprocessed by CodeMirror and can only be in the base theme.
|
|
20
22
|
*/
|
|
21
|
-
export const defaultTheme: {
|
|
22
|
-
[selector: string]: StyleSpec;
|
|
23
|
-
} = {
|
|
23
|
+
export const defaultTheme: ThemeStyles = {
|
|
24
24
|
//
|
|
25
25
|
// Main layout:
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
26
|
+
// https://codemirror.net/examples/styling
|
|
27
|
+
//
|
|
28
|
+
// <div class="cm-editor [cm-focused] [generated classes]">
|
|
29
|
+
// <div class="cm-scroller">
|
|
30
|
+
// <div class="cm-gutters">
|
|
31
|
+
// <div class="cm-gutter [...]">
|
|
32
|
+
// <div class="cm-gutterElement">...</div>
|
|
33
|
+
// </div>
|
|
34
|
+
// </div>
|
|
35
|
+
// <div class="cm-content" role="textbox" contenteditable="true">
|
|
36
|
+
// <div class="cm-line"></div>
|
|
37
|
+
// </div>
|
|
38
|
+
// <div class="cm-selectionLayer">
|
|
39
|
+
// <div class="cm-selectionBackground"></div>
|
|
40
|
+
// </div>
|
|
41
|
+
// <div class="cm-cursorLayer">
|
|
42
|
+
// <div class="cm-cursor"></div>
|
|
30
43
|
// </div>
|
|
31
44
|
// </div>
|
|
32
45
|
// </div>
|
|
33
46
|
//
|
|
47
|
+
|
|
34
48
|
'&': {},
|
|
35
49
|
'&.cm-focused': {
|
|
36
50
|
outline: 'none',
|
|
@@ -39,9 +53,10 @@ export const defaultTheme: {
|
|
|
39
53
|
overflow: 'visible',
|
|
40
54
|
fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
|
|
41
55
|
},
|
|
56
|
+
|
|
42
57
|
'& .cm-content': {
|
|
43
58
|
padding: 0,
|
|
44
|
-
// Base font size (otherwise defined by HTML tag, which might be different for storybook).
|
|
59
|
+
// NOTE: Base font size (otherwise defined by HTML tag, which might be different for storybook).
|
|
45
60
|
fontSize: '16px',
|
|
46
61
|
},
|
|
47
62
|
'&light .cm-content': {
|
|
@@ -72,31 +87,41 @@ export const defaultTheme: {
|
|
|
72
87
|
'& .cm-line': {
|
|
73
88
|
paddingInline: 0,
|
|
74
89
|
},
|
|
75
|
-
'& .cm-line *': {},
|
|
76
90
|
'& .cm-activeLine': {
|
|
77
|
-
|
|
91
|
+
background: 'inherit',
|
|
78
92
|
},
|
|
79
93
|
|
|
80
94
|
//
|
|
81
|
-
//
|
|
95
|
+
// Selection
|
|
82
96
|
//
|
|
83
97
|
|
|
84
|
-
'&light .cm-selectionBackground
|
|
85
|
-
background: get(tokens, 'extend.colors.primary.
|
|
98
|
+
'&light .cm-selectionBackground': {
|
|
99
|
+
background: get(tokens, 'extend.colors.primary.100'),
|
|
86
100
|
},
|
|
87
|
-
'&
|
|
88
|
-
background: get(tokens, 'extend.colors.primary.
|
|
101
|
+
'&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
|
|
102
|
+
background: get(tokens, 'extend.colors.primary.200'),
|
|
89
103
|
},
|
|
90
|
-
'&
|
|
91
|
-
background: get(tokens, 'extend.colors.primary.
|
|
104
|
+
'&dark .cm-selectionBackground': {
|
|
105
|
+
background: get(tokens, 'extend.colors.primary.700'),
|
|
92
106
|
},
|
|
93
|
-
'&dark .cm-
|
|
94
|
-
background: get(tokens, 'extend.colors.primary.600'
|
|
107
|
+
'&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': {
|
|
108
|
+
background: get(tokens, 'extend.colors.primary.600'),
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
//
|
|
112
|
+
// Search
|
|
113
|
+
//
|
|
114
|
+
|
|
115
|
+
'&light .cm-searchMatch': {
|
|
116
|
+
backgroundColor: get(tokens, 'extend.colors.yellow.100'),
|
|
117
|
+
},
|
|
118
|
+
'&dark .cm-searchMatch': {
|
|
119
|
+
backgroundColor: get(tokens, 'extend.colors.yellow.700'),
|
|
95
120
|
},
|
|
96
121
|
|
|
97
122
|
//
|
|
98
123
|
// collaboration
|
|
99
|
-
// TODO(burdon): Review classnames (YJS dependent?)
|
|
124
|
+
// TODO(burdon): Review classnames (YJS dependent?) Note: e2e tests depend on this class.
|
|
100
125
|
//
|
|
101
126
|
|
|
102
127
|
'&light .cm-ySelection, &light .cm-yLineSelection': {
|
|
@@ -211,6 +236,7 @@ export const defaultTheme: {
|
|
|
211
236
|
|
|
212
237
|
/**
|
|
213
238
|
* Panels
|
|
239
|
+
* TODO(burdon): Needs styling attention (esp. dark mode).
|
|
214
240
|
* https://github.com/codemirror/search/blob/main/src/search.ts#L745
|
|
215
241
|
*
|
|
216
242
|
* Find/replace panel.
|
|
@@ -226,7 +252,6 @@ export const defaultTheme: {
|
|
|
226
252
|
*/
|
|
227
253
|
'& .cm-panels': {
|
|
228
254
|
border: `1px solid ${get(tokens, 'extend.colors.neutral.200')}`,
|
|
229
|
-
borderBottom: 'none',
|
|
230
255
|
},
|
|
231
256
|
'& .cm-panel': {
|
|
232
257
|
background: get(tokens, 'extend.colors.neutral.50'),
|
|
@@ -236,20 +261,22 @@ export const defaultTheme: {
|
|
|
236
261
|
margin: '4px',
|
|
237
262
|
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
238
263
|
background: get(tokens, 'extend.colors.neutral.100'),
|
|
264
|
+
backgroundImage: 'none',
|
|
239
265
|
border: 'none',
|
|
266
|
+
'&:hover': {
|
|
267
|
+
background: get(tokens, 'extend.colors.neutral.200'),
|
|
268
|
+
},
|
|
269
|
+
'&:active': {
|
|
270
|
+
background: get(tokens, 'extend.colors.neutral.300'),
|
|
271
|
+
backgroundImage: 'none',
|
|
272
|
+
},
|
|
240
273
|
},
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
backgroundColor: get(tokens, 'extend.colors.yellow.100'),
|
|
244
|
-
},
|
|
245
|
-
'& .cm-searchMatch.cm-searchMatch-selected': {
|
|
246
|
-
backgroundColor: get(tokens, 'extend.colors.primary.100'),
|
|
274
|
+
'& .cm-panel input[type=checkbox]': {
|
|
275
|
+
marginRight: '0.4rem !important',
|
|
247
276
|
},
|
|
248
277
|
};
|
|
249
278
|
|
|
250
|
-
export const textTheme: {
|
|
251
|
-
[selector: string]: StyleSpec;
|
|
252
|
-
} = {
|
|
279
|
+
export const textTheme: ThemeStyles = {
|
|
253
280
|
'& .cm-scroller': {
|
|
254
281
|
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
255
282
|
},
|
|
@@ -258,18 +285,14 @@ export const textTheme: {
|
|
|
258
285
|
},
|
|
259
286
|
};
|
|
260
287
|
|
|
261
|
-
export const markdownTheme: {
|
|
262
|
-
[selector: string]: StyleSpec;
|
|
263
|
-
} = {
|
|
288
|
+
export const markdownTheme: ThemeStyles = {
|
|
264
289
|
// NOTE: Must leave base font family as is (i.e., monospace) due to fenced code blocks.
|
|
265
290
|
'& .cm-placeholder': {
|
|
266
291
|
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
267
292
|
},
|
|
268
293
|
};
|
|
269
294
|
|
|
270
|
-
export const codeTheme: {
|
|
271
|
-
[selector: string]: StyleSpec;
|
|
272
|
-
} = {
|
|
295
|
+
export const codeTheme: ThemeStyles = {
|
|
273
296
|
'& .cm-scroller': {
|
|
274
297
|
fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
|
|
275
298
|
},
|
|
@@ -17,8 +17,8 @@ import { ViewPlugin, type EditorView, type PluginValue, type ViewUpdate } from '
|
|
|
17
17
|
import * as automerge from '@dxos/automerge/automerge';
|
|
18
18
|
import { type Heads, type Prop } from '@dxos/automerge/automerge';
|
|
19
19
|
|
|
20
|
-
import { PatchSemaphore } from './PatchSemaphore';
|
|
21
20
|
import { type IDocHandle } from './handle';
|
|
21
|
+
import { PatchSemaphore } from './semaphore';
|
|
22
22
|
|
|
23
23
|
export type Value = {
|
|
24
24
|
lastHeads: Heads;
|
|
@@ -10,7 +10,8 @@ import { next as automerge } from '@dxos/automerge/automerge';
|
|
|
10
10
|
|
|
11
11
|
import { type IDocHandle } from './handle';
|
|
12
12
|
import { type Field, isReconcileTx, getPath, reconcileAnnotationType, updateHeads, getLastHeads } from './plugin';
|
|
13
|
-
import {
|
|
13
|
+
import { updateAutomerge } from './update-automerge';
|
|
14
|
+
import { updateCodeMirror } from './update-codemirror';
|
|
14
15
|
|
|
15
16
|
type Doc<T> = automerge.Doc<T>;
|
|
16
17
|
type Heads = automerge.Heads;
|
|
@@ -55,7 +56,7 @@ export class PatchSemaphore {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// Apply the unreconciled transactions to the document.
|
|
58
|
-
let newHeads =
|
|
59
|
+
let newHeads = updateAutomerge(this._field, handle, transactions, view.state);
|
|
59
60
|
|
|
60
61
|
// NOTE: null and undefined each come from automerge and repo respectively.
|
|
61
62
|
if (newHeads === null || newHeads === undefined) {
|
|
@@ -65,7 +66,7 @@ export class PatchSemaphore {
|
|
|
65
66
|
|
|
66
67
|
// Now get the diff between the updated state of the document and the heads and apply that to the codemirror doc.
|
|
67
68
|
const diff = automerge.equals(oldHeads, newHeads) ? [] : automerge.diff(handle.docSync()!, oldHeads, newHeads);
|
|
68
|
-
|
|
69
|
+
updateCodeMirror(view, selection, path, diff);
|
|
69
70
|
|
|
70
71
|
view.dispatch({
|
|
71
72
|
effects: updateHeads(newHeads),
|
|
@@ -8,10 +8,10 @@ import { type EditorState, type Text, type Transaction } from '@codemirror/state
|
|
|
8
8
|
|
|
9
9
|
import { next as am, type Heads } from '@dxos/automerge/automerge';
|
|
10
10
|
|
|
11
|
-
import { type IDocHandle } from '
|
|
12
|
-
import { type Field } from '
|
|
11
|
+
import { type IDocHandle } from './handle';
|
|
12
|
+
import { type Field } from './plugin';
|
|
13
13
|
|
|
14
|
-
export const
|
|
14
|
+
export const updateAutomerge = (
|
|
15
15
|
field: Field,
|
|
16
16
|
handle: IDocHandle,
|
|
17
17
|
transactions: Transaction[],
|
|
@@ -16,14 +16,9 @@ import {
|
|
|
16
16
|
type SpliceTextPatch,
|
|
17
17
|
} from '@dxos/automerge/automerge';
|
|
18
18
|
|
|
19
|
-
import { reconcileAnnotationType } from '
|
|
19
|
+
import { reconcileAnnotationType } from './plugin';
|
|
20
20
|
|
|
21
|
-
export const
|
|
22
|
-
view: EditorView,
|
|
23
|
-
selection: EditorSelection,
|
|
24
|
-
target: Prop[],
|
|
25
|
-
patches: Patch[],
|
|
26
|
-
) => {
|
|
21
|
+
export const updateCodeMirror = (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => {
|
|
27
22
|
for (const patch of patches) {
|
|
28
23
|
const changeSpec = handlePatch(patch, target, view.state);
|
|
29
24
|
if (changeSpec != null) {
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Extension } from '@codemirror/state';
|
|
5
|
+
import { type Extension, StateField } from '@codemirror/state';
|
|
6
6
|
import get from 'lodash.get';
|
|
7
7
|
import { useEffect, useState } from 'react';
|
|
8
8
|
import { yCollab } from 'y-codemirror.next';
|
|
9
9
|
import type * as awarenessProtocol from 'y-protocols/awareness';
|
|
10
|
+
import * as Y from 'yjs';
|
|
10
11
|
|
|
11
12
|
import { invariant } from '@dxos/invariant';
|
|
12
13
|
import {
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from '@dxos/react-client/echo';
|
|
20
21
|
import { type Identity } from '@dxos/react-client/halo';
|
|
21
22
|
import type { YText, YXmlFragment } from '@dxos/text-model';
|
|
23
|
+
import { arrayToString, stringToArray } from '@dxos/util';
|
|
22
24
|
|
|
23
25
|
import { automergePlugin } from './automerge';
|
|
24
26
|
import { SpaceAwarenessProvider } from './yjs';
|
|
@@ -26,19 +28,34 @@ import { SpaceAwarenessProvider } from './yjs';
|
|
|
26
28
|
// TODO(burdon): Move.
|
|
27
29
|
type Awareness = awarenessProtocol.Awareness;
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* State field makes the model available to other extensions.
|
|
33
|
+
*/
|
|
34
|
+
// TODO(burdon): Use facet?
|
|
35
|
+
export const modelState = StateField.define<EditorModel | undefined>({
|
|
36
|
+
create: () => undefined,
|
|
37
|
+
update: (model) => model,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type Range = {
|
|
31
41
|
from: number;
|
|
32
42
|
to: number;
|
|
33
43
|
};
|
|
34
44
|
|
|
45
|
+
export type CommentRange = {
|
|
46
|
+
id: string;
|
|
47
|
+
// TODO(burdon): Split into begin/end?
|
|
48
|
+
cursor: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
35
51
|
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
36
52
|
export type EditorModel = {
|
|
37
53
|
id: string;
|
|
38
54
|
// TODO(burdon): Remove.
|
|
39
55
|
content: string | YText | YXmlFragment | DocAccessor;
|
|
40
56
|
text: () => string;
|
|
41
|
-
|
|
57
|
+
getCursorFromRange?: (value: Range) => string;
|
|
58
|
+
getRangeFromCursor?: (cursor: string) => Range;
|
|
42
59
|
extension?: Extension;
|
|
43
60
|
awareness?: Awareness;
|
|
44
61
|
peer?: {
|
|
@@ -48,7 +65,7 @@ export type EditorModel = {
|
|
|
48
65
|
};
|
|
49
66
|
|
|
50
67
|
// TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
|
|
51
|
-
export type
|
|
68
|
+
export type UseTextModelProps = {
|
|
52
69
|
identity?: Identity | null;
|
|
53
70
|
space?: Space;
|
|
54
71
|
text?: TextObject;
|
|
@@ -56,39 +73,55 @@ export type UseTextModelOptions = {
|
|
|
56
73
|
|
|
57
74
|
// TODO(burdon): Remove YJS/Automerge deps (from UI component -- create abstraction; incl. all ECHO/Space deps).
|
|
58
75
|
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
59
|
-
export const useTextModel = (
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}, [identity, space, text]);
|
|
76
|
+
export const useTextModel = (props: UseTextModelProps): EditorModel | undefined => {
|
|
77
|
+
const { identity, space, text } = props;
|
|
78
|
+
const [model, setModel] = useState<EditorModel | undefined>(() => createModel(props));
|
|
79
|
+
useEffect(() => setModel(createModel(props)), [identity, space, text]);
|
|
64
80
|
return model;
|
|
65
81
|
};
|
|
66
82
|
|
|
67
|
-
const createModel = (
|
|
68
|
-
const { text } =
|
|
83
|
+
const createModel = (props: UseTextModelProps) => {
|
|
84
|
+
const { text } = props;
|
|
69
85
|
if (isActualAutomergeObject(text)) {
|
|
70
|
-
return createAutomergeModel(
|
|
86
|
+
return createAutomergeModel(props);
|
|
71
87
|
} else {
|
|
72
88
|
if (!text?.doc) {
|
|
73
89
|
return undefined;
|
|
74
90
|
}
|
|
75
91
|
|
|
76
|
-
return createYjsModel(
|
|
92
|
+
return createYjsModel(props);
|
|
77
93
|
}
|
|
78
94
|
};
|
|
79
95
|
|
|
80
|
-
const createYjsModel = ({ identity, space, text }:
|
|
96
|
+
const createYjsModel = ({ identity, space, text }: UseTextModelProps): EditorModel => {
|
|
81
97
|
invariant(text?.doc && text?.content);
|
|
82
98
|
const provider = space
|
|
83
99
|
? new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` })
|
|
84
100
|
: undefined;
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
const model: EditorModel = {
|
|
87
103
|
id: text.doc.guid,
|
|
88
104
|
content: text.content,
|
|
89
105
|
text: () => text.content!.toString(),
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
// https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
107
|
+
getCursorFromRange: (range: Range) => {
|
|
108
|
+
const from = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.from));
|
|
109
|
+
const to = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text.content as YText, range.to, -1));
|
|
110
|
+
return [arrayToString(from), arrayToString(to)].join(':');
|
|
111
|
+
},
|
|
112
|
+
getRangeFromCursor: (cursor: string) => {
|
|
113
|
+
const parts = cursor.split(':');
|
|
114
|
+
const from = Y.createAbsolutePositionFromRelativePosition(
|
|
115
|
+
Y.decodeRelativePosition(stringToArray(parts[0])),
|
|
116
|
+
text.doc!,
|
|
117
|
+
);
|
|
118
|
+
const to = Y.createAbsolutePositionFromRelativePosition(
|
|
119
|
+
Y.decodeRelativePosition(stringToArray(parts[1])),
|
|
120
|
+
text.doc!,
|
|
121
|
+
);
|
|
122
|
+
return { from: from!.index, to: to!.index };
|
|
123
|
+
},
|
|
124
|
+
extension: [yCollab(text.content as YText, provider?.awareness), modelState.init(() => model)],
|
|
92
125
|
awareness: provider?.awareness,
|
|
93
126
|
peer: identity
|
|
94
127
|
? {
|
|
@@ -97,18 +130,20 @@ const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorM
|
|
|
97
130
|
}
|
|
98
131
|
: undefined,
|
|
99
132
|
};
|
|
133
|
+
|
|
134
|
+
return model;
|
|
100
135
|
};
|
|
101
136
|
|
|
102
|
-
const createAutomergeModel = ({ identity, text }:
|
|
137
|
+
const createAutomergeModel = ({ identity, text }: UseTextModelProps): EditorModel => {
|
|
103
138
|
const obj = text as any as AutomergeTextCompat;
|
|
104
139
|
const doc = getRawDoc(obj, [obj.field]);
|
|
105
140
|
|
|
106
|
-
|
|
141
|
+
const model: EditorModel = {
|
|
107
142
|
id: obj.id,
|
|
108
143
|
content: doc,
|
|
109
144
|
text: () => get(doc.handle.docSync(), doc.path),
|
|
110
|
-
|
|
111
|
-
extension: automergePlugin(doc.handle, doc.path),
|
|
145
|
+
// TODO(burdon): https://automerge.org/automerge/api-docs/js/functions/next.getCursor.html
|
|
146
|
+
extension: [automergePlugin(doc.handle, doc.path), modelState.init(() => model)],
|
|
112
147
|
peer: identity
|
|
113
148
|
? {
|
|
114
149
|
id: identity.identityKey.toHex(),
|
|
@@ -116,4 +151,6 @@ const createAutomergeModel = ({ identity, text }: UseTextModelOptions): EditorMo
|
|
|
116
151
|
}
|
|
117
152
|
: undefined,
|
|
118
153
|
};
|
|
154
|
+
|
|
155
|
+
return model;
|
|
119
156
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { effect } from '@preact/signals-react';
|
|
6
|
+
import { expect } from 'chai';
|
|
7
|
+
import * as Y from 'yjs';
|
|
8
|
+
|
|
9
|
+
import { Trigger } from '@dxos/async';
|
|
10
|
+
import { Expando, setGlobalAutomergePreference, TextObject } from '@dxos/echo-schema';
|
|
11
|
+
import { registerSignalFactory } from '@dxos/echo-signals';
|
|
12
|
+
import { describe, test } from '@dxos/test';
|
|
13
|
+
import { type YText } from '@dxos/text-model';
|
|
14
|
+
|
|
15
|
+
describe('YJS', () => {
|
|
16
|
+
// https://docs.yjs.dev/api/shared-types/y.text
|
|
17
|
+
// https://github.com/yjs/yjs?tab=readme-ov-file#relative-positions
|
|
18
|
+
test('absolute position', () => {
|
|
19
|
+
const obj = new TextObject('hello world');
|
|
20
|
+
const index = obj.text.indexOf('world');
|
|
21
|
+
const relPos = Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(obj.content as YText, index));
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
|
|
25
|
+
expect(cursor!.index).to.equal(index);
|
|
26
|
+
expect(obj.text.substring(cursor!.index)).to.equal('world');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
(obj.content as YText).insert(index, 'cruel ');
|
|
30
|
+
expect(obj.text).to.equal('hello cruel world');
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
const cursor = Y.createAbsolutePositionFromRelativePosition(Y.decodeRelativePosition(relPos), obj.doc!);
|
|
34
|
+
expect(cursor!.index).not.to.equal(index);
|
|
35
|
+
expect(obj.text.substring(cursor!.index)).to.equal('world');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// TODO(burdon): Test deleting end character.
|
|
40
|
+
|
|
41
|
+
test.skip('effect', async () => {
|
|
42
|
+
setGlobalAutomergePreference(true);
|
|
43
|
+
registerSignalFactory();
|
|
44
|
+
const obj = new Expando({ comments: [] });
|
|
45
|
+
|
|
46
|
+
const trigger = new Trigger();
|
|
47
|
+
const unsubscribe = effect(() => {
|
|
48
|
+
// TODO(burdon): BUG: doesn't work with YJS.
|
|
49
|
+
console.log(obj.comments);
|
|
50
|
+
if (obj.comments.length > 0) {
|
|
51
|
+
trigger.wake();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
obj.comments.push({ message: 'hello' });
|
|
56
|
+
await trigger.wait();
|
|
57
|
+
unsubscribe();
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../../../../../src/components/TextEditor/extensions/demo.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAKnD,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,IAAI,sBAA0C,WAAW,KAAQ,SAgD7E,CAAC"}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type Extension } from '@codemirror/state';
|
|
2
|
-
import { type DecorationSet, MatchDecorator, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
|
3
|
-
export declare const styles: Extension;
|
|
4
|
-
export declare function hyperLinkExtension(): ViewPlugin<{
|
|
5
|
-
readonly decorator: MatchDecorator;
|
|
6
|
-
decorations: DecorationSet;
|
|
7
|
-
update(update: ViewUpdate): void;
|
|
8
|
-
}>;
|
|
9
|
-
export declare const hyperlinkWidget: Extension;
|
|
10
|
-
//# sourceMappingURL=experimental.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"experimental.d.ts","sourceRoot":"","sources":["../../../../../../src/components/TextEditor/extensions/experimental.tsx"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAEL,KAAK,aAAa,EAElB,cAAc,EACd,UAAU,EACV,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAC;AAG1B,eAAO,MAAM,MAAM,WAYjB,CAAC;AAiDH,wBAAgB,kBAAkB;wBAGR,cAAc;;mBAOnB,UAAU;GAwB9B;AAED,eAAO,MAAM,eAAe,EAAE,SAA0C,CAAC"}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type Extension, StateField } from '@codemirror/state';
|
|
2
|
-
import { EditorView } from '@codemirror/view';
|
|
3
|
-
import { type DocumentRange } from '../../../hooks';
|
|
4
|
-
type HighlightRange = DocumentRange & {
|
|
5
|
-
active?: boolean;
|
|
6
|
-
};
|
|
7
|
-
/**
|
|
8
|
-
* Update field set.
|
|
9
|
-
*/
|
|
10
|
-
export declare const useHighlights: (view: EditorView | undefined, ranges?: HighlightRange[]) => void;
|
|
11
|
-
/**
|
|
12
|
-
* State field that tracks highlight ranges.
|
|
13
|
-
*/
|
|
14
|
-
export declare const highlightStateField: StateField<HighlightRange[]>;
|
|
15
|
-
/**
|
|
16
|
-
* Decorate ranges.
|
|
17
|
-
*/
|
|
18
|
-
export declare const highlightDecorations: Extension;
|
|
19
|
-
export type HighlightOptions = {
|
|
20
|
-
onChange?: (active?: string) => {};
|
|
21
|
-
};
|
|
22
|
-
export declare const highlight: (options?: HighlightOptions) => Extension;
|
|
23
|
-
export {};
|
|
24
|
-
//# sourceMappingURL=highlight.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"highlight.d.ts","sourceRoot":"","sources":["../../../../../../src/components/TextEditor/extensions/highlight.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAe,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAc,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAI1D,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAsBpD,KAAK,cAAc,GAAG,aAAa,GAAG;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAK3D;;GAEG;AAEH,eAAO,MAAM,aAAa,SAAU,UAAU,GAAG,SAAS,WAAU,cAAc,EAAE,SAMnF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,8BAW9B,CAAC;AAEH;;GAEG;AAEH,eAAO,MAAM,oBAAoB,WAiB/B,CAAC;AAoCH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,EAAE,CAAC;CACpC,CAAC;AAEF,eAAO,MAAM,SAAS,aAAa,gBAAgB,KAAQ,SAE1D,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"PatchSemaphore.d.ts","sourceRoot":"","sources":["../../../../../src/hooks/automerge/PatchSemaphore.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAE9D,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,KAAK,KAAK,EAA8E,MAAM,UAAU,CAAC;AAGlH,KAAK,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC/B,KAAK,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC;AAE7B,KAAK,QAAQ,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,KAAK,GAAG,SAAS,CAAC;AAE3F;;GAEG;AACH,qBAAa,cAAc;IACzB,MAAM,EAAG,KAAK,CAAC;IACf,YAAY,UAAS;IACrB,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAM;gBAEjB,KAAK,CAAC,EAAE,KAAK;IAMzB,SAAS,WAAY,UAAU,QAAQ,UAAU,UA0C/C;CACH"}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { type EditorSelection } from '@codemirror/state';
|
|
2
|
-
import { type EditorView } from '@codemirror/view';
|
|
3
|
-
import { type Patch, type Prop } from '@dxos/automerge/automerge';
|
|
4
|
-
export declare const automergeToCodemirror: (view: EditorView, selection: EditorSelection, target: Prop[], patches: Patch[]) => void;
|
|
5
|
-
//# sourceMappingURL=automerge-to-codemirror.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"automerge-to-codemirror.d.ts","sourceRoot":"","sources":["../../../../../../src/hooks/automerge/translation/automerge-to-codemirror.ts"],"names":[],"mappings":"AAMA,OAAO,EAA8B,KAAK,eAAe,EAAoB,MAAM,mBAAmB,CAAC;AACvG,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAGL,KAAK,KAAK,EACV,KAAK,IAAI,EAGV,MAAM,2BAA2B,CAAC;AAInC,eAAO,MAAM,qBAAqB,SAC1B,UAAU,aACL,eAAe,UAClB,IAAI,EAAE,WACL,KAAK,EAAE,SAkBjB,CAAC"}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { type EditorState, type Transaction } from '@codemirror/state';
|
|
2
|
-
import { type Heads } from '@dxos/automerge/automerge';
|
|
3
|
-
import { type IDocHandle } from '../handle';
|
|
4
|
-
import { type Field } from '../plugin';
|
|
5
|
-
export declare const codemirrorToAutomerge: (field: Field, handle: IDocHandle, transactions: Transaction[], state: EditorState) => Heads | undefined;
|
|
6
|
-
//# sourceMappingURL=codemirror-to-automerge.d.ts.map
|