@dxos/react-ui-editor 0.4.8-main.6919ef3 → 0.4.8-main.6dbc1f1
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 +1388 -1298
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +1 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +12 -11
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +4 -2
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts +1 -0
- package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/extensions/dnd.d.ts +7 -1
- package/dist/types/src/extensions/dnd.d.ts.map +1 -1
- package/dist/types/src/extensions/factories.d.ts +5 -1
- package/dist/types/src/extensions/factories.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +0 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/action.d.ts +9 -0
- package/dist/types/src/extensions/markdown/action.d.ts.map +1 -0
- package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +7 -4
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/image.d.ts +6 -0
- package/dist/types/src/extensions/markdown/image.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/index.d.ts +1 -0
- package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/extensions/modes.d.ts +1 -1
- package/dist/types/src/extensions/modes.d.ts.map +1 -1
- package/dist/types/src/hooks/useActionHandler.d.ts +2 -2
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useDocAccessor.d.ts +4 -2
- package/dist/types/src/hooks/useDocAccessor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextEditor.stories.d.ts +1 -0
- package/dist/types/src/hooks/useTextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/themes/default.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/package.json +26 -24
- package/src/components/TextEditor/TextEditor.stories.tsx +14 -2
- package/src/components/TextEditor/TextEditor.tsx +2 -2
- package/src/components/Toolbar/Toolbar.stories.tsx +23 -24
- package/src/components/Toolbar/Toolbar.tsx +102 -60
- package/src/extensions/automerge/automerge.ts +4 -4
- package/src/extensions/command/state.ts +2 -2
- package/src/extensions/comments.ts +2 -2
- package/src/extensions/dnd.ts +31 -9
- package/src/extensions/factories.ts +41 -15
- package/src/extensions/index.ts +0 -1
- package/src/extensions/markdown/action.ts +112 -0
- package/src/extensions/markdown/bundle.ts +8 -11
- package/src/extensions/markdown/formatting.test.ts +13 -13
- package/src/extensions/markdown/formatting.ts +95 -87
- package/src/extensions/markdown/image.ts +6 -0
- package/src/extensions/markdown/index.ts +1 -0
- package/src/extensions/modes.ts +8 -1
- package/src/hooks/useActionHandler.ts +4 -85
- package/src/hooks/useDocAccessor.ts +8 -2
- package/src/hooks/useTextEditor.stories.tsx +3 -3
- package/src/hooks/useTextEditor.ts +3 -0
- package/src/themes/default.ts +24 -8
- package/src/translations.ts +1 -0
- package/dist/types/src/extensions/cursor-line-margin.d.ts +0 -7
- package/dist/types/src/extensions/cursor-line-margin.d.ts.map +0 -1
- package/src/extensions/cursor-line-margin.ts +0 -94
|
@@ -508,14 +508,14 @@ export const focusComment = (view: EditorView, id: string, center = true) => {
|
|
|
508
508
|
/**
|
|
509
509
|
* Update comments state field.
|
|
510
510
|
*/
|
|
511
|
-
export const useComments = (view: EditorView | null | undefined, id: string, comments
|
|
511
|
+
export const useComments = (view: EditorView | null | undefined, id: string, comments?: Comment[]) => {
|
|
512
512
|
useEffect(() => {
|
|
513
513
|
if (view) {
|
|
514
514
|
// Check same document.
|
|
515
515
|
// NOTE: Hook might be called before editor state is updated.
|
|
516
516
|
if (id === view.state.facet(documentId)) {
|
|
517
517
|
view.dispatch({
|
|
518
|
-
effects: setComments.of({ id, comments }),
|
|
518
|
+
effects: setComments.of({ id, comments: comments ?? [] }),
|
|
519
519
|
});
|
|
520
520
|
}
|
|
521
521
|
}
|
package/src/extensions/dnd.ts
CHANGED
|
@@ -3,15 +3,37 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import type { Extension } from '@codemirror/state';
|
|
6
|
-
import { EditorView } from '@codemirror/view';
|
|
6
|
+
import { dropCursor, EditorView } from '@codemirror/view';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { getToken } from '../styles';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
export type DNDOptions = { onDrop?: (view: EditorView, event: { files: FileList }) => void };
|
|
11
|
+
|
|
12
|
+
const styles = EditorView.baseTheme({
|
|
13
|
+
'.cm-dropCursor': {
|
|
14
|
+
borderLeft: `2px solid ${getToken('extend.colors.primary.500')}`,
|
|
15
|
+
color: getToken('extend.colors.primary.500'),
|
|
16
|
+
padding: '0 4px',
|
|
17
|
+
},
|
|
18
|
+
'.cm-dropCursor:after': {
|
|
19
|
+
content: '"←"',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const dropFile = (options: DNDOptions = {}): Extension => {
|
|
24
|
+
return [
|
|
25
|
+
styles,
|
|
26
|
+
dropCursor(),
|
|
27
|
+
EditorView.domEventHandlers({
|
|
28
|
+
drop: (event, view) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
const files = event.dataTransfer?.files;
|
|
31
|
+
const pos = view.posAtCoords(event);
|
|
32
|
+
if (files?.length && pos !== null) {
|
|
33
|
+
view.dispatch({ selection: { anchor: pos } });
|
|
34
|
+
options.onDrop?.(view, { files });
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
];
|
|
17
39
|
};
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { closeBrackets } from '@codemirror/autocomplete';
|
|
6
|
-
import { history } from '@codemirror/commands';
|
|
5
|
+
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
|
|
7
7
|
import { bracketMatching } from '@codemirror/language';
|
|
8
|
+
import { searchKeymap } from '@codemirror/search';
|
|
8
9
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
9
10
|
import {
|
|
10
11
|
EditorView,
|
|
11
|
-
|
|
12
|
+
type KeyBinding,
|
|
12
13
|
drawSelection,
|
|
13
14
|
dropCursor,
|
|
14
15
|
highlightActiveLine,
|
|
16
|
+
keymap,
|
|
15
17
|
lineNumbers,
|
|
16
18
|
placeholder,
|
|
17
19
|
scrollPastEnd,
|
|
@@ -32,8 +34,6 @@ import { awareness, SpaceAwarenessProvider } from './awareness';
|
|
|
32
34
|
import { type ThemeStyles } from '../styles';
|
|
33
35
|
import { defaultTheme } from '../themes';
|
|
34
36
|
|
|
35
|
-
// TODO(burdon): Move into extensions folder.
|
|
36
|
-
|
|
37
37
|
//
|
|
38
38
|
// Basic
|
|
39
39
|
//
|
|
@@ -41,39 +41,52 @@ import { defaultTheme } from '../themes';
|
|
|
41
41
|
/**
|
|
42
42
|
* https://codemirror.net/docs/extensions
|
|
43
43
|
* https://github.com/codemirror/basic-setup
|
|
44
|
+
* https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
|
|
44
45
|
*/
|
|
45
|
-
// TODO(burdon): Reconcile with createMarkdownExtensions.
|
|
46
46
|
export type BasicExtensionsOptions = {
|
|
47
47
|
allowMultipleSelections?: boolean;
|
|
48
48
|
bracketMatching?: boolean;
|
|
49
49
|
closeBrackets?: boolean;
|
|
50
|
-
crosshairCursor?: boolean;
|
|
51
50
|
dropCursor?: boolean;
|
|
52
51
|
drawSelection?: boolean;
|
|
53
52
|
editable?: boolean;
|
|
54
53
|
highlightActiveLine?: boolean;
|
|
55
54
|
history?: boolean;
|
|
55
|
+
indentWithTab?: boolean;
|
|
56
|
+
keymap?: null | 'default' | 'standard';
|
|
56
57
|
lineNumbers?: boolean;
|
|
57
58
|
lineWrapping?: boolean;
|
|
58
59
|
placeholder?: string;
|
|
59
60
|
readonly?: boolean;
|
|
61
|
+
search?: boolean;
|
|
60
62
|
scrollPastEnd?: boolean;
|
|
63
|
+
standardKeymap?: boolean;
|
|
61
64
|
tabSize?: number;
|
|
62
65
|
};
|
|
63
66
|
|
|
64
|
-
const
|
|
67
|
+
const defaultBasicOptions: BasicExtensionsOptions = {
|
|
68
|
+
allowMultipleSelections: true,
|
|
65
69
|
bracketMatching: true,
|
|
66
70
|
closeBrackets: true,
|
|
67
71
|
drawSelection: true,
|
|
68
72
|
editable: true,
|
|
69
73
|
history: true,
|
|
74
|
+
keymap: 'standard',
|
|
70
75
|
lineWrapping: true,
|
|
76
|
+
search: true,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const keymaps: { [key: string]: readonly KeyBinding[] } = {
|
|
80
|
+
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
81
|
+
standard: standardKeymap,
|
|
82
|
+
// https://codemirror.net/docs/ref/#commands.defaultKeymap
|
|
83
|
+
default: defaultKeymap,
|
|
71
84
|
};
|
|
72
85
|
|
|
73
86
|
export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
|
|
74
|
-
const props: BasicExtensionsOptions = defaultsDeep({}, _props,
|
|
87
|
+
const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaultBasicOptions);
|
|
75
88
|
return [
|
|
76
|
-
//
|
|
89
|
+
// NOTE: Doesn't catch errors in keymap functions.
|
|
77
90
|
EditorView.exceptionSink.of((err) => {
|
|
78
91
|
log.catch(err);
|
|
79
92
|
}),
|
|
@@ -81,7 +94,6 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
81
94
|
props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
|
|
82
95
|
props.bracketMatching && bracketMatching(),
|
|
83
96
|
props.closeBrackets && closeBrackets(),
|
|
84
|
-
props.crosshairCursor && crosshairCursor(),
|
|
85
97
|
props.dropCursor && dropCursor(),
|
|
86
98
|
props.drawSelection && drawSelection(),
|
|
87
99
|
props.highlightActiveLine && highlightActiveLine(),
|
|
@@ -92,6 +104,21 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
92
104
|
props.readonly && [EditorState.readOnly.of(true), EditorView.editable.of(false)],
|
|
93
105
|
props.scrollPastEnd && scrollPastEnd(),
|
|
94
106
|
props.tabSize && EditorState.tabSize.of(props.tabSize),
|
|
107
|
+
|
|
108
|
+
// https://codemirror.net/docs/ref/#view.KeyBinding
|
|
109
|
+
keymap.of(
|
|
110
|
+
[
|
|
111
|
+
...((props.keymap && keymaps[props.keymap]) ?? []),
|
|
112
|
+
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
113
|
+
...(props.indentWithTab ? [indentWithTab] : []),
|
|
114
|
+
// https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
|
|
115
|
+
...(props.closeBrackets ? closeBracketsKeymap : []),
|
|
116
|
+
// https://codemirror.net/docs/ref/#commands.historyKeymap
|
|
117
|
+
...(props.history ? historyKeymap : []),
|
|
118
|
+
// https://codemirror.net/docs/ref/#search.searchKeymap
|
|
119
|
+
...(props.search ? searchKeymap : []),
|
|
120
|
+
].filter(isNotFalsy),
|
|
121
|
+
),
|
|
95
122
|
].filter(isNotFalsy);
|
|
96
123
|
};
|
|
97
124
|
|
|
@@ -112,14 +139,14 @@ export type ThemeExtensionsOptions = {
|
|
|
112
139
|
};
|
|
113
140
|
};
|
|
114
141
|
|
|
115
|
-
const
|
|
142
|
+
const defaultThemeSlots = {
|
|
116
143
|
editor: {
|
|
117
144
|
className: 'w-full bs-full',
|
|
118
145
|
},
|
|
119
146
|
};
|
|
120
147
|
|
|
121
148
|
export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
|
|
122
|
-
const slots = defaultsDeep({}, _slots,
|
|
149
|
+
const slots = defaultsDeep({}, _slots, defaultThemeSlots);
|
|
123
150
|
return [
|
|
124
151
|
EditorView.baseTheme(defaultTheme),
|
|
125
152
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
@@ -135,12 +162,11 @@ export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: Theme
|
|
|
135
162
|
|
|
136
163
|
export type DataExtensionsProps = {
|
|
137
164
|
id: string;
|
|
138
|
-
text: DocAccessor;
|
|
165
|
+
text: DocAccessor;
|
|
139
166
|
space?: Space;
|
|
140
167
|
identity?: Identity | null;
|
|
141
168
|
};
|
|
142
169
|
|
|
143
|
-
// TODO(burdon): Factor out automerge defs and extension (not hook).
|
|
144
170
|
// TODO(burdon): Move out of react-ui-editor (remove echo deps).
|
|
145
171
|
export const createDataExtensions = ({ id, text, space, identity }: DataExtensionsProps): Extension[] => {
|
|
146
172
|
const extensions: Extension[] = [automerge(text)];
|
package/src/extensions/index.ts
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Inline,
|
|
9
|
+
List,
|
|
10
|
+
addBlockquote,
|
|
11
|
+
addCodeblock,
|
|
12
|
+
addLink,
|
|
13
|
+
addList,
|
|
14
|
+
insertTable,
|
|
15
|
+
removeBlockquote,
|
|
16
|
+
removeCodeblock,
|
|
17
|
+
removeLink,
|
|
18
|
+
removeList,
|
|
19
|
+
setHeading,
|
|
20
|
+
setStyle,
|
|
21
|
+
toggleBlockquote,
|
|
22
|
+
toggleList,
|
|
23
|
+
toggleStyle,
|
|
24
|
+
} from './formatting';
|
|
25
|
+
import { createComment } from '../comments';
|
|
26
|
+
|
|
27
|
+
export type ActionType =
|
|
28
|
+
| 'blockquote'
|
|
29
|
+
| 'strong'
|
|
30
|
+
| 'codeblock'
|
|
31
|
+
| 'comment'
|
|
32
|
+
| 'heading'
|
|
33
|
+
| 'image'
|
|
34
|
+
| 'emphasis'
|
|
35
|
+
| 'code'
|
|
36
|
+
| 'link'
|
|
37
|
+
| 'list-bullet'
|
|
38
|
+
| 'list-ordered'
|
|
39
|
+
| 'list-task'
|
|
40
|
+
| 'mention'
|
|
41
|
+
| 'prompt'
|
|
42
|
+
| 'strikethrough'
|
|
43
|
+
| 'table';
|
|
44
|
+
|
|
45
|
+
export type Action = {
|
|
46
|
+
type: ActionType;
|
|
47
|
+
data?: any;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ActionHandler = (view: EditorView, action: Action) => void;
|
|
51
|
+
|
|
52
|
+
export const processAction: ActionHandler = (view, action) => {
|
|
53
|
+
let inlineType, listType;
|
|
54
|
+
switch (action.type) {
|
|
55
|
+
case 'heading':
|
|
56
|
+
setHeading(parseInt(action.data))(view);
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case 'strong':
|
|
60
|
+
case 'emphasis':
|
|
61
|
+
case 'strikethrough':
|
|
62
|
+
case 'code':
|
|
63
|
+
inlineType =
|
|
64
|
+
action.type === 'strong'
|
|
65
|
+
? Inline.Strong
|
|
66
|
+
: action.type === 'emphasis'
|
|
67
|
+
? Inline.Emphasis
|
|
68
|
+
: action.type === 'strikethrough'
|
|
69
|
+
? Inline.Strikethrough
|
|
70
|
+
: Inline.Code;
|
|
71
|
+
(typeof action.data === 'boolean' ? setStyle(inlineType, action.data) : toggleStyle(inlineType))(view);
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 'list-ordered':
|
|
75
|
+
case 'list-bullet':
|
|
76
|
+
case 'list-task':
|
|
77
|
+
listType =
|
|
78
|
+
action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
|
|
79
|
+
(action.data === false ? removeList(listType) : action.data === true ? addList(listType) : toggleList(listType))(
|
|
80
|
+
view,
|
|
81
|
+
);
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'blockquote':
|
|
85
|
+
(action.data === false ? removeBlockquote : action.data === true ? addBlockquote : toggleBlockquote)(view);
|
|
86
|
+
break;
|
|
87
|
+
case 'codeblock':
|
|
88
|
+
(action.data === false ? removeCodeblock : addCodeblock)(view);
|
|
89
|
+
break;
|
|
90
|
+
case 'table':
|
|
91
|
+
insertTable(view);
|
|
92
|
+
break;
|
|
93
|
+
|
|
94
|
+
case 'link':
|
|
95
|
+
(action.data === false ? removeLink : addLink())(view);
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'image':
|
|
99
|
+
addLink({ url: action.data, image: true })(view);
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case 'comment':
|
|
103
|
+
createComment(view);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
requestAnimationFrame(() => {
|
|
108
|
+
if (!view.hasFocus) {
|
|
109
|
+
view.focus();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
};
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { completionKeymap } from '@codemirror/autocomplete';
|
|
6
|
+
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
|
7
7
|
import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
|
|
8
8
|
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
9
9
|
import { languages } from '@codemirror/language-data';
|
|
10
|
-
import {
|
|
10
|
+
import { lintKeymap } from '@codemirror/lint';
|
|
11
11
|
import { type Extension } from '@codemirror/state';
|
|
12
12
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
13
13
|
import { keymap } from '@codemirror/view';
|
|
@@ -60,14 +60,11 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
|
|
|
60
60
|
keymap.of([
|
|
61
61
|
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
62
62
|
indentWithTab,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
...
|
|
67
|
-
|
|
68
|
-
...searchKeymap,
|
|
69
|
-
// https://codemirror.net/docs/ref/#commands.standardKeymap
|
|
70
|
-
...standardKeymap,
|
|
63
|
+
|
|
64
|
+
// https://codemirror.net/docs/ref/#commands.defaultKeymap
|
|
65
|
+
...defaultKeymap,
|
|
66
|
+
...completionKeymap,
|
|
67
|
+
...lintKeymap,
|
|
71
68
|
]),
|
|
72
69
|
];
|
|
73
70
|
};
|
|
@@ -9,20 +9,20 @@ import { expect } from 'chai';
|
|
|
9
9
|
import { describe, test } from '@dxos/test';
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
addBlockquote,
|
|
13
|
+
addCodeblock,
|
|
14
|
+
addLink,
|
|
15
|
+
addList,
|
|
13
16
|
addStyle,
|
|
17
|
+
getFormatting,
|
|
14
18
|
removeStyle,
|
|
15
|
-
addLink,
|
|
16
19
|
removeLink,
|
|
17
|
-
addList,
|
|
18
20
|
removeList,
|
|
19
|
-
addBlockquote,
|
|
20
21
|
removeBlockquote,
|
|
21
|
-
addCodeblock,
|
|
22
22
|
removeCodeblock,
|
|
23
|
+
setHeading,
|
|
23
24
|
Inline,
|
|
24
25
|
List,
|
|
25
|
-
getFormatting,
|
|
26
26
|
type Formatting,
|
|
27
27
|
} from './formatting';
|
|
28
28
|
|
|
@@ -204,22 +204,22 @@ describe('removeStyle', () => {
|
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
describe('addLink', () => {
|
|
207
|
-
testCommand('adds a link', '{}', addLink, '[]({})');
|
|
207
|
+
testCommand('adds a link', '{}', addLink(), '[]({})');
|
|
208
208
|
|
|
209
|
-
testCommand('adds a link around text', 'hello {world}', addLink, 'hello [world]({})');
|
|
209
|
+
testCommand('adds a link around text', 'hello {world}', addLink(), 'hello [world]({})');
|
|
210
210
|
|
|
211
|
-
testCommand('clears existing links', '[hello {world}](foo)', addLink, 'hello [world]({})');
|
|
211
|
+
testCommand('clears existing links', '[hello {world}](foo)', addLink(), 'hello [world]({})');
|
|
212
212
|
|
|
213
|
-
testCommand('does nothing across blocks', '{one\n\ntwo}', addLink, null);
|
|
213
|
+
testCommand('does nothing across blocks', '{one\n\ntwo}', addLink(), null);
|
|
214
214
|
|
|
215
|
-
testCommand('does nothing in code blocks', '```\n{one}\n```', addLink, null);
|
|
215
|
+
testCommand('does nothing in code blocks', '```\n{one}\n```', addLink(), null);
|
|
216
216
|
|
|
217
|
-
testCommand('patches up overlapping styles before', '*foo {bar* baz}', addLink, '*foo* [*bar* baz]({})');
|
|
217
|
+
testCommand('patches up overlapping styles before', '*foo {bar* baz}', addLink(), '*foo* [*bar* baz]({})');
|
|
218
218
|
|
|
219
219
|
testCommand(
|
|
220
220
|
'patches up overlapping styles after',
|
|
221
221
|
'one {two ~~three} four~~',
|
|
222
|
-
addLink,
|
|
222
|
+
addLink(),
|
|
223
223
|
'one [two ~~three~~]({}) ~~four~~',
|
|
224
224
|
);
|
|
225
225
|
});
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from '@codemirror/state';
|
|
16
16
|
import { EditorView, keymap } from '@codemirror/view';
|
|
17
17
|
import { type SyntaxNodeRef, type SyntaxNode } from '@lezer/common';
|
|
18
|
-
import {
|
|
18
|
+
import { useMemo, useState } from 'react';
|
|
19
19
|
|
|
20
20
|
// Markdown refs:
|
|
21
21
|
// https://github.github.com/gfm
|
|
@@ -56,7 +56,7 @@ export type Formatting = {
|
|
|
56
56
|
listStyle: null | 'ordered' | 'bullet' | 'task';
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
export const
|
|
59
|
+
export const formattingEquals = (a: Formatting, b: Formatting) =>
|
|
60
60
|
a.blockType === b.blockType &&
|
|
61
61
|
a.strong === b.strong &&
|
|
62
62
|
a.emphasis === b.emphasis &&
|
|
@@ -470,90 +470,94 @@ export const removeLink: StateCommand = ({ state, dispatch }) => {
|
|
|
470
470
|
};
|
|
471
471
|
|
|
472
472
|
// Add link markup around the selection
|
|
473
|
-
export const addLink
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
473
|
+
export const addLink =
|
|
474
|
+
({ url, image }: { url?: string; image?: boolean } = {}): StateCommand =>
|
|
475
|
+
({ state, dispatch }) => {
|
|
476
|
+
const changes = state.changeByRange((range) => {
|
|
477
|
+
let { from, to } = range;
|
|
478
|
+
const cutStyles: SyntaxNode[] = [];
|
|
479
|
+
let okay: boolean | null = null;
|
|
480
|
+
// Check whether this range is in a position where a link makes sense.
|
|
481
|
+
syntaxTree(state).iterate({
|
|
482
|
+
from,
|
|
483
|
+
to,
|
|
484
|
+
enter: (node) => {
|
|
485
|
+
if (Object.hasOwn(Textblocks, node.name)) {
|
|
486
|
+
// If the selection spans multiple textblocks or is in a code block, abort.
|
|
487
|
+
okay =
|
|
488
|
+
Textblocks[node.name] !== 'codeblock' &&
|
|
489
|
+
from >= blockContentStart(node) &&
|
|
490
|
+
to <= blockContentEnd(node, state.doc);
|
|
491
|
+
} else if (Object.hasOwn(InlineMarker, node.name)) {
|
|
492
|
+
// Look for inline styles that partially overlap the range.
|
|
493
|
+
// Expand the range over them if they start directly outside, otherwise mark them for later.
|
|
494
|
+
const sNode = node.node;
|
|
495
|
+
if (node.from < from && node.to <= to) {
|
|
496
|
+
if (sNode.firstChild!.to === from) {
|
|
497
|
+
from = node.from;
|
|
498
|
+
} else {
|
|
499
|
+
cutStyles.push(sNode);
|
|
500
|
+
}
|
|
501
|
+
} else if (node.from >= from && node.to > to) {
|
|
502
|
+
if (sNode.lastChild!.from === to) {
|
|
503
|
+
to = node.to;
|
|
504
|
+
} else {
|
|
505
|
+
cutStyles.push(sNode);
|
|
506
|
+
}
|
|
506
507
|
}
|
|
507
508
|
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
});
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
511
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
512
|
+
if (okay === null) {
|
|
513
|
+
// No textblock found around selection. Check if the rest of the line is empty.
|
|
514
|
+
const line = state.doc.lineAt(from);
|
|
515
|
+
okay = to <= line.to && !/\S/.test(line.text.slice(from - line.from));
|
|
516
|
+
}
|
|
517
|
+
if (!okay) {
|
|
518
|
+
return { range };
|
|
519
|
+
}
|
|
520
520
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
521
|
+
const changes: ChangeSpec[] = [];
|
|
522
|
+
// Some changes must be moved to end of change array so that they are applied in the right order
|
|
523
|
+
const changesAfter: ChangeSpec[] = [];
|
|
524
|
+
// Clear existing links.
|
|
525
|
+
removeLinkInner(from, to, changesAfter, state);
|
|
526
|
+
let cursorOffset = 1;
|
|
527
|
+
// Close and reopen inline styles that partially overlap the range.
|
|
528
|
+
for (const style of cutStyles) {
|
|
529
|
+
const type = InlineMarker[style.name];
|
|
530
|
+
const mark = inlineMarkerText(type);
|
|
531
|
+
if (style.from < from) {
|
|
532
|
+
// Extends before.
|
|
533
|
+
changes.push({ from: skipSpaces(from, state.doc, -1), insert: mark });
|
|
534
|
+
changesAfter.push({ from: skipSpaces(from, state.doc, 1, to), insert: mark });
|
|
535
|
+
} else {
|
|
536
|
+
changes.push({ from: skipSpaces(to, state.doc, -1, from), insert: mark });
|
|
537
|
+
const after = skipSpaces(to, state.doc, 1);
|
|
538
|
+
if (after === to) {
|
|
539
|
+
cursorOffset += mark.length;
|
|
540
|
+
}
|
|
541
|
+
changesAfter.push({ from: after, insert: mark });
|
|
540
542
|
}
|
|
541
|
-
changesAfter.push({ from: after, insert: mark });
|
|
542
543
|
}
|
|
544
|
+
|
|
545
|
+
// Add the link markup.
|
|
546
|
+
changes.push({ from, insert: image ? '` });
|
|
547
|
+
const changeSet = state.changes(changes.concat(changesAfter));
|
|
548
|
+
// Put the cursor between the title or parenthesis.
|
|
549
|
+
return {
|
|
550
|
+
changes: changeSet,
|
|
551
|
+
range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset - (url ? url.length + 2 : 0)),
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
if (changes.changes.empty) {
|
|
555
|
+
return false;
|
|
543
556
|
}
|
|
544
|
-
// Add the link markup.
|
|
545
|
-
changes.push({ from, insert: '[' }, { from: to, insert: ']()' });
|
|
546
|
-
const changeSet = state.changes(changes.concat(changesAfter));
|
|
547
|
-
// Put the cursor between the parenthesis.
|
|
548
|
-
return { changes: changeSet, range: EditorSelection.cursor(changeSet.mapPos(to, 1) - cursorOffset) };
|
|
549
|
-
});
|
|
550
|
-
if (changes.changes.empty) {
|
|
551
|
-
return false;
|
|
552
|
-
}
|
|
553
557
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
};
|
|
558
|
+
dispatch(state.update(changes, { userEvent: 'format.link.add', scrollIntoView: true }));
|
|
559
|
+
return true;
|
|
560
|
+
};
|
|
557
561
|
|
|
558
562
|
//
|
|
559
563
|
// Lists
|
|
@@ -1057,7 +1061,7 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
|
1057
1061
|
const inline: (boolean | null)[] = [null, null, null, null];
|
|
1058
1062
|
let link: boolean = false;
|
|
1059
1063
|
let blockQuote: boolean | null = null;
|
|
1060
|
-
// False indicates mixed list styles
|
|
1064
|
+
// False indicates mixed list styles.
|
|
1061
1065
|
let listStyle: Formatting['listStyle'] | null | false = null;
|
|
1062
1066
|
|
|
1063
1067
|
// Track block context for list/blockquote handling.
|
|
@@ -1216,21 +1220,25 @@ export const getFormatting = (state: EditorState): Formatting => {
|
|
|
1216
1220
|
};
|
|
1217
1221
|
|
|
1218
1222
|
/**
|
|
1219
|
-
* Hook
|
|
1223
|
+
* Hook provides an extension to compute the current formatting state.
|
|
1220
1224
|
*/
|
|
1221
|
-
export const useFormattingState = (): [Formatting |
|
|
1222
|
-
const [state, setState] = useState<Formatting
|
|
1225
|
+
export const useFormattingState = (): [Formatting | undefined, Extension] => {
|
|
1226
|
+
const [state, setState] = useState<Formatting>();
|
|
1227
|
+
|
|
1223
1228
|
const observer = useMemo(
|
|
1224
1229
|
() =>
|
|
1225
1230
|
EditorView.updateListener.of((update) => {
|
|
1226
1231
|
if (update.docChanged || update.selectionSet) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1232
|
+
setState((prevState) => {
|
|
1233
|
+
const newState = getFormatting(update.state);
|
|
1234
|
+
if (!prevState || !formattingEquals(prevState, newState)) {
|
|
1235
|
+
return newState;
|
|
1236
|
+
}
|
|
1237
|
+
return prevState;
|
|
1238
|
+
});
|
|
1231
1239
|
}
|
|
1232
1240
|
}),
|
|
1233
|
-
[],
|
|
1241
|
+
[setState],
|
|
1234
1242
|
);
|
|
1235
1243
|
|
|
1236
1244
|
return [state, observer];
|