@dxos/react-ui-editor 0.3.10-main.16895f7 → 0.3.10-main.375af01
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 +728 -732
- 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 +35 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +4 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +9 -4
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +9 -0
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/{basic/bundle.d.ts → basic.d.ts} +1 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.d.ts → experimental.d.ts} +2 -2
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +5 -2
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts +7 -0
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +31 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +5 -0
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/index.d.ts +2 -0
- package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts +17 -0
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
- package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +0 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +4 -4
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +12 -10
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/package.json +20 -32
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +242 -0
- package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
- package/src/components/TextEditor/TextEditor.tsx +59 -45
- package/src/components/TextEditor/extensions/autocomplete.ts +72 -0
- package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +4 -2
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +17 -2
- package/src/components/TextEditor/extensions/index.ts +5 -2
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +68 -31
- package/src/components/TextEditor/extensions/{change.ts → listener.ts} +3 -3
- package/src/components/TextEditor/extensions/markdown/bundle.ts +15 -11
- package/src/components/TextEditor/extensions/markdown/highlight.ts +166 -0
- package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
- package/src/components/TextEditor/extensions/tasklist.ts +113 -0
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.tsx} +9 -9
- package/src/components/TextEditor/index.ts +3 -0
- package/src/components/TextEditor/themes/default.ts +170 -0
- package/src/components/TextEditor/themes/index.ts +5 -0
- package/src/components/TextEditor/yjs.stories.tsx +1 -0
- package/src/{automerge → hooks/automerge}/automerge.stories.tsx +2 -1
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useTextModel.ts +50 -40
- package/src/styles/markdown.ts +26 -24
- package/src/testing/replicator.ts +2 -2
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
- package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
- package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
- package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
- package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
- package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
- package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
- package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
- package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
- package/src/components/TextEditor/extensions/basic/index.ts +0 -6
- package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
- package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
- package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
- package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
- package/src/hooks/useCollaboration.ts +0 -45
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.d.ts +0 -0
- /package/dist/types/src/{automerge → hooks/automerge}/automerge.stories.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { markdownLanguage } from '@codemirror/lang-markdown';
|
|
6
|
+
import { HighlightStyle } from '@codemirror/language';
|
|
7
|
+
import { tags, styleTags, Tag } from '@lezer/highlight';
|
|
8
|
+
import { type MarkdownConfig } from '@lezer/markdown';
|
|
9
|
+
import get from 'lodash.get';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
blockquote,
|
|
13
|
+
bold,
|
|
14
|
+
code,
|
|
15
|
+
codeMark,
|
|
16
|
+
heading,
|
|
17
|
+
horizontalRule,
|
|
18
|
+
inlineUrl,
|
|
19
|
+
italic,
|
|
20
|
+
mark,
|
|
21
|
+
strikethrough,
|
|
22
|
+
tokens,
|
|
23
|
+
} from '../../../../styles';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Custom tags defined and processed by the GFM lezer extension.
|
|
27
|
+
* https://github.com/lezer-parser/markdown
|
|
28
|
+
* https://github.com/lezer-parser/markdown/blob/main/src/markdown.ts
|
|
29
|
+
*/
|
|
30
|
+
export const markdownTags = {
|
|
31
|
+
Blockquote: Tag.define(),
|
|
32
|
+
CodeMark: Tag.define(),
|
|
33
|
+
CodeText: Tag.define(),
|
|
34
|
+
EmphasisMark: Tag.define(),
|
|
35
|
+
HeaderMark: Tag.define(),
|
|
36
|
+
HorizontalRule: Tag.define(),
|
|
37
|
+
InlineCode: Tag.define(),
|
|
38
|
+
LinkLabel: Tag.define(),
|
|
39
|
+
LinkReference: Tag.define(),
|
|
40
|
+
ListMark: Tag.define(),
|
|
41
|
+
QuoteMark: Tag.define(),
|
|
42
|
+
URL: Tag.define(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const markdownTagsExtension: MarkdownConfig = {
|
|
46
|
+
props: [styleTags(markdownTags)],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Styling based on `lezer` parser tags.
|
|
51
|
+
* https://codemirror.net/examples/styling
|
|
52
|
+
* https://github.com/lezer-parser/highlight
|
|
53
|
+
* https://lezer.codemirror.net/docs/ref/#highlight (list of tags)
|
|
54
|
+
*/
|
|
55
|
+
export const markdownHighlightStyle = HighlightStyle.define(
|
|
56
|
+
[
|
|
57
|
+
{
|
|
58
|
+
tag: [
|
|
59
|
+
tags.keyword,
|
|
60
|
+
tags.name,
|
|
61
|
+
tags.deleted,
|
|
62
|
+
tags.character,
|
|
63
|
+
tags.propertyName,
|
|
64
|
+
tags.macroName,
|
|
65
|
+
tags.color,
|
|
66
|
+
tags.constant(tags.name),
|
|
67
|
+
tags.standard(tags.name),
|
|
68
|
+
tags.definition(tags.name),
|
|
69
|
+
tags.separator,
|
|
70
|
+
tags.typeName,
|
|
71
|
+
tags.className,
|
|
72
|
+
tags.number,
|
|
73
|
+
tags.changed,
|
|
74
|
+
tags.annotation,
|
|
75
|
+
tags.modifier,
|
|
76
|
+
tags.self,
|
|
77
|
+
tags.namespace,
|
|
78
|
+
tags.operator,
|
|
79
|
+
tags.operatorKeyword,
|
|
80
|
+
tags.escape,
|
|
81
|
+
tags.regexp,
|
|
82
|
+
tags.special(tags.string),
|
|
83
|
+
tags.meta,
|
|
84
|
+
tags.comment,
|
|
85
|
+
tags.atom,
|
|
86
|
+
tags.bool,
|
|
87
|
+
tags.special(tags.variableName),
|
|
88
|
+
tags.processingInstruction,
|
|
89
|
+
tags.string,
|
|
90
|
+
tags.inserted,
|
|
91
|
+
tags.invalid,
|
|
92
|
+
],
|
|
93
|
+
color: 'inherit !important',
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Markdown marks.
|
|
97
|
+
{
|
|
98
|
+
tag: [
|
|
99
|
+
tags.meta,
|
|
100
|
+
tags.processingInstruction,
|
|
101
|
+
markdownTags.CodeMark,
|
|
102
|
+
markdownTags.EmphasisMark,
|
|
103
|
+
markdownTags.HeaderMark,
|
|
104
|
+
markdownTags.LinkLabel,
|
|
105
|
+
markdownTags.LinkReference,
|
|
106
|
+
markdownTags.ListMark,
|
|
107
|
+
markdownTags.QuoteMark,
|
|
108
|
+
],
|
|
109
|
+
class: mark,
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// Headings.
|
|
113
|
+
{ tag: tags.heading1, class: heading[1] },
|
|
114
|
+
{ tag: tags.heading2, class: heading[2] },
|
|
115
|
+
{ tag: tags.heading3, class: heading[3] },
|
|
116
|
+
{ tag: tags.heading4, class: heading[4] },
|
|
117
|
+
{ tag: tags.heading5, class: heading[5] },
|
|
118
|
+
{ tag: tags.heading6, class: heading[6] },
|
|
119
|
+
|
|
120
|
+
// Emphasis.
|
|
121
|
+
{ tag: tags.emphasis, class: italic },
|
|
122
|
+
{ tag: tags.strong, class: bold },
|
|
123
|
+
{ tag: tags.strikethrough, class: strikethrough },
|
|
124
|
+
|
|
125
|
+
// Naked URLs.
|
|
126
|
+
{
|
|
127
|
+
tag: [markdownTags.URL],
|
|
128
|
+
class: inlineUrl,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
// E.g., code block language (after ```).
|
|
132
|
+
{
|
|
133
|
+
tag: [tags.function(tags.variableName), tags.labelName],
|
|
134
|
+
class: codeMark,
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// The `markdown` extension configures extensions for `lezer` to parse markdown tokens (incl. below).
|
|
138
|
+
// However, since `codeLanguages` is also defined, the `lezer` will not parse fenced code blocks,
|
|
139
|
+
// when a language is specified. In this case, the syntax highlighting extensions will colorize
|
|
140
|
+
// the code, but all other CSS properties will be inherited.
|
|
141
|
+
{
|
|
142
|
+
tag: [markdownTags.CodeText, markdownTags.InlineCode],
|
|
143
|
+
class: code,
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
tag: [markdownTags.QuoteMark],
|
|
148
|
+
class: blockquote,
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
tag: [markdownTags.HorizontalRule],
|
|
153
|
+
class: horizontalRule,
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
// Main content, paragraphs, etc.
|
|
157
|
+
// {
|
|
158
|
+
// tag: [tags.content],
|
|
159
|
+
// fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
160
|
+
// },
|
|
161
|
+
],
|
|
162
|
+
{
|
|
163
|
+
scope: markdownLanguage,
|
|
164
|
+
all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
|
|
165
|
+
},
|
|
166
|
+
);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import {
|
|
7
|
+
type EditorState,
|
|
8
|
+
type Extension,
|
|
9
|
+
type RangeSet,
|
|
10
|
+
RangeSetBuilder,
|
|
11
|
+
StateField,
|
|
12
|
+
type Transaction,
|
|
13
|
+
} from '@codemirror/state';
|
|
14
|
+
import { EditorView, Decoration, WidgetType } from '@codemirror/view';
|
|
15
|
+
|
|
16
|
+
class CheckboxWidget extends WidgetType {
|
|
17
|
+
constructor(private readonly _pos: number, private readonly _getCursor: () => number, private _checked: boolean) {
|
|
18
|
+
super();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override eq(other: CheckboxWidget) {
|
|
22
|
+
// Uniqueness based on position; also checks state.
|
|
23
|
+
// TODO(burdon): Is this efficient? Check life-cycle.
|
|
24
|
+
return this._pos === other._pos && this._checked === other._checked;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
toDOM(view: EditorView) {
|
|
28
|
+
// console.log('toDom', this._pos);
|
|
29
|
+
const wrap = document.createElement('span');
|
|
30
|
+
wrap.setAttribute('aria-hidden', 'true');
|
|
31
|
+
wrap.className = 'cm-task-item';
|
|
32
|
+
|
|
33
|
+
const box = wrap.appendChild(document.createElement('input'));
|
|
34
|
+
box.type = 'checkbox';
|
|
35
|
+
box.checked = this._checked;
|
|
36
|
+
box.onclick = (event) => {
|
|
37
|
+
this._checked = !isChecked(view.state, this._pos);
|
|
38
|
+
view.dispatch({
|
|
39
|
+
changes: {
|
|
40
|
+
from: this._pos + 1,
|
|
41
|
+
to: this._pos + 2,
|
|
42
|
+
insert: this._checked ? 'x' : ' ',
|
|
43
|
+
},
|
|
44
|
+
// TODO(burdon): Restore cursor position? More useful to move to end of line (can indent).
|
|
45
|
+
selection: {
|
|
46
|
+
anchor: this._pos + 4,
|
|
47
|
+
// anchor: this._getCursor(),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return true;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return wrap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// TODO(burdon): Remove listener?
|
|
57
|
+
// override destroy() {
|
|
58
|
+
// console.log('destroy', this._pos);
|
|
59
|
+
// }
|
|
60
|
+
|
|
61
|
+
override ignoreEvent() {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isChecked = (state: EditorState, pos: number) => {
|
|
67
|
+
return state.sliceDoc(pos, pos + 3).toLowerCase() === '[x]';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const statefield = (): Extension => {
|
|
71
|
+
let lastPosition: number = 0;
|
|
72
|
+
const listener = EditorView.updateListener.of((update) => {
|
|
73
|
+
lastPosition = update.startState.selection.main.head;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const checkbox = (state: EditorState) => {
|
|
77
|
+
const builder = new RangeSetBuilder();
|
|
78
|
+
syntaxTree(state).iterate({
|
|
79
|
+
enter: (node) => {
|
|
80
|
+
// console.log(node.name, node.from, node.to);
|
|
81
|
+
if (node.name === 'TaskMarker') {
|
|
82
|
+
builder.add(
|
|
83
|
+
node.from,
|
|
84
|
+
node.to,
|
|
85
|
+
Decoration.replace({
|
|
86
|
+
widget: new CheckboxWidget(node.from, () => lastPosition, isChecked(state, node.from)),
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return builder.finish();
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
listener,
|
|
98
|
+
StateField.define<RangeSet<any>>({
|
|
99
|
+
create: (state) => checkbox(state),
|
|
100
|
+
update: (_: RangeSet<any>, tr: Transaction) => checkbox(tr.state),
|
|
101
|
+
provide: (field) => EditorView.decorations.from(field),
|
|
102
|
+
}),
|
|
103
|
+
];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// TODO(burdon): Reconcile with theme.
|
|
107
|
+
export const styles = EditorView.baseTheme({
|
|
108
|
+
'& .cm-task-item': {
|
|
109
|
+
padding: '0 4px',
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const tasklist = () => [statefield(), styles];
|
|
@@ -2,21 +2,24 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
5
6
|
import { hoverTooltip } from '@codemirror/view';
|
|
6
7
|
|
|
7
8
|
import { tooltipContent } from '@dxos/react-ui-theme';
|
|
8
9
|
|
|
9
10
|
const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)/;
|
|
10
11
|
|
|
11
|
-
export type
|
|
12
|
+
export type TooltipOptions = {
|
|
13
|
+
onHover: (el: Element, url: string) => void;
|
|
14
|
+
regexp?: RegExp;
|
|
15
|
+
};
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* https://codemirror.net/examples/tooltip
|
|
15
19
|
* https://codemirror.net/docs/ref/#view.hoverTooltip
|
|
16
20
|
* https://github.com/codemirror/view/blob/main/src/tooltip.ts
|
|
17
21
|
*/
|
|
18
|
-
|
|
19
|
-
export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markdownLinkRegexp) =>
|
|
22
|
+
export const tooltip = ({ onHover, regexp = markdownLinkRegexp }: TooltipOptions): Extension =>
|
|
20
23
|
hoverTooltip((view, pos) => {
|
|
21
24
|
const { from, text } = view.state.doc.lineAt(pos);
|
|
22
25
|
const p = pos - from;
|
|
@@ -49,13 +52,10 @@ export const createHyperlinkTooltip = (onHover: OnHyperlinkHover, regexp = markd
|
|
|
49
52
|
end: idx + from + match[0].length,
|
|
50
53
|
above: true,
|
|
51
54
|
create: () => {
|
|
52
|
-
const el = document.createElement('
|
|
53
|
-
el.
|
|
54
|
-
el.className = tooltipContent({}, 'mb-2 p-1');
|
|
55
|
-
el.setAttribute('target', '_blank');
|
|
56
|
-
el.setAttribute('href', url);
|
|
55
|
+
const el = document.createElement('div');
|
|
56
|
+
el.className = tooltipContent({}, 'pli-2 plb-1');
|
|
57
57
|
onHover(el, url);
|
|
58
|
-
return { dom: el };
|
|
58
|
+
return { dom: el, offset: { x: 0, y: 12 } };
|
|
59
59
|
},
|
|
60
60
|
};
|
|
61
61
|
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import get from 'lodash.get';
|
|
6
|
+
import { type StyleSpec } from 'style-mod';
|
|
7
|
+
|
|
8
|
+
import { tokens } from '../../../styles';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Minimal styles.
|
|
12
|
+
* https://codemirror.net/examples/styling
|
|
13
|
+
* NOTE: Use one of '&', '&light', and '&dark' prefix to scope instance.
|
|
14
|
+
* NOTE: `light` and `dark` selectors are preprocessed by CodeMirror and can only be in the base theme.
|
|
15
|
+
*/
|
|
16
|
+
export const defaultTheme: {
|
|
17
|
+
[selector: string]: StyleSpec;
|
|
18
|
+
} = {
|
|
19
|
+
//
|
|
20
|
+
// Main layout:
|
|
21
|
+
// <div class=".cm-editor .cm-focused">
|
|
22
|
+
// <div class=".cm-scroller">
|
|
23
|
+
// <div class=".cm-content" role="textbox" contenteditable="true">
|
|
24
|
+
// <div class=".cm-line" />
|
|
25
|
+
// </div>
|
|
26
|
+
// </div>
|
|
27
|
+
// </div>
|
|
28
|
+
//
|
|
29
|
+
'&': {},
|
|
30
|
+
'&.cm-focused': {
|
|
31
|
+
outline: 'none',
|
|
32
|
+
},
|
|
33
|
+
'& .cm-scroller': {
|
|
34
|
+
overflow: 'visible',
|
|
35
|
+
},
|
|
36
|
+
'& .cm-content': {
|
|
37
|
+
padding: 0,
|
|
38
|
+
// Base font size (otherwise defined by HTML tag, which might be different for storybook).
|
|
39
|
+
fontSize: '16px',
|
|
40
|
+
},
|
|
41
|
+
'&light .cm-content': {
|
|
42
|
+
color: get(tokens, 'extend.colors.neutral.900', 'black'),
|
|
43
|
+
caretColor: 'black',
|
|
44
|
+
},
|
|
45
|
+
'&dark .cm-content': {
|
|
46
|
+
color: get(tokens, 'extend.colors.neutral.100', 'white'),
|
|
47
|
+
caretColor: 'white',
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
//
|
|
51
|
+
// Cursor
|
|
52
|
+
//
|
|
53
|
+
'&light .cm-cursor': {
|
|
54
|
+
borderLeft: '2px solid black',
|
|
55
|
+
},
|
|
56
|
+
'&dark .cm-cursor': {
|
|
57
|
+
borderLeft: '2px solid white',
|
|
58
|
+
},
|
|
59
|
+
'& .cm-placeholder': {
|
|
60
|
+
fontWeight: 100,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
//
|
|
64
|
+
// line
|
|
65
|
+
//
|
|
66
|
+
'& .cm-line': {
|
|
67
|
+
paddingInline: 0,
|
|
68
|
+
},
|
|
69
|
+
'& .cm-line *': {},
|
|
70
|
+
'& .cm-activeLine': {
|
|
71
|
+
backgroundColor: 'transparent',
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
//
|
|
75
|
+
// selection
|
|
76
|
+
//
|
|
77
|
+
|
|
78
|
+
'&light .cm-selectionBackground, &light.cm-focused .cm-selectionBackground': {
|
|
79
|
+
background: get(tokens, 'extend.colors.primary.150', '#00ffff'),
|
|
80
|
+
},
|
|
81
|
+
'&dark .cm-selectionBackground, &dark.cm-focused .cm-selectionBackground': {
|
|
82
|
+
background: get(tokens, 'extend.colors.primary.850', '#00ffff'),
|
|
83
|
+
},
|
|
84
|
+
'&light .cm-selectionMatch': {
|
|
85
|
+
background: get(tokens, 'extend.colors.primary.250', '#00ffff') + '44',
|
|
86
|
+
},
|
|
87
|
+
'&dark .cm-selectionMatch': {
|
|
88
|
+
background: get(tokens, 'extend.colors.primary.600', '#00ffff') + '44',
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
//
|
|
92
|
+
// collaboration
|
|
93
|
+
// TODO(burdon): Review classnames (YJS dependent?)
|
|
94
|
+
//
|
|
95
|
+
|
|
96
|
+
'&light .cm-ySelection, &light .cm-yLineSelection': {
|
|
97
|
+
mixBlendMode: 'multiply',
|
|
98
|
+
},
|
|
99
|
+
'&dark .cm-ySelection, &dark .cm-yLineSelection': {
|
|
100
|
+
mixBlendMode: 'screen',
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
'& .cm-ySelectionInfo': {
|
|
104
|
+
padding: '2px 4px',
|
|
105
|
+
marginBlockStart: '-4px',
|
|
106
|
+
},
|
|
107
|
+
'& .cm-ySelection, & .cm-selectionMatch': {
|
|
108
|
+
paddingBlockStart: '.15em',
|
|
109
|
+
paddingBlockEnd: '.15em',
|
|
110
|
+
},
|
|
111
|
+
'& .cm-ySelectionCaret': {
|
|
112
|
+
display: 'inline-block',
|
|
113
|
+
insetBlockStart: '.1em',
|
|
114
|
+
blockSize: '1.4em',
|
|
115
|
+
verticalAlign: 'top',
|
|
116
|
+
},
|
|
117
|
+
'& .cm-yLineSelection': {
|
|
118
|
+
margin: 0,
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
//
|
|
122
|
+
// link
|
|
123
|
+
//
|
|
124
|
+
'& .cm-link': {
|
|
125
|
+
color: get(tokens, 'extend.colors.primary.500'),
|
|
126
|
+
textDecorationLine: 'underline',
|
|
127
|
+
textDecorationThickness: '1px',
|
|
128
|
+
textUnderlineOffset: '2px',
|
|
129
|
+
borderRadius: '.125rem',
|
|
130
|
+
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
//
|
|
134
|
+
// tooltip
|
|
135
|
+
//
|
|
136
|
+
'& .cm-tooltip': {
|
|
137
|
+
border: 'none',
|
|
138
|
+
},
|
|
139
|
+
// '& .cm-tooltip-below': {},
|
|
140
|
+
// '& .cm-tooltip-autocomplete': {},
|
|
141
|
+
|
|
142
|
+
//
|
|
143
|
+
// font size
|
|
144
|
+
// TODO(thure): This appears to be the best or only way to set selection caret heights, but it's far more verbose than it needs to be.
|
|
145
|
+
//
|
|
146
|
+
...Object.keys(get(tokens, 'extend.fontSize', {})).reduce((acc: Record<string, any>, fontSize) => {
|
|
147
|
+
const height = get(tokens, ['extend', 'fontSize', fontSize, 1, 'lineHeight']);
|
|
148
|
+
|
|
149
|
+
acc[`& .text-${fontSize} + .cm-ySelectionCaret`] = { height };
|
|
150
|
+
acc[`& .text-${fontSize} + .cm-ySelection + .cm-ySelectionCaret`] = { height };
|
|
151
|
+
acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = { height };
|
|
152
|
+
return acc;
|
|
153
|
+
}, {}),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const textTheme: {
|
|
157
|
+
[selector: string]: StyleSpec;
|
|
158
|
+
} = {
|
|
159
|
+
'& .cm-scroller': {
|
|
160
|
+
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const codeTheme: {
|
|
165
|
+
[selector: string]: StyleSpec;
|
|
166
|
+
} = {
|
|
167
|
+
'& .cm-scroller': {
|
|
168
|
+
fontFamily: get(tokens, 'fontFamily.mono', []).join(','),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
@@ -12,7 +12,8 @@ import React, { useEffect, useRef, useState } from 'react';
|
|
|
12
12
|
import { type Prop } from '@dxos/automerge/automerge';
|
|
13
13
|
import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
|
|
14
14
|
|
|
15
|
-
import { type IDocHandle
|
|
15
|
+
import { type IDocHandle } from './handle';
|
|
16
|
+
import { automergePlugin } from './plugin';
|
|
16
17
|
|
|
17
18
|
type EditorProps = {
|
|
18
19
|
handle: IDocHandle;
|
package/src/hooks/index.ts
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import get from 'lodash.get';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { yCollab } from 'y-codemirror.next';
|
|
6
9
|
import type * as awarenessProtocol from 'y-protocols/awareness';
|
|
7
10
|
|
|
11
|
+
import { invariant } from '@dxos/invariant';
|
|
8
12
|
import {
|
|
9
13
|
type DocAccessor,
|
|
10
14
|
type Space,
|
|
@@ -16,77 +20,83 @@ import {
|
|
|
16
20
|
import { type Identity } from '@dxos/react-client/halo';
|
|
17
21
|
import type { YText, YXmlFragment } from '@dxos/text-model';
|
|
18
22
|
|
|
23
|
+
import { automergePlugin } from './automerge';
|
|
19
24
|
import { SpaceAwarenessProvider } from './yjs';
|
|
20
25
|
|
|
21
26
|
// TODO(burdon): Move.
|
|
22
27
|
type Awareness = awarenessProtocol.Awareness;
|
|
23
|
-
type Provider = { awareness: Awareness };
|
|
24
28
|
|
|
25
29
|
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
26
30
|
export type EditorModel = {
|
|
27
31
|
id: string;
|
|
28
32
|
content: string | YText | YXmlFragment | DocAccessor;
|
|
29
|
-
|
|
33
|
+
text: () => string;
|
|
34
|
+
extension?: Extension;
|
|
35
|
+
awareness?: Awareness;
|
|
30
36
|
peer?: {
|
|
31
37
|
id: string;
|
|
32
38
|
name?: string;
|
|
33
39
|
};
|
|
34
40
|
};
|
|
35
41
|
|
|
42
|
+
// TODO(burdon): Remove space/identity dependency. Define interface for the framework re content and presence.
|
|
36
43
|
export type UseTextModelOptions = {
|
|
37
44
|
identity?: Identity | null;
|
|
38
45
|
space?: Space;
|
|
39
46
|
text?: TextObject;
|
|
40
47
|
};
|
|
41
48
|
|
|
42
|
-
// TODO(burdon):
|
|
43
|
-
// TODO(burdon): Decouple space (make Editor less dependent on entire stack)?
|
|
49
|
+
// TODO(burdon): Remove YJS/Automerge deps (from UI component -- create abstraction; incl. all ECHO/Space deps).
|
|
44
50
|
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
45
51
|
export const useTextModel = ({ identity, space, text }: UseTextModelOptions): EditorModel | undefined => {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` });
|
|
56
|
-
}, [identity, space, text, text?.doc]);
|
|
52
|
+
const [model, setModel] = useState<EditorModel | undefined>(() => createModel({ identity, space, text }));
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setModel(createModel({ identity, space, text }));
|
|
55
|
+
}, [identity, space, text]);
|
|
56
|
+
return model;
|
|
57
|
+
};
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const obj = text as any as AutomergeTextCompat;
|
|
61
|
-
return getRawDoc(obj, [obj.field]);
|
|
62
|
-
} else {
|
|
63
|
-
return text?.content;
|
|
64
|
-
}
|
|
65
|
-
}, [text]);
|
|
59
|
+
const createModel = (options: UseTextModelOptions) => {
|
|
60
|
+
const { space, text } = options;
|
|
66
61
|
|
|
67
62
|
if (isActualAutomergeObject(text)) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
? {
|
|
75
|
-
id: identity.identityKey.toHex(),
|
|
76
|
-
name: identity.profile?.displayName,
|
|
77
|
-
}
|
|
78
|
-
: undefined,
|
|
79
|
-
};
|
|
63
|
+
return createAutomergeModel(options);
|
|
64
|
+
} else {
|
|
65
|
+
if (!space || !text?.doc || !text?.content) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return createYjsModel(options);
|
|
80
69
|
}
|
|
70
|
+
};
|
|
81
71
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
72
|
+
const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorModel => {
|
|
73
|
+
invariant(space && text?.doc && text?.content);
|
|
74
|
+
const provider = new SpaceAwarenessProvider({ space, doc: text.doc, channel: `yjs.awareness.${text.id}` });
|
|
85
75
|
|
|
86
76
|
return {
|
|
87
77
|
id: text.doc.guid,
|
|
88
78
|
content: text.content,
|
|
89
|
-
|
|
79
|
+
text: () => text.content!.toString(),
|
|
80
|
+
extension: yCollab(text.content as YText, provider.awareness),
|
|
81
|
+
awareness: provider.awareness,
|
|
82
|
+
peer: identity
|
|
83
|
+
? {
|
|
84
|
+
id: identity.identityKey.toHex(),
|
|
85
|
+
name: identity.profile?.displayName,
|
|
86
|
+
}
|
|
87
|
+
: undefined,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const createAutomergeModel = ({ identity, space, text }: UseTextModelOptions): EditorModel => {
|
|
92
|
+
const obj = text as any as AutomergeTextCompat;
|
|
93
|
+
const doc = getRawDoc(obj, [obj.field]);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
id: obj.id,
|
|
97
|
+
content: doc,
|
|
98
|
+
text: () => get(doc.handle.docSync(), doc.path),
|
|
99
|
+
extension: automergePlugin(doc.handle, doc.path),
|
|
90
100
|
peer: identity
|
|
91
101
|
? {
|
|
92
102
|
id: identity.identityKey.toHex(),
|