@dxos/react-ui-editor 0.4.10-main.fa5a270 → 0.4.10-main.fe71b4c
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 +706 -693
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts +1 -1
- package/dist/types/src/extensions/automerge/cursor.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +1 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/table.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +0 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/package.json +30 -29
- package/src/components/TextEditor/TextEditor.stories.tsx +20 -18
- package/src/components/TextEditor/TextEditor.tsx +6 -3
- package/src/components/Toolbar/Toolbar.stories.tsx +9 -9
- package/src/extensions/automerge/automerge.stories.tsx +7 -6
- package/src/extensions/automerge/cursor.ts +4 -37
- package/src/extensions/factories.ts +2 -2
- package/src/extensions/markdown/decorate.ts +90 -62
- package/src/extensions/markdown/formatting.ts +42 -35
- package/src/extensions/markdown/highlight.ts +3 -1
- package/src/extensions/markdown/table.ts +7 -6
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useTextEditor.ts +1 -1
- package/dist/types/src/hooks/useDocAccessor.d.ts +0 -9
- package/dist/types/src/hooks/useDocAccessor.d.ts.map +0 -1
- package/src/hooks/useDocAccessor.ts +0 -27
|
@@ -10,7 +10,8 @@ import defaultsDeep from 'lodash.defaultsdeep';
|
|
|
10
10
|
import React, { type FC, type KeyboardEvent, StrictMode, useMemo, useRef, useState } from 'react';
|
|
11
11
|
import { createRoot } from 'react-dom/client';
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { TextV0Type } from '@braneframe/types';
|
|
14
|
+
import { createDocAccessor, create, createEchoObject } from '@dxos/echo-schema';
|
|
14
15
|
import { keySymbols, parseShortcut } from '@dxos/keyboard';
|
|
15
16
|
import { PublicKey } from '@dxos/keys';
|
|
16
17
|
import { log } from '@dxos/log';
|
|
@@ -49,7 +50,6 @@ import {
|
|
|
49
50
|
type CommentsOptions,
|
|
50
51
|
type SelectionState,
|
|
51
52
|
} from '../../extensions';
|
|
52
|
-
import { useDocAccessor } from '../../hooks';
|
|
53
53
|
import translations from '../../translations';
|
|
54
54
|
|
|
55
55
|
faker.seed(101);
|
|
@@ -65,11 +65,11 @@ const text = {
|
|
|
65
65
|
//
|
|
66
66
|
'## Tasks',
|
|
67
67
|
'',
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
`- [x] ${faker.lorem.sentences()}`,
|
|
69
|
+
`- [ ] ${faker.lorem.sentences()}`,
|
|
70
|
+
` - [ ] ${faker.lorem.sentences()}`,
|
|
71
|
+
` - [ ] ${faker.lorem.sentences()}`,
|
|
72
|
+
` - [x] ${faker.lorem.sentences()}`,
|
|
73
73
|
'',
|
|
74
74
|
),
|
|
75
75
|
|
|
@@ -77,9 +77,9 @@ const text = {
|
|
|
77
77
|
//
|
|
78
78
|
'## List',
|
|
79
79
|
'',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
`- ${faker.lorem.sentences()}`,
|
|
81
|
+
`- ${faker.lorem.sentences()}`,
|
|
82
|
+
`- ${faker.lorem.sentences()}`,
|
|
83
83
|
'',
|
|
84
84
|
),
|
|
85
85
|
|
|
@@ -87,9 +87,9 @@ const text = {
|
|
|
87
87
|
//
|
|
88
88
|
'## Numbered',
|
|
89
89
|
'',
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
`1. ${faker.lorem.sentences()}`,
|
|
91
|
+
`2. ${faker.lorem.sentences()}`,
|
|
92
|
+
`3. ${faker.lorem.sentences()}`,
|
|
93
93
|
'',
|
|
94
94
|
),
|
|
95
95
|
|
|
@@ -241,7 +241,7 @@ type StoryProps = {
|
|
|
241
241
|
comments?: Comment[];
|
|
242
242
|
readonly?: boolean;
|
|
243
243
|
placeholder?: string;
|
|
244
|
-
} & Pick<TextEditorProps, 'extensions'>;
|
|
244
|
+
} & Pick<TextEditorProps, 'selection' | 'extensions'>;
|
|
245
245
|
|
|
246
246
|
const Story = ({
|
|
247
247
|
id = 'editor-' + PublicKey.random().toHex().slice(0, 8),
|
|
@@ -252,7 +252,7 @@ const Story = ({
|
|
|
252
252
|
placeholder = 'New document.',
|
|
253
253
|
...props
|
|
254
254
|
}: StoryProps) => {
|
|
255
|
-
const {
|
|
255
|
+
const [object] = useState(createEchoObject(create(TextV0Type, { content: text ?? '' })));
|
|
256
256
|
|
|
257
257
|
const viewRef = useRef<EditorView>(null);
|
|
258
258
|
useComments(viewRef.current, id, comments);
|
|
@@ -268,10 +268,10 @@ const Story = ({
|
|
|
268
268
|
editor: { className: 'min-bs-dvh px-8 bg-white dark:bg-black' },
|
|
269
269
|
},
|
|
270
270
|
}),
|
|
271
|
-
createDataExtensions({ id, text:
|
|
271
|
+
createDataExtensions({ id, text: createDocAccessor(object, ['content']) }),
|
|
272
272
|
_extensions,
|
|
273
273
|
],
|
|
274
|
-
[_extensions],
|
|
274
|
+
[_extensions, object],
|
|
275
275
|
);
|
|
276
276
|
|
|
277
277
|
return (
|
|
@@ -294,11 +294,13 @@ export default {
|
|
|
294
294
|
parameters: { translations, layout: 'fullscreen' },
|
|
295
295
|
};
|
|
296
296
|
|
|
297
|
+
// TODO(burdon): Test invalid inputs (e.g., selection).
|
|
298
|
+
|
|
297
299
|
const defaults = [
|
|
298
300
|
autocomplete({
|
|
299
301
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
300
302
|
}),
|
|
301
|
-
decorateMarkdown({ renderLinkButton }),
|
|
303
|
+
decorateMarkdown({ renderLinkButton, selectionChangeDelay: 100 }),
|
|
302
304
|
formattingKeymap(),
|
|
303
305
|
image(),
|
|
304
306
|
table(),
|
|
@@ -16,6 +16,7 @@ import React, {
|
|
|
16
16
|
} from 'react';
|
|
17
17
|
|
|
18
18
|
import { log } from '@dxos/log';
|
|
19
|
+
import { useDefaultValue } from '@dxos/react-ui';
|
|
19
20
|
import { isNotFalsy } from '@dxos/util';
|
|
20
21
|
|
|
21
22
|
import { documentId, editorMode, focusEvent } from '../../extensions';
|
|
@@ -51,12 +52,13 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
51
52
|
(
|
|
52
53
|
{
|
|
53
54
|
id,
|
|
55
|
+
// TODO(wittjosiah): Rename initialText?
|
|
54
56
|
doc,
|
|
55
57
|
selection,
|
|
56
58
|
extensions,
|
|
57
59
|
className,
|
|
58
60
|
autoFocus,
|
|
59
|
-
scrollTo
|
|
61
|
+
scrollTo: propsScrollTo,
|
|
60
62
|
moveToEndOfLine,
|
|
61
63
|
debug,
|
|
62
64
|
dataTestId,
|
|
@@ -65,6 +67,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
65
67
|
) => {
|
|
66
68
|
// NOTE: Increments by 2 in strict mode.
|
|
67
69
|
const [instanceId] = useState(() => `text-editor-${++instanceCount}`);
|
|
70
|
+
const scrollTo = useDefaultValue(propsScrollTo, EditorView.scrollIntoView(0, { yMargin: 0 }));
|
|
68
71
|
|
|
69
72
|
// TODO(burdon): Make tabster optional.
|
|
70
73
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
@@ -90,10 +93,10 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
90
93
|
//
|
|
91
94
|
// EditorState
|
|
92
95
|
// https://codemirror.net/docs/ref/#state.EditorStateConfig
|
|
96
|
+
// NOTE: Don't set selection here in case it is invalid (and crashes the state); dispatch below.
|
|
93
97
|
//
|
|
94
98
|
const state = EditorState.create({
|
|
95
99
|
doc,
|
|
96
|
-
selection,
|
|
97
100
|
extensions: [
|
|
98
101
|
id && documentId.of(id),
|
|
99
102
|
// TODO(burdon): NOTE: Doesn't catch errors in keymap functions.
|
|
@@ -150,7 +153,7 @@ export const TextEditor = forwardRef<EditorView | null, TextEditorProps>(
|
|
|
150
153
|
log('destroy', { id, instanceId });
|
|
151
154
|
view?.destroy();
|
|
152
155
|
};
|
|
153
|
-
}, [
|
|
156
|
+
}, [id, selection, scrollTo, editorMode, extensions]);
|
|
154
157
|
|
|
155
158
|
// Focus editor on Enter (e.g., when tabbing to this component).
|
|
156
159
|
const handleKeyUp = useCallback<KeyboardEventHandler<HTMLDivElement>>(
|
|
@@ -6,7 +6,8 @@ import '@dxosTheme';
|
|
|
6
6
|
|
|
7
7
|
import React, { type FC, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { TextV0Type } from '@braneframe/types';
|
|
10
|
+
import { createDocAccessor, create } from '@dxos/echo-schema';
|
|
10
11
|
import { PublicKey } from '@dxos/keys';
|
|
11
12
|
import { faker } from '@dxos/random';
|
|
12
13
|
import { Tooltip, useThemeContext } from '@dxos/react-ui';
|
|
@@ -28,26 +29,25 @@ import {
|
|
|
28
29
|
useComments,
|
|
29
30
|
useFormattingState,
|
|
30
31
|
} from '../../extensions';
|
|
31
|
-
import { useActionHandler,
|
|
32
|
+
import { useActionHandler, useTextEditor } from '../../hooks';
|
|
32
33
|
import translations from '../../translations';
|
|
33
34
|
|
|
34
35
|
faker.seed(101);
|
|
35
36
|
|
|
36
37
|
const Story: FC<{ content: string }> = ({ content }) => {
|
|
37
38
|
const { themeMode } = useThemeContext();
|
|
38
|
-
const [
|
|
39
|
-
const { id, doc, accessor } = useDocAccessor(item.text);
|
|
39
|
+
const [text] = useState(create(TextV0Type, { content }));
|
|
40
40
|
const [formattingState, formattingObserver] = useFormattingState();
|
|
41
41
|
const { parentRef, view } = useTextEditor(() => {
|
|
42
42
|
return {
|
|
43
|
-
id,
|
|
44
|
-
doc,
|
|
43
|
+
id: text.id,
|
|
44
|
+
doc: text.content,
|
|
45
45
|
extensions: [
|
|
46
46
|
formattingObserver,
|
|
47
47
|
createBasicExtensions(),
|
|
48
48
|
createMarkdownExtensions({ themeMode }),
|
|
49
49
|
createThemeExtensions({ themeMode, slots: { editor: { className: 'p-2' } } }),
|
|
50
|
-
createDataExtensions({ id, text:
|
|
50
|
+
createDataExtensions({ id: text.id, text: createDocAccessor(text, ['content']) }),
|
|
51
51
|
comments({
|
|
52
52
|
onCreate: ({ cursor }) => {
|
|
53
53
|
const id = PublicKey.random().toHex();
|
|
@@ -61,12 +61,12 @@ const Story: FC<{ content: string }> = ({ content }) => {
|
|
|
61
61
|
table(),
|
|
62
62
|
],
|
|
63
63
|
};
|
|
64
|
-
}, [
|
|
64
|
+
}, [text, formattingObserver, themeMode]);
|
|
65
65
|
|
|
66
66
|
const handleAction = useActionHandler(view);
|
|
67
67
|
|
|
68
68
|
const [_comments, setComments] = useState<Comment[]>([]);
|
|
69
|
-
useComments(view, id, _comments);
|
|
69
|
+
useComments(view, text.id, _comments);
|
|
70
70
|
|
|
71
71
|
return (
|
|
72
72
|
<Tooltip.Provider>
|
|
@@ -8,10 +8,11 @@ import { BroadcastChannelNetworkAdapter } from '@automerge/automerge-repo-networ
|
|
|
8
8
|
import '@preact/signals-react';
|
|
9
9
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
10
10
|
|
|
11
|
+
import { TextV0Type } from '@braneframe/types';
|
|
11
12
|
import { Repo } from '@dxos/automerge/automerge-repo';
|
|
12
|
-
import {
|
|
13
|
+
import { Filter, DocAccessor, create, createDocAccessor, type Expando } from '@dxos/echo-schema';
|
|
13
14
|
import { type PublicKey } from '@dxos/keys';
|
|
14
|
-
import {
|
|
15
|
+
import { useSpace } from '@dxos/react-client/echo';
|
|
15
16
|
import { ClientRepeater } from '@dxos/react-client/testing';
|
|
16
17
|
import { useThemeContext } from '@dxos/react-ui';
|
|
17
18
|
import { withTheme } from '@dxos/storybook-utils';
|
|
@@ -97,9 +98,9 @@ const EchoStory = ({ spaceKey }: { spaceKey: PublicKey }) => {
|
|
|
97
98
|
// const identity = useIdentity();
|
|
98
99
|
const space = useSpace(spaceKey);
|
|
99
100
|
const source = useMemo<DocAccessor | undefined>(() => {
|
|
100
|
-
const { objects = [] } = space?.db.query(Filter.from({ type: 'test' })) ?? {};
|
|
101
|
+
const { objects = [] } = space?.db.query<Expando>(Filter.from({ type: 'test' })) ?? {};
|
|
101
102
|
if (objects.length) {
|
|
102
|
-
return createDocAccessor(objects[0].content);
|
|
103
|
+
return createDocAccessor(objects[0].content, ['content']);
|
|
103
104
|
}
|
|
104
105
|
}, [space]);
|
|
105
106
|
|
|
@@ -124,9 +125,9 @@ export const WithEcho = {
|
|
|
124
125
|
createSpace
|
|
125
126
|
onCreateSpace={async (space) => {
|
|
126
127
|
space.db.add(
|
|
127
|
-
|
|
128
|
+
create({
|
|
128
129
|
type: 'test',
|
|
129
|
-
content:
|
|
130
|
+
content: create(TextV0Type, { content: initialContent }),
|
|
130
131
|
}),
|
|
131
132
|
);
|
|
132
133
|
}}
|
|
@@ -2,30 +2,16 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import { next as A } from '@dxos/automerge/automerge';
|
|
8
|
-
import { type DocAccessor } from '@dxos/echo-schema';
|
|
5
|
+
import { toCursor, type DocAccessor, fromCursor } from '@dxos/echo-schema';
|
|
9
6
|
import { log } from '@dxos/log';
|
|
10
7
|
|
|
11
8
|
import { type CursorConverter } from '../cursor';
|
|
12
9
|
|
|
13
|
-
export const cursorConverter = (
|
|
10
|
+
export const cursorConverter = (accessor: DocAccessor): CursorConverter => ({
|
|
14
11
|
// TODO(burdon): Handle assoc to associate with a previous character.
|
|
15
12
|
toCursor: (pos) => {
|
|
16
|
-
const doc = handle.docSync();
|
|
17
|
-
if (!doc) {
|
|
18
|
-
return '';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const value = get(doc, path);
|
|
22
|
-
if (typeof value === 'string' && value.length <= pos) {
|
|
23
|
-
return 'end';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
13
|
try {
|
|
27
|
-
|
|
28
|
-
return A.getCursor(doc, path.slice(), pos);
|
|
14
|
+
return toCursor(accessor, pos);
|
|
29
15
|
} catch (err) {
|
|
30
16
|
log.catch(err);
|
|
31
17
|
return ''; // In case of invalid request (e.g., wrong document).
|
|
@@ -33,27 +19,8 @@ export const cursorConverter = ({ handle, path }: DocAccessor): CursorConverter
|
|
|
33
19
|
},
|
|
34
20
|
|
|
35
21
|
fromCursor: (cursor) => {
|
|
36
|
-
if (cursor === '') {
|
|
37
|
-
return 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const doc = handle.docSync();
|
|
41
|
-
if (!doc) {
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (cursor === 'end') {
|
|
46
|
-
const value = get(doc, path);
|
|
47
|
-
if (typeof value === 'string') {
|
|
48
|
-
return value.length;
|
|
49
|
-
} else {
|
|
50
|
-
return 0;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
22
|
try {
|
|
55
|
-
|
|
56
|
-
return A.getCursorPosition(doc, path.slice(), cursor);
|
|
23
|
+
return fromCursor(accessor, cursor);
|
|
57
24
|
} catch (err) {
|
|
58
25
|
log.catch(err);
|
|
59
26
|
return 0; // In case of invalid request (e.g., wrong document).
|
|
@@ -162,14 +162,14 @@ export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: Theme
|
|
|
162
162
|
|
|
163
163
|
export type DataExtensionsProps = {
|
|
164
164
|
id: string;
|
|
165
|
-
text
|
|
165
|
+
text?: DocAccessor;
|
|
166
166
|
space?: Space;
|
|
167
167
|
identity?: Identity | null;
|
|
168
168
|
};
|
|
169
169
|
|
|
170
170
|
// TODO(burdon): Move out of react-ui-editor (remove echo deps).
|
|
171
171
|
export const createDataExtensions = ({ id, text, space, identity }: DataExtensionsProps): Extension[] => {
|
|
172
|
-
const extensions: Extension[] = [automerge(text)];
|
|
172
|
+
const extensions: Extension[] = text ? [automerge(text)] : [];
|
|
173
173
|
|
|
174
174
|
if (space && identity) {
|
|
175
175
|
const peerId = identity?.identityKey.toHex();
|
|
@@ -81,6 +81,7 @@ const horizontalRule = Decoration.replace({ widget: new HorizontalRuleWidget() }
|
|
|
81
81
|
const checkedTask = Decoration.replace({ widget: new CheckboxWidget(true) });
|
|
82
82
|
const uncheckedTask = Decoration.replace({ widget: new CheckboxWidget(false) });
|
|
83
83
|
|
|
84
|
+
// Check if cursor is inside text.
|
|
84
85
|
const editingRange = (state: EditorState, range: { from: number; to: number }, focus: boolean) => {
|
|
85
86
|
const {
|
|
86
87
|
readOnly,
|
|
@@ -103,76 +104,103 @@ const buildDecorations = (view: EditorView, options: DecorateOptions, focus: boo
|
|
|
103
104
|
from,
|
|
104
105
|
to,
|
|
105
106
|
enter: (node) => {
|
|
106
|
-
|
|
107
|
-
const editing = editingRange(state, node, focus);
|
|
107
|
+
switch (node.name) {
|
|
108
108
|
// FencedCode > CodeMark > [CodeInfo] > CodeText > CodeMark
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
case 'FencedCode': {
|
|
110
|
+
const editing = editingRange(state, node, focus);
|
|
111
|
+
for (const block of view.viewportLineBlocks) {
|
|
112
|
+
if (block.to < node.from) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (block.from > node.to) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
const first = block.from <= node.from;
|
|
119
|
+
const last = block.to >= node.to && /^(\s>)*```$/.test(state.doc.sliceString(block.from, block.to));
|
|
120
|
+
deco.add(
|
|
121
|
+
block.from,
|
|
122
|
+
block.from,
|
|
123
|
+
first ? fencedCodeLineFirst : last ? fencedCodeLineLast : fencedCodeLine,
|
|
124
|
+
);
|
|
125
|
+
if (!editing && (first || last)) {
|
|
126
|
+
atomicDeco.add(block.from, block.to, hide);
|
|
127
|
+
}
|
|
121
128
|
}
|
|
129
|
+
return false;
|
|
122
130
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
marks[1].from,
|
|
136
|
-
Decoration.mark({
|
|
137
|
-
tagName: 'a',
|
|
138
|
-
attributes: {
|
|
139
|
-
class: 'cm-link',
|
|
140
|
-
href: url,
|
|
141
|
-
rel: 'noreferrer',
|
|
142
|
-
target: '_blank',
|
|
143
|
-
},
|
|
144
|
-
}),
|
|
145
|
-
);
|
|
146
|
-
if (!editing) {
|
|
147
|
-
atomicDeco.add(
|
|
131
|
+
|
|
132
|
+
case 'Link': {
|
|
133
|
+
const marks = node.node.getChildren('LinkMark');
|
|
134
|
+
const urlNode = node.node.getChild('URL');
|
|
135
|
+
const editing = editingRange(state, node, focus);
|
|
136
|
+
if (urlNode && marks.length >= 2) {
|
|
137
|
+
const url = state.sliceDoc(urlNode.from, urlNode.to);
|
|
138
|
+
if (!editing) {
|
|
139
|
+
atomicDeco.add(node.from, marks[0].to, hide);
|
|
140
|
+
}
|
|
141
|
+
deco.add(
|
|
142
|
+
marks[0].to,
|
|
148
143
|
marks[1].from,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
144
|
+
Decoration.mark({
|
|
145
|
+
tagName: 'a',
|
|
146
|
+
attributes: {
|
|
147
|
+
class: 'cm-link',
|
|
148
|
+
href: url,
|
|
149
|
+
rel: 'noreferrer',
|
|
150
|
+
target: '_blank',
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
153
|
);
|
|
154
|
+
if (!editing) {
|
|
155
|
+
atomicDeco.add(
|
|
156
|
+
marks[1].from,
|
|
157
|
+
node.to,
|
|
158
|
+
options.renderLinkButton
|
|
159
|
+
? Decoration.replace({ widget: new LinkButton(url, options.renderLinkButton) })
|
|
160
|
+
: hide,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case 'HeaderMark': {
|
|
168
|
+
const parent = node.node.parent!;
|
|
169
|
+
if (/^ATX/.test(parent.name) && !editingRange(state, state.doc.lineAt(node.from), focus)) {
|
|
170
|
+
const next = state.doc.sliceString(node.to, node.to + 1);
|
|
171
|
+
atomicDeco.add(node.from, node.to + (next === ' ' ? 1 : 0), hide);
|
|
154
172
|
}
|
|
173
|
+
break;
|
|
155
174
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
175
|
+
|
|
176
|
+
case 'HorizontalRule': {
|
|
177
|
+
if (!editingRange(state, node, focus)) {
|
|
178
|
+
deco.add(node.from, node.to, horizontalRule);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
161
181
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
182
|
+
|
|
183
|
+
case 'TaskMarker': {
|
|
184
|
+
if (!editingRange(state, node, focus)) {
|
|
185
|
+
const checked = state.doc.sliceString(node.from + 1, node.to - 1) === 'x';
|
|
186
|
+
atomicDeco.add(node.from - 2, node.from - 1, Decoration.mark({ class: 'cm-task' }));
|
|
187
|
+
atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
165
190
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
atomicDeco.add(node.from, node.to, checked ? checkedTask : uncheckedTask);
|
|
191
|
+
|
|
192
|
+
case 'ListItem': {
|
|
193
|
+
const start = state.doc.lineAt(node.from);
|
|
194
|
+
deco.add(start.from, start.from, Decoration.line({ class: 'cm-list-item' }));
|
|
195
|
+
break;
|
|
172
196
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
197
|
+
|
|
198
|
+
default: {
|
|
199
|
+
if (MarksByParent.has(node.name)) {
|
|
200
|
+
if (!editingRange(state, node.node.parent!, focus)) {
|
|
201
|
+
atomicDeco.add(node.from, node.to, hide);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
176
204
|
}
|
|
177
205
|
}
|
|
178
206
|
},
|
|
@@ -224,8 +252,6 @@ export const decorateMarkdown = (options: DecorateOptions = {}) => {
|
|
|
224
252
|
}
|
|
225
253
|
}
|
|
226
254
|
|
|
227
|
-
// TODO(burdon): BUG: If the cursor is at the end of a link at the end of a line,
|
|
228
|
-
// the cursor will float in space or be in the wrong position after the decoration is applied.
|
|
229
255
|
scheduleUpdate(view: EditorView) {
|
|
230
256
|
this.clearUpdate();
|
|
231
257
|
this.pendingUpdate = setTimeout(() => {
|
|
@@ -304,4 +330,6 @@ const formattingStyles = EditorView.baseTheme({
|
|
|
304
330
|
marginLeft: '4px',
|
|
305
331
|
marginRight: '4px',
|
|
306
332
|
},
|
|
333
|
+
|
|
334
|
+
// '& .cm-list-item > span:nth-child(2)': {},
|
|
307
335
|
});
|