@dxos/react-ui-editor 0.6.7 → 0.6.8-main.3be982f
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 +1019 -796
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/{hooks/InputMode.stories.d.ts → InputMode.stories.d.ts} +6 -4
- package/dist/types/src/InputMode.stories.d.ts.map +1 -0
- package/dist/types/src/{hooks/TextEditor.stories.d.ts → TextEditor.stories.d.ts} +43 -26
- package/dist/types/src/TextEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +9 -9
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/defaults.d.ts +10 -0
- package/dist/types/src/defaults.d.ts.map +1 -0
- package/dist/types/src/extensions/autocomplete.d.ts +2 -2
- package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +6 -4
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/defs.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/update-automerge.d.ts.map +1 -1
- package/dist/types/src/extensions/blast.d.ts.map +1 -1
- package/dist/types/src/extensions/command/state.d.ts +1 -1
- package/dist/types/src/extensions/command/state.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/folding.d.ts +7 -0
- package/dist/types/src/extensions/folding.d.ts.map +1 -0
- package/dist/types/src/extensions/index.d.ts +1 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts +5 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +9 -9
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/link-paste.d.ts +6 -0
- package/dist/types/src/extensions/markdown/link-paste.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/link-paste.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/link-paste.test.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/parser.test.d.ts +2 -0
- package/dist/types/src/extensions/markdown/parser.test.d.ts.map +1 -0
- package/dist/types/src/extensions/state.d.ts.map +1 -1
- package/dist/types/src/extensions/util/error.d.ts +2 -0
- package/dist/types/src/extensions/util/error.d.ts.map +1 -0
- package/dist/types/src/extensions/util/index.d.ts +3 -1
- package/dist/types/src/extensions/util/index.d.ts.map +1 -1
- package/dist/types/src/extensions/util/overlap.d.ts.map +1 -1
- package/dist/types/src/extensions/util/react.d.ts +3 -0
- package/dist/types/src/extensions/util/react.d.ts.map +1 -0
- package/dist/types/src/hooks/useActionHandler.d.ts +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/styles/index.d.ts +1 -1
- package/dist/types/src/styles/index.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +0 -1
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/styles/theme.d.ts +36 -0
- package/dist/types/src/styles/theme.d.ts.map +1 -0
- package/dist/types/src/styles/tokens.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +26 -29
- package/src/{hooks/InputMode.stories.tsx → InputMode.stories.tsx} +6 -11
- package/src/{hooks/TextEditor.stories.tsx → TextEditor.stories.tsx} +139 -92
- package/src/components/Toolbar/Toolbar.tsx +17 -9
- package/src/defaults.ts +28 -0
- package/src/extensions/autocomplete.ts +24 -18
- package/src/extensions/automerge/automerge.stories.tsx +4 -6
- package/src/extensions/comments.ts +4 -0
- package/src/extensions/factories.ts +3 -2
- package/src/extensions/folding.tsx +34 -0
- package/src/extensions/index.ts +1 -0
- package/src/extensions/markdown/bundle.ts +1 -1
- package/src/extensions/markdown/decorate.ts +359 -129
- package/src/extensions/markdown/formatting.ts +10 -12
- package/src/extensions/markdown/image.ts +3 -1
- package/src/extensions/markdown/link-paste.test.ts +28 -0
- package/src/extensions/markdown/link-paste.ts +104 -0
- package/src/extensions/markdown/parser.test.ts +47 -0
- package/src/extensions/markdown/table.ts +21 -24
- package/src/extensions/util/error.ts +15 -0
- package/src/extensions/util/index.ts +3 -1
- package/src/extensions/util/overlap.ts +1 -0
- package/src/extensions/util/react.tsx +15 -0
- package/src/hooks/useTextEditor.ts +1 -1
- package/src/index.ts +2 -2
- package/src/styles/index.ts +1 -1
- package/src/styles/markdown.ts +4 -3
- package/src/{themes/default.ts → styles/theme.ts} +51 -43
- package/src/styles/tokens.ts +0 -1
- package/src/translations.ts +2 -0
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +0 -57
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/linkPaste.d.ts +0 -16
- package/dist/types/src/extensions/markdown/linkPaste.d.ts.map +0 -1
- package/dist/types/src/extensions/markdown/linkPaste.test.d.ts +0 -2
- package/dist/types/src/extensions/markdown/linkPaste.test.d.ts.map +0 -1
- package/dist/types/src/hooks/InputMode.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/TextEditor.stories.d.ts.map +0 -1
- package/dist/types/src/styles/layout.d.ts +0 -4
- package/dist/types/src/styles/layout.d.ts.map +0 -1
- package/dist/types/src/themes/default.d.ts +0 -14
- package/dist/types/src/themes/default.d.ts.map +0 -1
- package/dist/types/src/themes/index.d.ts +0 -2
- package/dist/types/src/themes/index.d.ts.map +0 -1
- package/src/components/Toolbar/Toolbar.stories.tsx +0 -119
- package/src/extensions/markdown/linkPaste.test.ts +0 -45
- package/src/extensions/markdown/linkPaste.ts +0 -113
- package/src/styles/layout.ts +0 -9
- package/src/themes/index.ts +0 -5
|
@@ -26,10 +26,10 @@ import { useMemo, useState } from 'react';
|
|
|
26
26
|
// the field only holds true when *all* selected text has the style,
|
|
27
27
|
// or when the selection is a cursor inside such a style.
|
|
28
28
|
export type Formatting = {
|
|
29
|
-
blankLine
|
|
29
|
+
blankLine?: boolean;
|
|
30
30
|
// The type of the block at the selection.
|
|
31
31
|
// If multiple different block types are selected, this will hold null.
|
|
32
|
-
blockType
|
|
32
|
+
blockType?:
|
|
33
33
|
| 'codeblock'
|
|
34
34
|
| 'heading1'
|
|
35
35
|
| 'heading2'
|
|
@@ -41,19 +41,19 @@ export type Formatting = {
|
|
|
41
41
|
| 'tablecell'
|
|
42
42
|
| null;
|
|
43
43
|
// Whether all selected text is wrapped in a blockquote.
|
|
44
|
-
blockQuote
|
|
44
|
+
blockQuote?: boolean;
|
|
45
45
|
// Whether the selected text is strong.
|
|
46
|
-
strong
|
|
46
|
+
strong?: boolean;
|
|
47
47
|
// Whether the selected text is emphasized.
|
|
48
|
-
emphasis
|
|
48
|
+
emphasis?: boolean;
|
|
49
49
|
// Whether the selected text is stricken through.
|
|
50
|
-
strikethrough
|
|
50
|
+
strikethrough?: boolean;
|
|
51
51
|
// Whether the selected text is inline code.
|
|
52
|
-
code
|
|
52
|
+
code?: boolean;
|
|
53
53
|
// Whether there are links in the selected text.
|
|
54
|
-
link
|
|
54
|
+
link?: boolean;
|
|
55
55
|
// If all selected blocks have the same (innermost) list style, that is indicated here.
|
|
56
|
-
listStyle
|
|
56
|
+
listStyle?: null | 'ordered' | 'bullet' | 'task';
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
export const formattingEquals = (a: Formatting, b: Formatting) =>
|
|
@@ -705,7 +705,6 @@ export const addList = (type: List): StateCommand => {
|
|
|
705
705
|
renumberListItems(next.firstChild, last.counter + 1, changes, state.doc);
|
|
706
706
|
}
|
|
707
707
|
}
|
|
708
|
-
('Oeswe');
|
|
709
708
|
const changeSet = state.changes(changes);
|
|
710
709
|
dispatch(
|
|
711
710
|
state.update({
|
|
@@ -1249,7 +1248,6 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
|
1249
1248
|
*/
|
|
1250
1249
|
export const useFormattingState = (): [Formatting | undefined, Extension] => {
|
|
1251
1250
|
const [state, setState] = useState<Formatting>();
|
|
1252
|
-
|
|
1253
1251
|
const observer = useMemo(
|
|
1254
1252
|
() =>
|
|
1255
1253
|
EditorView.updateListener.of((update) => {
|
|
@@ -1263,7 +1261,7 @@ export const useFormattingState = (): [Formatting | undefined, Extension] => {
|
|
|
1263
1261
|
});
|
|
1264
1262
|
}
|
|
1265
1263
|
}),
|
|
1266
|
-
[
|
|
1264
|
+
[],
|
|
1267
1265
|
);
|
|
1268
1266
|
|
|
1269
1267
|
return [state, observer];
|
|
@@ -8,7 +8,7 @@ import { Decoration, type DecorationSet, EditorView, WidgetType } from '@codemir
|
|
|
8
8
|
|
|
9
9
|
export type ImageOptions = {};
|
|
10
10
|
|
|
11
|
-
export const image = (
|
|
11
|
+
export const image = (_options: ImageOptions = {}): Extension => {
|
|
12
12
|
return StateField.define<DecorationSet>({
|
|
13
13
|
create: (state) => {
|
|
14
14
|
return Decoration.set(buildDecorations(0, state.doc.length, state));
|
|
@@ -17,6 +17,7 @@ export const image = (options: ImageOptions = {}): Extension => {
|
|
|
17
17
|
if (!tr.docChanged && !tr.selection) {
|
|
18
18
|
return value;
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
// Find range of changes and cursor changes.
|
|
21
22
|
const cursor = tr.state.selection.main.head;
|
|
22
23
|
const oldCursor = tr.changes.mapPos(tr.startState.selection.main.head);
|
|
@@ -26,6 +27,7 @@ export const image = (options: ImageOptions = {}): Extension => {
|
|
|
26
27
|
from = Math.min(from, fromB);
|
|
27
28
|
to = Math.max(to, toB);
|
|
28
29
|
});
|
|
30
|
+
|
|
29
31
|
// Expand to cover lines.
|
|
30
32
|
from = tr.state.doc.lineAt(from).from;
|
|
31
33
|
to = tr.state.doc.lineAt(to).to;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import { describe, test } from '@dxos/test';
|
|
8
|
+
|
|
9
|
+
import { createLinkLabel } from './link-paste';
|
|
10
|
+
|
|
11
|
+
const testCases = [
|
|
12
|
+
{ input: 'https://www.example.com', expected: 'example.com' },
|
|
13
|
+
{ input: 'http://example.com', expected: 'example.com' },
|
|
14
|
+
{ input: 'https://example.com/', expected: 'example.com' },
|
|
15
|
+
{ input: 'https://www.example.com/', expected: 'example.com' },
|
|
16
|
+
{ input: 'https://example.com/test', expected: 'example.com/test' },
|
|
17
|
+
{ input: 'https://www.example.com/test', expected: 'example.com/test' },
|
|
18
|
+
{ input: 'https://example.com?name=value', expected: 'example.com' },
|
|
19
|
+
{ input: 'ftp://example.com', expected: 'ftp://example.com' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
describe('links', () => {
|
|
23
|
+
test('createLinkLabel', () => {
|
|
24
|
+
testCases.forEach(({ input, expected }) => {
|
|
25
|
+
expect(createLinkLabel(new URL(input))).to.eq(expected);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { syntaxTree } from '@codemirror/language';
|
|
6
|
+
import { type EditorState, Transaction } from '@codemirror/state';
|
|
7
|
+
import { ViewPlugin, type ViewUpdate, type PluginValue } from '@codemirror/view';
|
|
8
|
+
import { type SyntaxNode } from '@lezer/common';
|
|
9
|
+
|
|
10
|
+
export const linkPastePlugin = ViewPlugin.fromClass(
|
|
11
|
+
class implements PluginValue {
|
|
12
|
+
update(update: ViewUpdate) {
|
|
13
|
+
for (const tr of update.transactions) {
|
|
14
|
+
const event = tr.annotation(Transaction.userEvent);
|
|
15
|
+
if (event === 'input.paste') {
|
|
16
|
+
const changes = tr.changes;
|
|
17
|
+
if (changes.empty) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
changes.iterChangedRanges((fromA, toA, fromB, toB) => {
|
|
22
|
+
const insertedUrl = getValidUrl(update.view.state.sliceDoc(fromB, toB));
|
|
23
|
+
if (insertedUrl && isValidPosition(update.view.state, fromB)) {
|
|
24
|
+
// We might be pasting over an existing text.
|
|
25
|
+
const replacedText = tr.startState.sliceDoc(fromA, toA);
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
update.view.dispatch(
|
|
28
|
+
update.view.state.update({
|
|
29
|
+
changes: { from: fromA, to: toB, insert: createLink(insertedUrl, replacedText) },
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const createLink = (url: URL, label: string): string => {
|
|
42
|
+
// Check if image.
|
|
43
|
+
// Example: https://dxos.network/dxos-logotype-blue.png
|
|
44
|
+
const { host, pathname } = url;
|
|
45
|
+
const [, extension] = pathname.split('.');
|
|
46
|
+
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
|
|
47
|
+
if (imageExtensions.includes(extension)) {
|
|
48
|
+
return ``;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!label) {
|
|
52
|
+
label = createLinkLabel(url);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return `[${label}](${url})`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const createLinkLabel = (url: URL): string => {
|
|
59
|
+
let { protocol, host, pathname } = url;
|
|
60
|
+
if (protocol === 'http:' || protocol === 'https:') {
|
|
61
|
+
protocol = '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// NOTE(Zan): Consult: https://github.com/dxos/dxos/issues/7331 before changing this.
|
|
65
|
+
// Remove 'www.' if at the beginning of the URL
|
|
66
|
+
host = host.replace(/^www\./, '');
|
|
67
|
+
|
|
68
|
+
return [protocol, host].filter(Boolean).join('//') + (pathname !== '/' ? pathname : '');
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns a valid URL if appropriate for a link.
|
|
73
|
+
*/
|
|
74
|
+
const getValidUrl = (str: string): URL | undefined => {
|
|
75
|
+
const validProtocols = ['http:', 'https:', 'mailto:', 'tel:'];
|
|
76
|
+
try {
|
|
77
|
+
const url = new URL(str);
|
|
78
|
+
if (!validProtocols.includes(url.protocol)) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return url;
|
|
83
|
+
} catch (_err) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Traverses the syntax tree upwards from the position.
|
|
90
|
+
*/
|
|
91
|
+
const isValidPosition = (state: EditorState, pos: number): boolean => {
|
|
92
|
+
const invalidPositions = new Set(['Link', 'LinkMark', 'Code', 'FencedCode']);
|
|
93
|
+
const tree = syntaxTree(state);
|
|
94
|
+
let node: SyntaxNode | null = tree.resolveInner(pos, -1);
|
|
95
|
+
while (node) {
|
|
96
|
+
if (invalidPositions.has(node.name)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
node = node.parent;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return true;
|
|
104
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
import { testTree } from '@lezer/generator/test';
|
|
7
|
+
import { parser } from '@lezer/markdown';
|
|
8
|
+
|
|
9
|
+
import { describe, test } from '@dxos/test';
|
|
10
|
+
|
|
11
|
+
describe('parser', () => {
|
|
12
|
+
// https://www.markdownguide.org/basic-syntax/#lists-1
|
|
13
|
+
test('lists', () => {
|
|
14
|
+
// Indented list must have 4 spaces.
|
|
15
|
+
const result = parser.parse(
|
|
16
|
+
[
|
|
17
|
+
'# H1',
|
|
18
|
+
'1. one',
|
|
19
|
+
'2. two',
|
|
20
|
+
'3. three',
|
|
21
|
+
' 1. four',
|
|
22
|
+
'',
|
|
23
|
+
// TODO(burdon): Test list termination without heading as break.
|
|
24
|
+
'# H2',
|
|
25
|
+
'1. one',
|
|
26
|
+
].join('\n'),
|
|
27
|
+
);
|
|
28
|
+
testTree(
|
|
29
|
+
result,
|
|
30
|
+
[
|
|
31
|
+
'Document(',
|
|
32
|
+
'ATXHeading1(HeaderMark)',
|
|
33
|
+
'OrderedList(',
|
|
34
|
+
'ListItem(ListMark,Paragraph),',
|
|
35
|
+
'ListItem(ListMark,Paragraph),',
|
|
36
|
+
'ListItem(ListMark,Paragraph,OrderedList(ListItem(ListMark,Paragraph)))',
|
|
37
|
+
')',
|
|
38
|
+
'',
|
|
39
|
+
'ATXHeading1(HeaderMark)',
|
|
40
|
+
'OrderedList(',
|
|
41
|
+
'ListItem(ListMark,Paragraph),',
|
|
42
|
+
')',
|
|
43
|
+
')',
|
|
44
|
+
].join(''),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -39,7 +39,7 @@ type Table = {
|
|
|
39
39
|
rows?: string[][];
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const update = (state: EditorState,
|
|
42
|
+
const update = (state: EditorState, _options: TableOptions) => {
|
|
43
43
|
const builder = new RangeSetBuilder();
|
|
44
44
|
const cursor = state.selection.main.head;
|
|
45
45
|
|
|
@@ -114,31 +114,28 @@ class TableWidget extends WidgetType {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
override toDOM(view: EditorView) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
const div = document.createElement('div');
|
|
118
|
+
const table = div.appendChild(document.createElement('table'));
|
|
119
|
+
|
|
120
|
+
const header = table.appendChild(document.createElement('thead'));
|
|
121
|
+
const tr = header.appendChild(document.createElement('tr'));
|
|
122
|
+
this._table.header?.forEach((cell) => {
|
|
123
|
+
const th = document.createElement('th');
|
|
124
|
+
th.setAttribute('class', 'cm-table-head');
|
|
125
|
+
tr.appendChild(th).textContent = cell;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const body = table.appendChild(document.createElement('tbody'));
|
|
129
|
+
this._table.rows?.forEach((row) => {
|
|
130
|
+
const tr = body.appendChild(document.createElement('tr'));
|
|
131
|
+
row.forEach((cell) => {
|
|
132
|
+
const td = document.createElement('td');
|
|
133
|
+
td.setAttribute('class', 'cm-table-cell');
|
|
134
|
+
tr.appendChild(td).textContent = cell;
|
|
126
135
|
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
{
|
|
130
|
-
const body = table.appendChild(document.createElement('tbody'));
|
|
131
|
-
this._table.rows?.forEach((row) => {
|
|
132
|
-
const tr = body.appendChild(document.createElement('tr'));
|
|
133
|
-
row.forEach((cell) => {
|
|
134
|
-
const td = document.createElement('td');
|
|
135
|
-
td.setAttribute('class', 'cm-table-cell');
|
|
136
|
-
tr.appendChild(td).textContent = cell;
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
}
|
|
136
|
+
});
|
|
140
137
|
|
|
141
|
-
return
|
|
138
|
+
return div;
|
|
142
139
|
}
|
|
143
140
|
|
|
144
141
|
override ignoreEvent(e: Event) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { log } from '@dxos/log';
|
|
6
|
+
|
|
7
|
+
export const wrapWithCatch = (fn: (...args: any[]) => any) => {
|
|
8
|
+
return (...args: any[]) => {
|
|
9
|
+
try {
|
|
10
|
+
return fn(...args);
|
|
11
|
+
} catch (err) {
|
|
12
|
+
log.catch(err);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type ReactNode } from 'react';
|
|
6
|
+
import { createRoot } from 'react-dom/client';
|
|
7
|
+
|
|
8
|
+
import { ThemeProvider } from '@dxos/react-ui';
|
|
9
|
+
import { defaultTx } from '@dxos/react-ui-theme';
|
|
10
|
+
|
|
11
|
+
export const renderRoot = (node: ReactNode) => {
|
|
12
|
+
const el = document.createElement('div');
|
|
13
|
+
createRoot(el).render(<ThemeProvider tx={defaultTx}>{node}</ThemeProvider>);
|
|
14
|
+
return el;
|
|
15
|
+
};
|
|
@@ -99,7 +99,7 @@ export const useTextEditor = (
|
|
|
99
99
|
selection: initialSelection,
|
|
100
100
|
extensions: [
|
|
101
101
|
id && documentId.of(id),
|
|
102
|
-
//
|
|
102
|
+
// NOTE: Doesn't catch errors in keymap functions.
|
|
103
103
|
EditorView.exceptionSink.of((err) => {
|
|
104
104
|
log.catch(err);
|
|
105
105
|
}),
|
package/src/index.ts
CHANGED
|
@@ -11,9 +11,9 @@ export { tags } from '@lezer/highlight';
|
|
|
11
11
|
export { TextKind } from '@dxos/protocols/proto/dxos/echo/model/text';
|
|
12
12
|
|
|
13
13
|
export * from './components';
|
|
14
|
+
export * from './defaults';
|
|
14
15
|
export * from './extensions';
|
|
15
16
|
export * from './hooks';
|
|
16
|
-
export { getToken, editorWithToolbarLayout, editorFillLayoutRoot, editorFillLayoutEditor } from './styles';
|
|
17
|
-
export * from './themes';
|
|
18
17
|
export * from './util';
|
|
18
|
+
|
|
19
19
|
export { translations };
|
package/src/styles/index.ts
CHANGED
package/src/styles/markdown.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
|
8
8
|
|
|
9
9
|
// TODO(burdon): Better way to align vertically than negative margin? Font-specific?
|
|
10
10
|
// https://tailwindcss.com/docs/font-weight
|
|
11
|
-
|
|
11
|
+
const headings: Record<HeadingLevel, string> = {
|
|
12
12
|
1: 'mbs-4 mbe-2 font-medium text-inherit no-underline text-4xl',
|
|
13
13
|
2: 'mbs-4 mbe-2 font-medium text-inherit no-underline text-3xl',
|
|
14
14
|
3: 'mbs-4 mbe-2 font-medium text-inherit no-underline text-2xl',
|
|
@@ -17,8 +17,9 @@ export const headings: Record<HeadingLevel, string> = {
|
|
|
17
17
|
6: 'mbs-4 mbe-2 font-medium text-inherit no-underline',
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
// TODO(burdon): Themes.
|
|
20
21
|
export const heading = (level: HeadingLevel) => {
|
|
21
|
-
return headings[level];
|
|
22
|
+
return mx(headings[level], 'dark:text-primary-400');
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
export const text = 'text-neutral-800 dark:text-neutral-200';
|
|
@@ -38,7 +39,7 @@ export const codeBlock = 'mlb-2 font-mono bg-neutral-500/10 p-3 rounded';
|
|
|
38
39
|
|
|
39
40
|
export const inlineUrl = mx(code, 'px-1');
|
|
40
41
|
|
|
41
|
-
export const blockquote = mx('pl-1 mr-1 border-is-4 border-
|
|
42
|
+
export const blockquote = mx('pl-1 mr-1 border-is-4 border-orange-500 dark:border-orange-500 text-transparent');
|
|
42
43
|
|
|
43
44
|
export const horizontalRule =
|
|
44
45
|
'flex mlb-4 border-b text-neutral-100 dark:text-neutral-900 border-neutral-200 dark:border-neutral-800';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import get from 'lodash.get';
|
|
6
6
|
|
|
7
|
-
import { type ThemeStyles, tokens } from '
|
|
7
|
+
import { type ThemeStyles, tokens } from './tokens';
|
|
8
8
|
|
|
9
9
|
// TODO(burdon): Can we use @apply and import css file?
|
|
10
10
|
// https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply?
|
|
@@ -17,61 +17,56 @@ import { type ThemeStyles, tokens } from '../styles';
|
|
|
17
17
|
* - https://github.com/codemirror/view/blob/main/src/theme.ts
|
|
18
18
|
* - https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
|
|
19
19
|
*
|
|
20
|
+
* Main layout:
|
|
21
|
+
* https://codemirror.net/examples/styling
|
|
22
|
+
*
|
|
23
|
+
* <div class="cm-editor [cm-focused] [generated classes]">
|
|
24
|
+
* <div class="cm-scroller">
|
|
25
|
+
* <div class="cm-gutters">
|
|
26
|
+
* <div class="cm-gutter [...]">
|
|
27
|
+
* <div class="cm-gutterElement">...</div>
|
|
28
|
+
* </div>
|
|
29
|
+
* </div>
|
|
30
|
+
* <div class="cm-content" role="textbox" contenteditable="true">
|
|
31
|
+
* <div class="cm-line"></div>
|
|
32
|
+
* </div>
|
|
33
|
+
* <div class="cm-selectionLayer">
|
|
34
|
+
* <div class="cm-selectionBackground"></div>
|
|
35
|
+
* </div>
|
|
36
|
+
* <div class="cm-cursorLayer">
|
|
37
|
+
* <div class="cm-cursor"></div>
|
|
38
|
+
* </div>
|
|
39
|
+
* </div>
|
|
40
|
+
* </div>
|
|
41
|
+
*
|
|
20
42
|
* NOTE: Use one of '&', '&light', and '&dark' prefix to scope instance.
|
|
21
43
|
* NOTE: `light` and `dark` selectors are preprocessed by CodeMirror and can only be in the base theme.
|
|
22
44
|
*/
|
|
23
45
|
export const defaultTheme: ThemeStyles = {
|
|
24
|
-
//
|
|
25
|
-
// Main layout:
|
|
26
|
-
// https://codemirror.net/examples/styling
|
|
27
|
-
//
|
|
28
|
-
// <div class="cm-editor [cm-focused] [generated classes]">
|
|
29
|
-
// <div class="cm-scroller">
|
|
30
|
-
// <div class="cm-gutters">
|
|
31
|
-
// <div class="cm-gutter [...]">
|
|
32
|
-
// <div class="cm-gutterElement">...</div>
|
|
33
|
-
// </div>
|
|
34
|
-
// </div>
|
|
35
|
-
// <div class="cm-content" role="textbox" contenteditable="true">
|
|
36
|
-
// <div class="cm-line"></div>
|
|
37
|
-
// </div>
|
|
38
|
-
// <div class="cm-selectionLayer">
|
|
39
|
-
// <div class="cm-selectionBackground"></div>
|
|
40
|
-
// </div>
|
|
41
|
-
// <div class="cm-cursorLayer">
|
|
42
|
-
// <div class="cm-cursor"></div>
|
|
43
|
-
// </div>
|
|
44
|
-
// </div>
|
|
45
|
-
// </div>
|
|
46
|
-
//
|
|
47
|
-
|
|
48
46
|
'&': {},
|
|
49
47
|
'&.cm-focused': {
|
|
50
48
|
outline: 'none',
|
|
51
49
|
},
|
|
52
50
|
|
|
51
|
+
// Scroller.
|
|
53
52
|
// NOTE: See https://codemirror.net/docs/guide (DOM Structure).
|
|
54
53
|
'.cm-scroller': {
|
|
55
|
-
// TODO(burdon): Reconcile with docs: https://codemirror.net/docs/guide
|
|
56
|
-
// Inside of that is the scroller element. If the editor has its own scrollbar, this one should be styled with overflow: auto. But it doesn't have to—the editor also supports growing to accomodate its content, or growing up to a certain max-height and then scrolling.
|
|
57
54
|
overflowY: 'auto',
|
|
58
55
|
fontFamily: get(tokens, 'fontFamily.body', []).join(','),
|
|
59
56
|
lineHeight: 1.5,
|
|
60
57
|
},
|
|
61
58
|
|
|
59
|
+
// Content.
|
|
62
60
|
'.cm-content': {
|
|
63
|
-
|
|
64
|
-
// padding: 'unset',
|
|
61
|
+
padding: 'unset',
|
|
65
62
|
// NOTE: Base font size (otherwise defined by HTML tag, which might be different for storybook).
|
|
66
63
|
fontSize: '16px',
|
|
67
64
|
},
|
|
68
65
|
'&light .cm-content': {
|
|
69
66
|
color: get(tokens, 'extend.semanticColors.base.fg.light', 'black'),
|
|
70
|
-
caretColor: 'black',
|
|
71
67
|
},
|
|
72
68
|
'&dark .cm-content': {
|
|
73
|
-
color: get(tokens, 'extend.semanticColors.base.fg.dark', '
|
|
74
|
-
caretColor: 'white',
|
|
69
|
+
color: get(tokens, 'extend.semanticColors.base.fg.dark', 'red'),
|
|
75
70
|
},
|
|
76
71
|
|
|
77
72
|
//
|
|
@@ -103,8 +98,8 @@ export const defaultTheme: ThemeStyles = {
|
|
|
103
98
|
//
|
|
104
99
|
// gutter
|
|
105
100
|
//
|
|
106
|
-
'.cm-
|
|
107
|
-
|
|
101
|
+
'.cm-lineNumbers': {
|
|
102
|
+
minWidth: '36px',
|
|
108
103
|
},
|
|
109
104
|
|
|
110
105
|
//
|
|
@@ -150,9 +145,7 @@ export const defaultTheme: ThemeStyles = {
|
|
|
150
145
|
//
|
|
151
146
|
// tooltip
|
|
152
147
|
//
|
|
153
|
-
'.cm-tooltip': {
|
|
154
|
-
border: 'none',
|
|
155
|
-
},
|
|
148
|
+
'.cm-tooltip': {},
|
|
156
149
|
'&light .cm-tooltip': {
|
|
157
150
|
background: `${get(tokens, 'extend.colors.neutral.100')} !important`,
|
|
158
151
|
},
|
|
@@ -165,21 +158,27 @@ export const defaultTheme: ThemeStyles = {
|
|
|
165
158
|
// autocomplete
|
|
166
159
|
// https://github.com/codemirror/autocomplete/blob/main/src/completion.ts
|
|
167
160
|
//
|
|
168
|
-
'.cm-tooltip-autocomplete': {
|
|
161
|
+
'.cm-tooltip.cm-tooltip-autocomplete': {
|
|
169
162
|
marginTop: '4px',
|
|
170
163
|
marginLeft: '-3px',
|
|
171
164
|
},
|
|
172
|
-
'.cm-tooltip-autocomplete > ul': {
|
|
165
|
+
'.cm-tooltip.cm-tooltip-autocomplete > ul': {
|
|
173
166
|
maxHeight: '20em !important',
|
|
174
167
|
},
|
|
175
|
-
'.cm-tooltip-autocomplete > ul > li': {},
|
|
176
|
-
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {},
|
|
168
|
+
'.cm-tooltip.cm-tooltip-autocomplete > ul > li': {},
|
|
169
|
+
'.cm-tooltip.cm-tooltip-autocomplete > ul > li[aria-selected]': {},
|
|
177
170
|
// TODO(burdon): Can we add a class prefix to avoid adding !important?
|
|
178
171
|
'.cm-tooltip.cm-tooltip-autocomplete > ul > completion-section': {
|
|
179
172
|
paddingLeft: '4px !important',
|
|
180
173
|
borderBottom: 'none !important',
|
|
181
174
|
color: get(tokens, 'extend.colors.primary.500'),
|
|
182
175
|
},
|
|
176
|
+
'.cm-tooltip.cm-completionInfo': {
|
|
177
|
+
border: get(tokens, 'extend.colors.neutral.500'),
|
|
178
|
+
width: '360px !important',
|
|
179
|
+
margin: '-10px 1px 0 1px',
|
|
180
|
+
padding: '8px !important',
|
|
181
|
+
},
|
|
183
182
|
'.cm-completionIcon': {
|
|
184
183
|
display: 'none',
|
|
185
184
|
},
|
|
@@ -200,9 +199,8 @@ export const defaultTheme: ThemeStyles = {
|
|
|
200
199
|
},
|
|
201
200
|
'.cm-table-head': {
|
|
202
201
|
padding: '2px 16px 2px 0px',
|
|
203
|
-
borderBottom: `1px solid ${get(tokens, 'extend.colors.neutral.500')}`,
|
|
204
|
-
fontWeight: 100,
|
|
205
202
|
textAlign: 'left',
|
|
203
|
+
borderBottom: `1px solid ${get(tokens, 'extend.colors.primary.500')}`,
|
|
206
204
|
color: get(tokens, 'extend.colors.neutral.500'),
|
|
207
205
|
},
|
|
208
206
|
'.cm-table-cell': {
|
|
@@ -236,6 +234,16 @@ export const defaultTheme: ThemeStyles = {
|
|
|
236
234
|
// return acc;
|
|
237
235
|
// }, {}),
|
|
238
236
|
|
|
237
|
+
// TODO(burdon): Override vars --cm-background.
|
|
238
|
+
// https://www.npmjs.com/package/codemirror-theme-vars
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Gutters
|
|
242
|
+
*/
|
|
243
|
+
'.cm-gutters': {
|
|
244
|
+
background: 'transparent',
|
|
245
|
+
},
|
|
246
|
+
|
|
239
247
|
/**
|
|
240
248
|
* Panels
|
|
241
249
|
* TODO(burdon): Needs styling attention (esp. dark mode).
|
package/src/styles/tokens.ts
CHANGED
|
@@ -11,7 +11,6 @@ export type ThemeStyles = {
|
|
|
11
11
|
[selector: string]: StyleSpec;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
// TODO(thure): Why export the whole theme? Can this be done differently?
|
|
15
14
|
export const tokens: TailwindConfig['theme'] = tailwindConfig({}).theme;
|
|
16
15
|
|
|
17
16
|
export const getToken = (path: string, defaultValue: any = undefined) => get(tokens, path, defaultValue);
|
package/src/translations.ts
CHANGED
|
@@ -19,6 +19,8 @@ export default [
|
|
|
19
19
|
'blockquote label': 'Block quote',
|
|
20
20
|
'codeblock label': 'Code block',
|
|
21
21
|
'comment label': 'Create comment',
|
|
22
|
+
'selection overlaps existing comment label': 'Selection overlaps existing comment',
|
|
23
|
+
'select text to comment label': 'Select text to comment',
|
|
22
24
|
'image label': 'Insert image',
|
|
23
25
|
'heading label': 'Heading level',
|
|
24
26
|
'table label': 'Create table',
|