@dxos/react-ui-editor 0.3.10-main.c5193cc → 0.3.10-main.c9b5e9b
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 +615 -380
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +6 -3
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/code.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +24 -0
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +8 -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 +3 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +6 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts +1 -1
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/package.json +22 -20
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +48 -32
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -1
- package/src/components/TextEditor/TextEditor.tsx +18 -1
- package/src/components/TextEditor/extensions/basic.ts +4 -3
- package/src/components/TextEditor/extensions/blast.ts +14 -28
- package/src/components/TextEditor/extensions/code.ts +99 -0
- package/src/components/TextEditor/extensions/comments.ts +22 -40
- package/src/components/TextEditor/extensions/experimental.tsx +15 -36
- package/src/components/TextEditor/extensions/highlight.ts +128 -0
- package/src/components/TextEditor/extensions/index.ts +2 -0
- package/src/components/TextEditor/extensions/link.ts +1 -2
- package/src/components/TextEditor/extensions/markdown/bundle.ts +19 -19
- package/src/components/TextEditor/extensions/markdown/highlight.ts +37 -11
- package/src/components/TextEditor/extensions/table.ts +44 -48
- package/src/components/TextEditor/extensions/tasklist.ts +9 -9
- package/src/components/TextEditor/themes/default.ts +43 -2
- package/src/hooks/automerge/automerge.stories.tsx +1 -0
- package/src/hooks/useTextModel.ts +9 -0
- package/src/hooks/yjs/yjs.stories.tsx +1 -1
- package/src/styles/markdown.ts +5 -4
- package/src/testing/replicator.ts +1 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import { Decoration, EditorView } from '@codemirror/view';
|
|
7
|
+
import sortBy from 'lodash.sortby';
|
|
8
|
+
import { useEffect } from 'react';
|
|
9
|
+
|
|
10
|
+
import { type DocumentRange } from '../../../hooks';
|
|
11
|
+
|
|
12
|
+
// TODO(burdon): Detect click, or cursor proximity.
|
|
13
|
+
|
|
14
|
+
// TODO(burdon): Reconcile with theme.
|
|
15
|
+
const styles = EditorView.baseTheme({
|
|
16
|
+
'& .cm-highlight': {
|
|
17
|
+
backgroundColor: 'yellow', // TODO(burdon): Reconcile with tailwind theme.
|
|
18
|
+
},
|
|
19
|
+
'& .cm-highlight::before': {
|
|
20
|
+
content: ']',
|
|
21
|
+
},
|
|
22
|
+
'& .cm-highlight-active': {
|
|
23
|
+
backgroundColor: 'lime',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const marks = {
|
|
28
|
+
highlight: Decoration.mark({ class: 'cm-highlight' }),
|
|
29
|
+
HighlightActive: Decoration.mark({ class: 'cm-highlight-active' }),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type HighlightRange = DocumentRange & { active?: boolean };
|
|
33
|
+
|
|
34
|
+
// TODO(burdon): Make configurable to support multiple layers.
|
|
35
|
+
const setHighlights = StateEffect.define<HighlightRange[]>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Update field set.
|
|
39
|
+
*/
|
|
40
|
+
// TODO(burdon): Reconcile range from data model, vs. in-markdown characters.
|
|
41
|
+
export const useHighlights = (view: EditorView | undefined, ranges: HighlightRange[] = []) => {
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
view?.dispatch({
|
|
44
|
+
effects: setHighlights.of(ranges),
|
|
45
|
+
});
|
|
46
|
+
}, [view, ranges]);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* State field that tracks highlight ranges.
|
|
51
|
+
*/
|
|
52
|
+
export const highlightStateField = StateField.define<HighlightRange[]>({
|
|
53
|
+
create: () => [],
|
|
54
|
+
update: (threads, tr) => {
|
|
55
|
+
for (const effect of tr.effects) {
|
|
56
|
+
if (effect.is(setHighlights)) {
|
|
57
|
+
return effect.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return threads;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decorate ranges.
|
|
67
|
+
*/
|
|
68
|
+
// TODO(burdon): Pass in field to make reusable (e.g., comments, search).
|
|
69
|
+
export const highlightDecorations = EditorView.decorations.compute([highlightStateField], (state) => {
|
|
70
|
+
const selectionRanges = state.field(highlightStateField);
|
|
71
|
+
const decorations =
|
|
72
|
+
sortBy(selectionRanges ?? [], (selection) => selection.from)?.flatMap((selection) => {
|
|
73
|
+
const range = { from: selection.from, to: selection.to + 1 };
|
|
74
|
+
if (selection.to > selection.from) {
|
|
75
|
+
if (selection.active) {
|
|
76
|
+
return marks.HighlightActive.range(range.from, range.to);
|
|
77
|
+
} else {
|
|
78
|
+
return marks.highlight.range(range.from, range.to);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}) ?? [];
|
|
84
|
+
|
|
85
|
+
return Decoration.set(decorations);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// TODO(burdon): Use current range to create comment.
|
|
89
|
+
// TODO(burdon): Allow/prevent overlapping?
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Track cursor entering and existing ranges.
|
|
93
|
+
*/
|
|
94
|
+
const listener = (options: HighlightOptions) => {
|
|
95
|
+
return EditorView.updateListener.of((update) => {
|
|
96
|
+
const { view, state } = update;
|
|
97
|
+
const { /* from, to, */ head } = state.selection.main;
|
|
98
|
+
const ranges = state.field(highlightStateField);
|
|
99
|
+
|
|
100
|
+
// TODO(burdon): Modify range if editing.
|
|
101
|
+
// console.log('::::', update);
|
|
102
|
+
|
|
103
|
+
// Set active.
|
|
104
|
+
let mod = false;
|
|
105
|
+
const newRanges = ranges.map((range) => {
|
|
106
|
+
const active = head >= range.from && head <= range.to;
|
|
107
|
+
if (active !== range.active) {
|
|
108
|
+
mod = true;
|
|
109
|
+
return { ...range, active };
|
|
110
|
+
} else {
|
|
111
|
+
return range;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (mod) {
|
|
116
|
+
view.dispatch({ effects: setHighlights.of(newRanges) });
|
|
117
|
+
options?.onChange?.(newRanges.find((range) => range.active)?.id);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type HighlightOptions = {
|
|
123
|
+
onChange?: (active?: string) => {};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const highlight = (options: HighlightOptions = {}): Extension => {
|
|
127
|
+
return [highlightStateField, highlightDecorations, listener(options), styles];
|
|
128
|
+
};
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
export * from './autocomplete';
|
|
6
6
|
export * from './basic';
|
|
7
7
|
export * from './blast';
|
|
8
|
+
export * from './code';
|
|
8
9
|
export * from './demo';
|
|
9
10
|
export * from './comments';
|
|
11
|
+
export * from './highlight';
|
|
10
12
|
export * from './image';
|
|
11
13
|
export * from './link';
|
|
12
14
|
export * from './listener';
|
|
@@ -101,14 +101,13 @@ const update = (state: EditorState, options: LinkOptions) => {
|
|
|
101
101
|
if (node.name === 'Link') {
|
|
102
102
|
const marks = node.node.getChildren('LinkMark');
|
|
103
103
|
const text = marks.length >= 2 ? state.sliceDoc(marks[0].to, marks[1].from) : '';
|
|
104
|
-
|
|
105
104
|
const urlNode = node.node.getChild('URL');
|
|
106
105
|
const url = urlNode ? state.sliceDoc(urlNode.from, urlNode.to) : '';
|
|
107
106
|
if (!url) {
|
|
108
107
|
return false;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
if (cursor < node.from || cursor > node.to) {
|
|
110
|
+
if (state.readOnly || cursor < node.from || cursor > node.to) {
|
|
112
111
|
builder.add(
|
|
113
112
|
node.from,
|
|
114
113
|
node.to,
|
|
@@ -7,7 +7,7 @@ import { history, historyKeymap, indentWithTab, standardKeymap } from '@codemirr
|
|
|
7
7
|
import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
|
|
8
8
|
import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
|
|
9
9
|
import { languages } from '@codemirror/language-data';
|
|
10
|
-
import {
|
|
10
|
+
import { searchKeymap } from '@codemirror/search';
|
|
11
11
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
12
12
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
13
13
|
import {
|
|
@@ -16,16 +16,13 @@ import {
|
|
|
16
16
|
dropCursor,
|
|
17
17
|
EditorView,
|
|
18
18
|
highlightActiveLine,
|
|
19
|
-
highlightActiveLineGutter,
|
|
20
|
-
highlightSpecialChars,
|
|
21
19
|
keymap,
|
|
22
20
|
placeholder,
|
|
23
|
-
rectangularSelection,
|
|
24
21
|
} from '@codemirror/view';
|
|
25
22
|
|
|
26
23
|
import type { ThemeMode } from '@dxos/react-ui';
|
|
27
24
|
|
|
28
|
-
import { markdownHighlightStyle,
|
|
25
|
+
import { markdownHighlightStyle, markdownTagsExtensions } from './highlight';
|
|
29
26
|
|
|
30
27
|
export type MarkdownBundleOptions = {
|
|
31
28
|
readonly?: boolean;
|
|
@@ -33,33 +30,36 @@ export type MarkdownBundleOptions = {
|
|
|
33
30
|
placeholder?: string;
|
|
34
31
|
};
|
|
35
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Markdown bundle.
|
|
35
|
+
* Refs:
|
|
36
|
+
* https://github.com/codemirror/basic-setup
|
|
37
|
+
* https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts#L50
|
|
38
|
+
* https://codemirror.net/docs/community
|
|
39
|
+
* https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
40
|
+
*/
|
|
41
|
+
// TODO(burdon): Add Composer here: https://codemirror.net/docs/community
|
|
36
42
|
export const markdownBundle = ({
|
|
37
43
|
readonly,
|
|
38
44
|
themeMode,
|
|
39
45
|
placeholder: _placeholder,
|
|
40
46
|
}: MarkdownBundleOptions): Extension[] => {
|
|
41
|
-
// All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
|
|
42
|
-
// https://codemirror.net/docs/ref/#codemirror.basicSetup
|
|
43
47
|
return [
|
|
44
|
-
bracketMatching(),
|
|
45
|
-
closeBrackets(),
|
|
46
|
-
_placeholder && placeholder(_placeholder),
|
|
47
|
-
|
|
48
48
|
EditorState.allowMultipleSelections.of(true),
|
|
49
|
+
EditorState.tabSize.of(2),
|
|
49
50
|
EditorView.lineWrapping,
|
|
50
51
|
|
|
52
|
+
customKeymap(),
|
|
53
|
+
|
|
54
|
+
bracketMatching(),
|
|
55
|
+
closeBrackets(),
|
|
51
56
|
crosshairCursor(),
|
|
52
57
|
dropCursor(),
|
|
53
58
|
drawSelection(),
|
|
54
59
|
highlightActiveLine(),
|
|
55
|
-
highlightActiveLineGutter(),
|
|
56
|
-
highlightSelectionMatches(),
|
|
57
|
-
highlightSpecialChars(),
|
|
58
60
|
history(),
|
|
59
61
|
indentOnInput(),
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
customkeymap(),
|
|
62
|
+
_placeholder && placeholder(_placeholder),
|
|
63
63
|
|
|
64
64
|
// Main extension.
|
|
65
65
|
// https://github.com/codemirror/lang-markdown
|
|
@@ -78,7 +78,7 @@ export const markdownBundle = ({
|
|
|
78
78
|
// Parser extensions.
|
|
79
79
|
extensions: [
|
|
80
80
|
// GFM provided by default.
|
|
81
|
-
|
|
81
|
+
markdownTagsExtensions,
|
|
82
82
|
],
|
|
83
83
|
}),
|
|
84
84
|
|
|
@@ -91,7 +91,7 @@ export const markdownBundle = ({
|
|
|
91
91
|
};
|
|
92
92
|
|
|
93
93
|
// https://codemirror.net/docs/ref/#view.keymap
|
|
94
|
-
const
|
|
94
|
+
const customKeymap = () =>
|
|
95
95
|
keymap.of([
|
|
96
96
|
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
97
97
|
indentWithTab,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { markdownLanguage } from '@codemirror/lang-markdown';
|
|
6
6
|
import { HighlightStyle } from '@codemirror/language';
|
|
7
7
|
import { tags, styleTags, Tag } from '@lezer/highlight';
|
|
8
|
-
import { type MarkdownConfig } from '@lezer/markdown';
|
|
8
|
+
import { type MarkdownConfig, Table } from '@lezer/markdown';
|
|
9
9
|
import get from 'lodash.get';
|
|
10
10
|
|
|
11
11
|
import { mx } from '@dxos/react-ui-theme';
|
|
@@ -24,11 +24,6 @@ import {
|
|
|
24
24
|
tokens,
|
|
25
25
|
} from '../../../../styles';
|
|
26
26
|
|
|
27
|
-
// TODO(burdon): Style table in monospace.
|
|
28
|
-
// https://discuss.codemirror.net/t/markdown-table-highlighting/658
|
|
29
|
-
|
|
30
|
-
// TODO(burdon): Readonly: use remark rather than hiding markup chars (which cursor can still move into).
|
|
31
|
-
|
|
32
27
|
/**
|
|
33
28
|
* Custom tags defined and processed by the GFM lezer extension.
|
|
34
29
|
* https://github.com/lezer-parser/markdown
|
|
@@ -47,24 +42,42 @@ export const markdownTags = {
|
|
|
47
42
|
ListMark: Tag.define(),
|
|
48
43
|
QuoteMark: Tag.define(),
|
|
49
44
|
URL: Tag.define(),
|
|
50
|
-
};
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
// Custom.
|
|
47
|
+
TableCell: Tag.define(),
|
|
54
48
|
};
|
|
55
49
|
|
|
50
|
+
// TODO(burdon): Customize table parser (make all content monospace).
|
|
51
|
+
// https://github.com/lezer-parser/markdown/blob/main/src/extension.ts
|
|
52
|
+
Table.defineNodes?.forEach((node: any) => {
|
|
53
|
+
switch (node?.name) {
|
|
54
|
+
case 'TableCell': {
|
|
55
|
+
node.style = markdownTags.TableCell;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const markdownTagsExtensions: MarkdownConfig[] = [
|
|
62
|
+
Table,
|
|
63
|
+
{
|
|
64
|
+
props: [styleTags(markdownTags)],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
56
68
|
/**
|
|
57
69
|
* Styling based on `lezer` parser tags.
|
|
58
70
|
* https://codemirror.net/examples/styling
|
|
59
71
|
* https://github.com/lezer-parser/highlight
|
|
72
|
+
* https://github.com/lezer-parser/highlight/blob/main/src/highlight.ts#L427
|
|
60
73
|
* https://lezer.codemirror.net/docs/ref/#highlight.tags (list of tags)
|
|
61
74
|
*
|
|
62
75
|
* Examples:
|
|
63
76
|
* - https://github.com/codemirror/language/blob/main/src/highlight.ts#L194
|
|
64
77
|
* - https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts#L115
|
|
65
78
|
*/
|
|
66
|
-
export const markdownHighlightStyle = (readonly?: boolean) =>
|
|
67
|
-
HighlightStyle.define(
|
|
79
|
+
export const markdownHighlightStyle = (readonly?: boolean) => {
|
|
80
|
+
return HighlightStyle.define(
|
|
68
81
|
[
|
|
69
82
|
{
|
|
70
83
|
tag: [
|
|
@@ -129,6 +142,11 @@ export const markdownHighlightStyle = (readonly?: boolean) =>
|
|
|
129
142
|
class: readonly ? 'hidden' : codeMark,
|
|
130
143
|
},
|
|
131
144
|
|
|
145
|
+
{
|
|
146
|
+
tag: [tags.monospace],
|
|
147
|
+
class: 'font-mono',
|
|
148
|
+
},
|
|
149
|
+
|
|
132
150
|
// Headings.
|
|
133
151
|
{ tag: tags.heading1, class: heading(1, readonly) },
|
|
134
152
|
{ tag: tags.heading2, class: heading(2, readonly) },
|
|
@@ -163,11 +181,18 @@ export const markdownHighlightStyle = (readonly?: boolean) =>
|
|
|
163
181
|
class: blockquote,
|
|
164
182
|
},
|
|
165
183
|
|
|
184
|
+
// TODO(burdon): Replace with extension.
|
|
166
185
|
{
|
|
167
186
|
tag: [markdownTags.HorizontalRule],
|
|
168
187
|
class: mx(horizontalRule, readonly && '-indent-[100rem]'),
|
|
169
188
|
},
|
|
170
189
|
|
|
190
|
+
// TODO(burdon): Only if being edited.
|
|
191
|
+
{
|
|
192
|
+
tag: [markdownTags.TableCell],
|
|
193
|
+
class: 'font-mono',
|
|
194
|
+
},
|
|
195
|
+
|
|
171
196
|
// Main content, paragraphs, etc.
|
|
172
197
|
// {
|
|
173
198
|
// tag: [tags.content],
|
|
@@ -179,3 +204,4 @@ export const markdownHighlightStyle = (readonly?: boolean) =>
|
|
|
179
204
|
all: { fontFamily: get(tokens, 'fontFamily.body', []).join(',') },
|
|
180
205
|
},
|
|
181
206
|
);
|
|
207
|
+
};
|
|
@@ -25,61 +25,57 @@ export const table = (options: TableOptions = {}) => {
|
|
|
25
25
|
|
|
26
26
|
type Table = {
|
|
27
27
|
from: number;
|
|
28
|
-
to
|
|
28
|
+
to: number;
|
|
29
29
|
header?: string[];
|
|
30
30
|
rows?: string[][];
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const update = (state: EditorState, options: TableOptions) => {
|
|
34
34
|
const builder = new RangeSetBuilder();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
getTable().header?.push(state.sliceDoc(node.from, node.to));
|
|
68
|
-
}
|
|
69
|
-
break;
|
|
70
|
-
}
|
|
71
|
-
case 'TableDelimiter': {
|
|
72
|
-
getTable().to = node.to;
|
|
73
|
-
break;
|
|
35
|
+
const cursor = state.selection.main.head;
|
|
36
|
+
|
|
37
|
+
const tables: Table[] = [];
|
|
38
|
+
const getTable = () => tables[tables.length - 1];
|
|
39
|
+
const getRow = () => {
|
|
40
|
+
const table = getTable();
|
|
41
|
+
return table.rows?.[table.rows.length - 1];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Parse table.
|
|
45
|
+
syntaxTree(state).iterate({
|
|
46
|
+
enter: (node) => {
|
|
47
|
+
// Check if cursor is inside text.
|
|
48
|
+
switch (node.name) {
|
|
49
|
+
case 'Table': {
|
|
50
|
+
tables.push({ from: node.from, to: node.to });
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case 'TableHeader': {
|
|
54
|
+
getTable().header = [];
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case 'TableRow': {
|
|
58
|
+
(getTable().rows ??= []).push([]);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case 'TableCell': {
|
|
62
|
+
const row = getRow();
|
|
63
|
+
if (row) {
|
|
64
|
+
row.push(state.sliceDoc(node.from, node.to));
|
|
65
|
+
} else {
|
|
66
|
+
getTable().header?.push(state.sliceDoc(node.from, node.to));
|
|
74
67
|
}
|
|
68
|
+
break;
|
|
75
69
|
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
78
73
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
|
|
74
|
+
tables.forEach((table) => {
|
|
75
|
+
const hide = state.readOnly || cursor < table.from || cursor > table.to;
|
|
76
|
+
hide && builder.add(table.from, table.to, Decoration.replace({ widget: new TableWidget(table) }));
|
|
77
|
+
builder.add(table.from, table.to, Decoration.mark({ class: 'cm-table' }));
|
|
78
|
+
});
|
|
83
79
|
|
|
84
80
|
return builder.finish();
|
|
85
81
|
};
|
|
@@ -98,11 +94,11 @@ class TableWidget extends WidgetType {
|
|
|
98
94
|
|
|
99
95
|
{
|
|
100
96
|
const header = table.appendChild(document.createElement('thead'));
|
|
101
|
-
const
|
|
97
|
+
const tr = header.appendChild(document.createElement('tr'));
|
|
102
98
|
this._table.header?.forEach((cell) => {
|
|
103
99
|
const th = document.createElement('th');
|
|
104
100
|
th.setAttribute('class', 'cm-table-head');
|
|
105
|
-
|
|
101
|
+
tr.appendChild(th).textContent = cell;
|
|
106
102
|
});
|
|
107
103
|
}
|
|
108
104
|
|
|
@@ -12,6 +12,14 @@ import {
|
|
|
12
12
|
type ViewUpdate,
|
|
13
13
|
} from '@codemirror/view';
|
|
14
14
|
|
|
15
|
+
// TODO(burdon): Reconcile with theme.
|
|
16
|
+
const styles = EditorView.baseTheme({
|
|
17
|
+
'& .cm-task-item': {
|
|
18
|
+
paddingLeft: '4px',
|
|
19
|
+
paddingRight: '8px',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
15
23
|
class CheckboxWidget extends WidgetType {
|
|
16
24
|
constructor(
|
|
17
25
|
private readonly _pos: number,
|
|
@@ -53,14 +61,6 @@ class CheckboxWidget extends WidgetType {
|
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
// TODO(burdon): Reconcile with theme.
|
|
57
|
-
const styles = EditorView.baseTheme({
|
|
58
|
-
'& .cm-task-item': {
|
|
59
|
-
paddingLeft: '4px',
|
|
60
|
-
paddingRight: '8px',
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
|
|
64
64
|
export type TasklistOptions = {};
|
|
65
65
|
|
|
66
66
|
export const tasklist = (options: TasklistOptions = {}) => {
|
|
@@ -109,7 +109,7 @@ export const tasklist = (options: TasklistOptions = {}) => {
|
|
|
109
109
|
}
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
|
-
decorations: (
|
|
112
|
+
decorations: (value) => value.tasks,
|
|
113
113
|
provide: (plugin) =>
|
|
114
114
|
EditorView.atomicRanges.of((view) => {
|
|
115
115
|
return view.plugin(plugin)?.tasks || Decoration.none;
|
|
@@ -55,10 +55,10 @@ export const defaultTheme: {
|
|
|
55
55
|
//
|
|
56
56
|
// Cursor
|
|
57
57
|
//
|
|
58
|
-
'&light .cm-cursor': {
|
|
58
|
+
'&light .cm-cursor, &light .cm-dropCursor': {
|
|
59
59
|
borderLeft: '2px solid black',
|
|
60
60
|
},
|
|
61
|
-
'&dark .cm-cursor': {
|
|
61
|
+
'&dark .cm-cursor, &dark .cm-dropCursor': {
|
|
62
62
|
borderLeft: '2px solid white',
|
|
63
63
|
},
|
|
64
64
|
'& .cm-placeholder': {
|
|
@@ -173,6 +173,10 @@ export const defaultTheme: {
|
|
|
173
173
|
//
|
|
174
174
|
// table
|
|
175
175
|
//
|
|
176
|
+
'& .cm-table *': {
|
|
177
|
+
fontFamily: `${get(tokens, 'fontFamily.mono', []).join(',')} !important`,
|
|
178
|
+
textDecoration: 'none !important',
|
|
179
|
+
},
|
|
176
180
|
'& .cm-table-head': {
|
|
177
181
|
padding: '2px 16px 2px 0px',
|
|
178
182
|
borderBottom: `1px solid ${get(tokens, 'extend.colors.neutral.500')}`,
|
|
@@ -203,6 +207,43 @@ export const defaultTheme: {
|
|
|
203
207
|
acc[`& .text-${fontSize} + .cm-widgetBuffer + .cm-ySelectionCaret`] = { height };
|
|
204
208
|
return acc;
|
|
205
209
|
}, {}),
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Panels
|
|
213
|
+
* https://github.com/codemirror/search/blob/main/src/search.ts#L745
|
|
214
|
+
*
|
|
215
|
+
* Find/replace panel.
|
|
216
|
+
* <div class="cm-announced">...</div>
|
|
217
|
+
* <div class="cm-scroller">...</div>
|
|
218
|
+
* <div class="cm-panels cm-panels-bottom">
|
|
219
|
+
* <div class="cm-search cm-panel">
|
|
220
|
+
* <input class="cm-textfield" />
|
|
221
|
+
* <button class="cm-button">...</button>
|
|
222
|
+
* <label><input type="checkbox" />...</label>
|
|
223
|
+
* </div>
|
|
224
|
+
* </div
|
|
225
|
+
*/
|
|
226
|
+
'& .cm-panels': {
|
|
227
|
+
border: `1px solid ${get(tokens, 'extend.colors.neutral.200')}`,
|
|
228
|
+
borderBottom: 'none',
|
|
229
|
+
},
|
|
230
|
+
'& .cm-panel': {
|
|
231
|
+
background: get(tokens, 'extend.colors.neutral.50'),
|
|
232
|
+
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
233
|
+
},
|
|
234
|
+
'& .cm-button': {
|
|
235
|
+
margin: '4px',
|
|
236
|
+
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
237
|
+
background: get(tokens, 'extend.colors.neutral.100'),
|
|
238
|
+
border: 'none',
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
'& .cm-searchMatch': {
|
|
242
|
+
backgroundColor: get(tokens, 'extend.colors.yellow.100'),
|
|
243
|
+
},
|
|
244
|
+
'& .cm-searchMatch.cm-searchMatch-selected': {
|
|
245
|
+
backgroundColor: get(tokens, 'extend.colors.primary.100'),
|
|
246
|
+
},
|
|
206
247
|
};
|
|
207
248
|
|
|
208
249
|
export const textTheme: {
|
|
@@ -9,6 +9,7 @@ import { basicSetup } from 'codemirror';
|
|
|
9
9
|
import get from 'lodash.get';
|
|
10
10
|
import React, { useEffect, useRef, useState } from 'react';
|
|
11
11
|
|
|
12
|
+
// TODO(burdon): Why separate imports?
|
|
12
13
|
import { type Prop } from '@dxos/automerge/automerge';
|
|
13
14
|
import { type DocHandle, Repo } from '@dxos/automerge/automerge-repo';
|
|
14
15
|
|
|
@@ -26,12 +26,19 @@ import { SpaceAwarenessProvider } from './yjs';
|
|
|
26
26
|
// TODO(burdon): Move.
|
|
27
27
|
type Awareness = awarenessProtocol.Awareness;
|
|
28
28
|
|
|
29
|
+
export type DocumentRange = {
|
|
30
|
+
id: string;
|
|
31
|
+
from: number;
|
|
32
|
+
to: number;
|
|
33
|
+
};
|
|
34
|
+
|
|
29
35
|
// TODO(wittjosiah): Factor out to common package? @dxos/react-client?
|
|
30
36
|
export type EditorModel = {
|
|
31
37
|
id: string;
|
|
32
38
|
// TODO(burdon): Remove.
|
|
33
39
|
content: string | YText | YXmlFragment | DocAccessor;
|
|
34
40
|
text: () => string;
|
|
41
|
+
ranges: DocumentRange[];
|
|
35
42
|
extension?: Extension;
|
|
36
43
|
awareness?: Awareness;
|
|
37
44
|
peer?: {
|
|
@@ -80,6 +87,7 @@ const createYjsModel = ({ identity, space, text }: UseTextModelOptions): EditorM
|
|
|
80
87
|
id: text.doc.guid,
|
|
81
88
|
content: text.content,
|
|
82
89
|
text: () => text.content!.toString(),
|
|
90
|
+
ranges: [],
|
|
83
91
|
extension: yCollab(text.content as YText, provider?.awareness),
|
|
84
92
|
awareness: provider?.awareness,
|
|
85
93
|
peer: identity
|
|
@@ -99,6 +107,7 @@ const createAutomergeModel = ({ identity, text }: UseTextModelOptions): EditorMo
|
|
|
99
107
|
id: obj.id,
|
|
100
108
|
content: doc,
|
|
101
109
|
text: () => get(doc.handle.docSync(), doc.path),
|
|
110
|
+
ranges: [],
|
|
102
111
|
extension: automergePlugin(doc.handle, doc.path),
|
|
103
112
|
peer: identity
|
|
104
113
|
? {
|
|
@@ -16,6 +16,7 @@ import { MarkdownEditor } from '../../components';
|
|
|
16
16
|
import { Replicator, useYjsModel } from '../../testing';
|
|
17
17
|
|
|
18
18
|
export default {
|
|
19
|
+
title: 'react-ui-editor/YJS',
|
|
19
20
|
component: MarkdownEditor,
|
|
20
21
|
decorators: [withTheme],
|
|
21
22
|
};
|
|
@@ -50,7 +51,6 @@ const Story = () => {
|
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
export const Default = {
|
|
53
|
-
title: 'react-ui-editor/YJS',
|
|
54
54
|
// TODO(wittjosiah): Decorator for doing this without clients being initialized?
|
|
55
55
|
render: () => <ClientRepeater count={2} Component={Story} />,
|
|
56
56
|
};
|
package/src/styles/markdown.ts
CHANGED
|
@@ -6,12 +6,13 @@ import { mx } from '@dxos/react-ui-theme';
|
|
|
6
6
|
|
|
7
7
|
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
8
|
|
|
9
|
+
// TODO(burdon): Better way to align vertically than negative margin? Font-specific?
|
|
9
10
|
// https://tailwindcss.com/docs/font-weight
|
|
10
11
|
export const headings: Record<HeadingLevel, string[]> = {
|
|
11
|
-
1: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-4xl', '-ml-
|
|
12
|
-
2: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-3xl', '-ml-
|
|
13
|
-
3: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-2xl', '-ml-
|
|
14
|
-
4: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-xl', '-ml-[
|
|
12
|
+
1: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-4xl', '-ml-[10px]'],
|
|
13
|
+
2: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-3xl', '-ml-[8px]'],
|
|
14
|
+
3: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-2xl', '-ml-[7px]'],
|
|
15
|
+
4: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-xl', '-ml-[6px]'],
|
|
15
16
|
5: ['mbs-4 mbe-2 font-medium text-inherit no-underline text-lg', '-ml-[5px]'],
|
|
16
17
|
6: ['mbs-4 mbe-2 font-medium text-inherit no-underline', '-ml-[4px]'],
|
|
17
18
|
};
|