@dxos/react-ui-editor 0.3.10 → 0.3.11-main.0ac3ef5
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 +1645 -1066
- 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 +21 -13
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +15 -19
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +6 -3
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/{hooks/automerge → components/TextEditor}/automerge.stories.d.ts +6 -2
- package/dist/types/src/components/TextEditor/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/codemirror.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/comments.stories.d.ts +26 -0
- package/dist/types/src/components/TextEditor/comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/index.d.ts +0 -2
- package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
- package/dist/types/src/{hooks/yjs → components/TextEditor}/yjs.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/automerge.d.ts +9 -0
- package/dist/types/src/extensions/automerge/automerge.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/cursor.d.ts +5 -0
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/defs.d.ts +26 -0
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -0
- package/dist/types/src/extensions/automerge/index.d.ts +4 -0
- package/dist/types/src/extensions/automerge/index.d.ts.map +1 -0
- package/dist/types/src/{hooks/automerge/PatchSemaphore.d.ts → extensions/automerge/semaphore.d.ts} +5 -5
- 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 +5 -0
- package/dist/types/src/extensions/automerge/update-codemirror.d.ts.map +1 -0
- package/dist/types/src/extensions/awareness.d.ts +46 -0
- package/dist/types/src/extensions/awareness.d.ts.map +1 -0
- package/dist/types/src/extensions/basic.d.ts.map +1 -0
- package/dist/types/src/extensions/blast.d.ts.map +1 -0
- package/dist/types/src/extensions/code.d.ts.map +1 -0
- package/dist/types/src/extensions/comments.d.ts +50 -0
- package/dist/types/src/extensions/comments.d.ts.map +1 -0
- package/dist/types/src/extensions/cursor.d.ts +25 -0
- package/dist/types/src/extensions/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/hr.d.ts +2 -0
- package/dist/types/src/extensions/hr.d.ts.map +1 -0
- package/dist/types/src/extensions/image.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions → extensions}/index.d.ts +6 -3
- package/dist/types/src/extensions/index.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions → extensions}/link.d.ts +1 -0
- package/dist/types/src/extensions/link.d.ts.map +1 -0
- package/dist/types/src/extensions/listener.d.ts +10 -0
- package/dist/types/src/extensions/listener.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/highlight.d.ts +0 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -0
- package/dist/types/src/extensions/mention.d.ts.map +1 -0
- package/dist/types/src/extensions/table.d.ts.map +1 -0
- package/dist/types/src/extensions/tasklist.d.ts.map +1 -0
- package/dist/types/src/{components/TextEditor/extensions/demo.d.ts → extensions/typewriter.d.ts} +2 -3
- package/dist/types/src/extensions/typewriter.d.ts.map +1 -0
- package/dist/types/src/extensions/yjs/cursor.d.ts +4 -0
- package/dist/types/src/extensions/yjs/cursor.d.ts.map +1 -0
- package/dist/types/src/extensions/yjs/index.d.ts +2 -0
- package/dist/types/src/extensions/yjs/index.d.ts.map +1 -0
- package/dist/types/src/extensions/yjs/yjs.d.ts +4 -0
- package/dist/types/src/extensions/yjs/yjs.d.ts.map +1 -0
- package/dist/types/src/extensions/yjs/yjs.test.d.ts +2 -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 +15 -11
- 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 +6 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/cursors.d.ts.map +1 -0
- package/dist/types/src/styles/index.d.ts +1 -0
- package/dist/types/src/styles/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/tokens.d.ts +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts +2 -2
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor/themes → themes}/default.d.ts +5 -13
- package/dist/types/src/themes/default.d.ts.map +1 -0
- package/dist/types/src/themes/index.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +5 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +26 -20
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +140 -79
- package/src/components/TextEditor/TextEditor.stories.tsx +14 -35
- package/src/components/TextEditor/TextEditor.tsx +87 -79
- package/src/{hooks/automerge → components/TextEditor}/automerge.stories.tsx +67 -8
- package/src/components/TextEditor/codemirror.stories.ts +3 -2
- package/src/components/TextEditor/comments.stories.tsx +315 -0
- package/src/components/TextEditor/index.ts +0 -3
- package/src/{hooks/yjs → components/TextEditor}/yjs.stories.tsx +1 -1
- package/src/{components/TextEditor/extensions → extensions}/autocomplete.ts +18 -17
- package/src/extensions/automerge/automerge.ts +94 -0
- package/src/extensions/automerge/cursor.ts +52 -0
- package/src/extensions/automerge/defs.ts +62 -0
- package/src/extensions/automerge/index.ts +7 -0
- package/src/{hooks/automerge/PatchSemaphore.ts → extensions/automerge/semaphore.ts} +18 -15
- package/src/{hooks/automerge/translation/codemirror-to-automerge.ts → extensions/automerge/update-automerge.ts} +6 -7
- package/src/{hooks/automerge/translation/automerge-to-codemirror.ts → extensions/automerge/update-codemirror.ts} +14 -11
- package/src/extensions/awareness.ts +309 -0
- package/src/{components/TextEditor/extensions → extensions}/code.ts +25 -18
- package/src/extensions/comments.ts +325 -0
- package/src/extensions/cursor.ts +46 -0
- package/src/extensions/hr.ts +73 -0
- package/src/extensions/image.ts +87 -0
- package/src/{components/TextEditor/extensions → extensions}/index.ts +6 -3
- package/src/{components/TextEditor/extensions → extensions}/link.ts +116 -62
- package/src/extensions/listener.ts +35 -0
- package/src/{components/TextEditor/extensions → extensions}/markdown/highlight.ts +4 -16
- package/src/{components/TextEditor/extensions → extensions}/table.ts +23 -4
- package/src/{components/TextEditor/extensions → extensions}/tasklist.ts +3 -5
- package/src/{components/TextEditor/extensions/demo.ts → extensions/typewriter.ts} +2 -3
- package/src/extensions/yjs/cursor.ts +22 -0
- package/src/extensions/yjs/index.ts +5 -0
- package/src/extensions/yjs/yjs.test.ts +35 -0
- 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 +40 -79
- package/src/hooks/{yjs/space-provider.ts → yjs.ts} +49 -19
- package/src/index.ts +6 -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 -16
- package/src/themes/default.ts +275 -0
- package/src/util.ts +18 -0
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +0 -18
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +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/components/TextEditor/extensions/image.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts +0 -9
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +0 -12
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/themes/index.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/PatchSemaphore.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/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/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/src/components/TextEditor/extensions/comments.ts +0 -173
- package/src/components/TextEditor/extensions/experimental.tsx +0 -114
- package/src/components/TextEditor/extensions/highlight.ts +0 -128
- package/src/components/TextEditor/extensions/image.ts +0 -67
- package/src/components/TextEditor/extensions/listener.ts +0 -23
- package/src/components/TextEditor/extensions/tooltip.tsx +0 -61
- package/src/components/TextEditor/themes/default.ts +0 -279
- package/src/hooks/automerge/handle.ts +0 -14
- package/src/hooks/automerge/index.ts +0 -6
- package/src/hooks/automerge/plugin.ts +0 -134
- package/src/hooks/automerge/translation/index.ts +0 -6
- package/src/hooks/yjs/index.ts +0 -6
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/autocomplete.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/basic.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/blast.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/code.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/image.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/bundle.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/markdown/index.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/mention.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/table.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/extensions → extensions}/tasklist.d.ts +0 -0
- /package/dist/types/src/{hooks/yjs → styles}/cursors.d.ts +0 -0
- /package/dist/types/src/{components/TextEditor/themes → themes}/index.d.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/basic.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/blast.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/markdown/bundle.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/markdown/index.ts +0 -0
- /package/src/{components/TextEditor/extensions → extensions}/mention.ts +0 -0
- /package/src/{hooks/yjs → styles}/cursors.ts +0 -0
- /package/src/{components/TextEditor/themes → themes}/index.ts +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import { hoverTooltip, keymap, type Command, Decoration, EditorView, type Rect } from '@codemirror/view';
|
|
7
|
+
import sortBy from 'lodash.sortby';
|
|
8
|
+
|
|
9
|
+
import { debounce } from '@dxos/async';
|
|
10
|
+
import { invariant } from '@dxos/invariant';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { nonNullable } from '@dxos/util';
|
|
13
|
+
|
|
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';
|
|
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
|
+
// TODO(burdon): Reconcile with theme.
|
|
23
|
+
const styles = EditorView.baseTheme({
|
|
24
|
+
'& .cm-comment': {
|
|
25
|
+
backgroundColor: getToken('extend.colors.yellow.50'),
|
|
26
|
+
},
|
|
27
|
+
'& .cm-comment-active': {
|
|
28
|
+
backgroundColor: getToken('extend.colors.yellow.100'),
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const marks = {
|
|
33
|
+
highlight: Decoration.mark({ class: 'cm-comment' }),
|
|
34
|
+
highlightActive: Decoration.mark({ class: 'cm-comment-active' }),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// TODO(burdon): Should this be part of state?
|
|
38
|
+
type CommentSelected = {
|
|
39
|
+
active?: string;
|
|
40
|
+
closest?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// TODO(burdon): Rename.
|
|
44
|
+
type ExtendedCommentRange = Range &
|
|
45
|
+
CommentRange & {
|
|
46
|
+
// TODO(burdon): Not part of state; just required for callback.
|
|
47
|
+
location?: Rect | null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type CommentsState = CommentSelected & {
|
|
51
|
+
ranges: ExtendedCommentRange[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const setFocus = (view: EditorView, thread: string) => {
|
|
55
|
+
const range = view.state.field(commentsStateField).ranges.find((range) => range.id === thread);
|
|
56
|
+
if (!range) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
view.dispatch({
|
|
61
|
+
effects: setSelection.of({ active: thread }),
|
|
62
|
+
selection: { anchor: range.from },
|
|
63
|
+
scrollIntoView: true,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const setCommentRange = StateEffect.define<{ model: EditorModel; comments: CommentRange[] }>();
|
|
68
|
+
|
|
69
|
+
export const setSelection = StateEffect.define<CommentSelected>();
|
|
70
|
+
|
|
71
|
+
const setCommentState = StateEffect.define<CommentsState>();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* State field (reducer) that tracks comment ranges.
|
|
75
|
+
* The ranges are tracked as codemirror ranges (i.e., not relative YJS/Automerge positions), and
|
|
76
|
+
* therefore must be updated when the document changes.
|
|
77
|
+
*/
|
|
78
|
+
const commentsStateField = StateField.define<CommentsState>({
|
|
79
|
+
create: () => ({ ranges: [] }),
|
|
80
|
+
update: (value, tr) => {
|
|
81
|
+
const cursorConverter = tr.state.facet(Cursor.converter);
|
|
82
|
+
|
|
83
|
+
for (const effect of tr.effects) {
|
|
84
|
+
// Update selection.
|
|
85
|
+
if (effect.is(setSelection)) {
|
|
86
|
+
return { ...value, ...effect.value };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Update range from store.
|
|
90
|
+
if (effect.is(setCommentRange)) {
|
|
91
|
+
const { comments } = effect.value;
|
|
92
|
+
const ranges: ExtendedCommentRange[] = comments
|
|
93
|
+
.map((comment) => {
|
|
94
|
+
const range = Cursor.getRangeFromCursor(cursorConverter, comment.cursor);
|
|
95
|
+
return range && { ...comment, ...range };
|
|
96
|
+
})
|
|
97
|
+
.filter(nonNullable);
|
|
98
|
+
|
|
99
|
+
return { ...value, ranges };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Update entire state.
|
|
103
|
+
if (effect.is(setCommentState)) {
|
|
104
|
+
return effect.value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return value;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Decorate ranges.
|
|
114
|
+
*/
|
|
115
|
+
const highlightDecorations = EditorView.decorations.compute([commentsStateField], (state) => {
|
|
116
|
+
const { active, ranges } = state.field(commentsStateField);
|
|
117
|
+
const decorations =
|
|
118
|
+
sortBy(ranges ?? [], (range) => range.from)
|
|
119
|
+
?.flatMap((selection) => {
|
|
120
|
+
// TODO(burdon): Invalid range (e.g., deleted).
|
|
121
|
+
if (selection.from === selection.to) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const range = { from: selection.from, to: selection.to };
|
|
126
|
+
if (selection.id === active) {
|
|
127
|
+
return marks.highlightActive.range(range.from, range.to);
|
|
128
|
+
} else {
|
|
129
|
+
return marks.highlight.range(range.from, range.to);
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
.filter(nonNullable) ?? [];
|
|
133
|
+
|
|
134
|
+
return Decoration.set(decorations);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
export type CommentsOptions = {
|
|
138
|
+
key?: string;
|
|
139
|
+
footnote?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Called to create a new thread and return the thread id.
|
|
142
|
+
*/
|
|
143
|
+
onCreate?: (cursor: string, location?: Rect | null) => string | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* Called to notify which thread is currently closest to the cursor.
|
|
146
|
+
*/
|
|
147
|
+
onSelect?: (state: CommentsState) => void;
|
|
148
|
+
/**
|
|
149
|
+
* Called to render tooltip.
|
|
150
|
+
*/
|
|
151
|
+
onHover?: (el: Element, shortcut: string) => void;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Comment threads.
|
|
156
|
+
* 1). Updates the EditorModel to store relative selections for a set of comments threads.
|
|
157
|
+
* Since the selections are relative, they do not need to be updated when the document is edited.
|
|
158
|
+
* 2). Implements a StateField to track absolute selections corresponding to the comments (i.e., when the document is edited).
|
|
159
|
+
* 3). Creates decoration marks to apply classes to each selection.
|
|
160
|
+
* 4). Tracks the current cursor position to:
|
|
161
|
+
* a). Update the decoration to show if the cursor is within a current selection.
|
|
162
|
+
* b). Calls a handler to indicate which is the closest selection (e.g., to update the thread sidebar).
|
|
163
|
+
* 5). Optionally, inserts a markdown footnote when creating a comment thread.
|
|
164
|
+
* 6). Optionally, implements a hoverTooltip to show hints when creating a selection range.
|
|
165
|
+
*/
|
|
166
|
+
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
167
|
+
const { key: shortcut = "meta-'" } = options;
|
|
168
|
+
|
|
169
|
+
const handleSelect = debounce((state: CommentsState) => options.onSelect?.(state), 200);
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create comment thread action.
|
|
173
|
+
*/
|
|
174
|
+
const createCommentThread: Command = (view) => {
|
|
175
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
176
|
+
|
|
177
|
+
invariant(options.onCreate);
|
|
178
|
+
const { head, from, to } = view.state.selection.main;
|
|
179
|
+
if (from === to) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const cursor = Cursor.getCursorFromRange(cursorConverter, { from, to });
|
|
184
|
+
if (cursor) {
|
|
185
|
+
// Create thread via callback.
|
|
186
|
+
const id = callbackWrapper(options.onCreate)(cursor, view.coordsAtPos(from));
|
|
187
|
+
if (id) {
|
|
188
|
+
// Update range.
|
|
189
|
+
view.dispatch({
|
|
190
|
+
effects: setSelection.of({ active: id }),
|
|
191
|
+
selection: { anchor: from },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Insert footnote.
|
|
195
|
+
if (options.footnote) {
|
|
196
|
+
const tag = `[^${id}]`;
|
|
197
|
+
view.dispatch({
|
|
198
|
+
changes: { from: head, insert: tag },
|
|
199
|
+
selection: { anchor: head + tag.length },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return false;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return [
|
|
211
|
+
commentsStateField,
|
|
212
|
+
highlightDecorations,
|
|
213
|
+
styles,
|
|
214
|
+
|
|
215
|
+
//
|
|
216
|
+
// Keymap.
|
|
217
|
+
//
|
|
218
|
+
options.onCreate
|
|
219
|
+
? keymap.of([
|
|
220
|
+
{
|
|
221
|
+
key: shortcut,
|
|
222
|
+
run: callbackWrapper(createCommentThread),
|
|
223
|
+
},
|
|
224
|
+
])
|
|
225
|
+
: [],
|
|
226
|
+
|
|
227
|
+
//
|
|
228
|
+
// Hover tooltip (for key shortcut hints, etc.)
|
|
229
|
+
// TODO(burdon): Factor out to generic hints extension for current selection/line.
|
|
230
|
+
//
|
|
231
|
+
options.onHover
|
|
232
|
+
? hoverTooltip(
|
|
233
|
+
(view, pos) => {
|
|
234
|
+
const selection = view.state.selection.main;
|
|
235
|
+
if (selection && pos >= selection.from && pos <= selection.to) {
|
|
236
|
+
return {
|
|
237
|
+
pos: selection.from,
|
|
238
|
+
end: selection.to,
|
|
239
|
+
above: true,
|
|
240
|
+
create: () => {
|
|
241
|
+
// TODO(burdon): Dispatch to react callback to render (or use SSR)?
|
|
242
|
+
const el = document.createElement('div');
|
|
243
|
+
options.onHover?.(el, shortcut);
|
|
244
|
+
return { dom: el, offset: { x: 0, y: 8 } };
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return null;
|
|
250
|
+
},
|
|
251
|
+
{ hideOnChange: true, hoverTime: 1000 },
|
|
252
|
+
)
|
|
253
|
+
: [],
|
|
254
|
+
|
|
255
|
+
//
|
|
256
|
+
// Monitor cursor movement and text updates.
|
|
257
|
+
// TODO(burdon): Is there a better (finer grained) way to do this?
|
|
258
|
+
//
|
|
259
|
+
EditorView.updateListener.of(({ view, state, changes }) => {
|
|
260
|
+
const cursorConverter = view.state.facet(Cursor.converter);
|
|
261
|
+
|
|
262
|
+
//
|
|
263
|
+
// Test if need to recompute indexed range if document changes before the end of the range.
|
|
264
|
+
//
|
|
265
|
+
{
|
|
266
|
+
let mod = false;
|
|
267
|
+
const { active, ranges } = state.field(commentsStateField);
|
|
268
|
+
changes.iterChanges((from, to, from2, to2) => {
|
|
269
|
+
ranges.forEach((range) => {
|
|
270
|
+
if (from2 === to2) {
|
|
271
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
272
|
+
if (!newRange || newRange.to - newRange.from === 0) {
|
|
273
|
+
// TODO(burdon): Delete range if empty.
|
|
274
|
+
log.info('deleted comment', { thread: range.id });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (from <= range.to) {
|
|
279
|
+
const newRange = Cursor.getRangeFromCursor(cursorConverter, range.cursor);
|
|
280
|
+
Object.assign(range, newRange);
|
|
281
|
+
mod = true;
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
if (mod) {
|
|
287
|
+
view.dispatch({ effects: setCommentState.of({ active, ranges }) });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
//
|
|
292
|
+
// Track the current selection.
|
|
293
|
+
//
|
|
294
|
+
{
|
|
295
|
+
const { head } = state.selection.main;
|
|
296
|
+
|
|
297
|
+
let min = Infinity;
|
|
298
|
+
const { active, closest, ranges } = state.field(commentsStateField);
|
|
299
|
+
const selected: CommentSelected = { active: undefined, closest: undefined };
|
|
300
|
+
ranges.forEach((comment) => {
|
|
301
|
+
const d = Math.min(Math.abs(head - comment.from), Math.abs(head - comment.to));
|
|
302
|
+
if (head >= comment.from && head <= comment.to) {
|
|
303
|
+
selected.active = comment.id;
|
|
304
|
+
}
|
|
305
|
+
if (d < min) {
|
|
306
|
+
selected.closest = comment.id;
|
|
307
|
+
min = d;
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// TODO(burdon): Fire debounced callback if range selected (to show hint).
|
|
312
|
+
|
|
313
|
+
if (selected.active !== active || selected.closest !== closest) {
|
|
314
|
+
view.dispatch({ effects: setSelection.of(selected) });
|
|
315
|
+
|
|
316
|
+
// Update callback.
|
|
317
|
+
handleSelect({
|
|
318
|
+
...selected,
|
|
319
|
+
ranges: ranges.map((range) => ({ ...range, location: view.coordsAtPos(range.from) })),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}),
|
|
324
|
+
];
|
|
325
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Decoration,
|
|
7
|
+
type DecorationSet,
|
|
8
|
+
EditorView,
|
|
9
|
+
MatchDecorator,
|
|
10
|
+
ViewPlugin,
|
|
11
|
+
type ViewUpdate,
|
|
12
|
+
WidgetType,
|
|
13
|
+
} from '@codemirror/view';
|
|
14
|
+
|
|
15
|
+
import { getToken } from '../styles';
|
|
16
|
+
|
|
17
|
+
// TODO(burdon): Reconcile with theme.
|
|
18
|
+
const styles = EditorView.baseTheme({
|
|
19
|
+
'& .cm-hr': {
|
|
20
|
+
// TODO(burdon): ???
|
|
21
|
+
// Note that block-level decorations should not have vertical margins,
|
|
22
|
+
borderBottom: `1px solid ${getToken('extend.colors.neutral.200')}`,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
class HorizontalRuleWidget extends WidgetType {
|
|
27
|
+
constructor(readonly _pos: number) {
|
|
28
|
+
super();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
override eq(other: this) {
|
|
32
|
+
return this._pos === (other as any as HorizontalRuleWidget)._pos;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override toDOM(view: EditorView) {
|
|
36
|
+
// TODO(burdon): Create <hr> element? Does this clash with markdown parser?
|
|
37
|
+
const el = document.createElement('div');
|
|
38
|
+
el.className = 'cm-hr';
|
|
39
|
+
return el;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const placeholderMatcher = new MatchDecorator({
|
|
44
|
+
regexp: /^---$/g,
|
|
45
|
+
decoration: (match, view, pos) =>
|
|
46
|
+
Decoration.replace({
|
|
47
|
+
widget: new HorizontalRuleWidget(pos),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const hr = () => [
|
|
52
|
+
styles,
|
|
53
|
+
ViewPlugin.fromClass(
|
|
54
|
+
class {
|
|
55
|
+
rules: DecorationSet;
|
|
56
|
+
constructor(view: EditorView) {
|
|
57
|
+
this.rules = placeholderMatcher.createDeco(view);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
update(update: ViewUpdate) {
|
|
61
|
+
this.rules = placeholderMatcher.updateDeco(update, this.rules);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
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
|
+
},
|
|
72
|
+
),
|
|
73
|
+
];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type EditorState, type Extension, StateField, type Transaction, type Range } from '@codemirror/state';
|
|
7
|
+
import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemirror/view';
|
|
8
|
+
|
|
9
|
+
export type ImageOptions = {};
|
|
10
|
+
|
|
11
|
+
export const image = (options: ImageOptions = {}): Extension => {
|
|
12
|
+
return StateField.define<DecorationSet>({
|
|
13
|
+
create: (state) => {
|
|
14
|
+
return Decoration.set(buildDecorations(0, state.doc.length, state));
|
|
15
|
+
},
|
|
16
|
+
update: (value: DecorationSet, tr: Transaction) => {
|
|
17
|
+
if (!tr.docChanged && !tr.selection) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
// Find range of changes and cursor changes.
|
|
21
|
+
const cursor = tr.state.selection.main.head;
|
|
22
|
+
const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
|
|
23
|
+
let from = Math.min(cursor, oldCursor);
|
|
24
|
+
let to = Math.max(cursor, oldCursor);
|
|
25
|
+
tr.changes.iterChangedRanges((fromA, toA, fromB, toB) => {
|
|
26
|
+
from = Math.min(from, fromB);
|
|
27
|
+
to = Math.max(to, toB);
|
|
28
|
+
});
|
|
29
|
+
// Expand to cover lines
|
|
30
|
+
from = tr.state.doc.lineAt(from).from;
|
|
31
|
+
to = tr.state.doc.lineAt(to).to;
|
|
32
|
+
return value.map(tr.changes).update({
|
|
33
|
+
filterFrom: from,
|
|
34
|
+
filterTo: to,
|
|
35
|
+
filter: () => false,
|
|
36
|
+
add: buildDecorations(from, to, tr.state),
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const buildDecorations = (from: number, to: number, state: EditorState) => {
|
|
44
|
+
const decorations: Range<Decoration>[] = [];
|
|
45
|
+
const cursor = state.selection.main.head;
|
|
46
|
+
|
|
47
|
+
syntaxTree(state).iterate({
|
|
48
|
+
enter: (node) => {
|
|
49
|
+
if (node.name === 'Image') {
|
|
50
|
+
const urlNode = node.node.getChild('URL');
|
|
51
|
+
if (urlNode) {
|
|
52
|
+
const hide = state.readOnly || cursor < node.from || cursor > node.to;
|
|
53
|
+
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
54
|
+
decorations.push(
|
|
55
|
+
Decoration.replace({
|
|
56
|
+
block: true, // Prevent cursor from entering.
|
|
57
|
+
widget: new ImageWidget(url),
|
|
58
|
+
}).range(hide ? node.from : node.to, node.to),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
from,
|
|
64
|
+
to,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return decorations;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
class ImageWidget extends WidgetType {
|
|
71
|
+
constructor(readonly _url: string) {
|
|
72
|
+
super();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override eq(other: this) {
|
|
76
|
+
return this._url === (other as any as ImageWidget)._url;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override toDOM(view: EditorView) {
|
|
80
|
+
const img = document.createElement('img');
|
|
81
|
+
img.setAttribute('src', this._url);
|
|
82
|
+
img.setAttribute('class', 'cm-image');
|
|
83
|
+
// Images are hidden until successfully loaded to avoid flickering effects.
|
|
84
|
+
img.onload = () => img.classList.add('cm-loaded-image');
|
|
85
|
+
return img;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -3,12 +3,14 @@
|
|
|
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
|
-
export * from './demo';
|
|
10
11
|
export * from './comments';
|
|
11
|
-
export * from './
|
|
12
|
+
export * from './cursor';
|
|
13
|
+
export * from './hr';
|
|
12
14
|
export * from './image';
|
|
13
15
|
export * from './link';
|
|
14
16
|
export * from './listener';
|
|
@@ -16,4 +18,5 @@ export * from './markdown';
|
|
|
16
18
|
export * from './mention';
|
|
17
19
|
export * from './table';
|
|
18
20
|
export * from './tasklist';
|
|
19
|
-
export * from './
|
|
21
|
+
export * from './typewriter';
|
|
22
|
+
export * from './yjs';
|