@dxos/react-ui-editor 0.4.8-main.46f461d → 0.4.8-main.49cea32
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 +1289 -1328
- 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.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +1 -6
- package/dist/types/src/components/Toolbar/Toolbar.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 +2 -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/index.d.ts +1 -0
- package/dist/types/src/extensions/markdown/index.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/package.json +24 -24
- package/src/components/TextEditor/TextEditor.stories.tsx +14 -2
- package/src/components/Toolbar/Toolbar.tsx +2 -26
- package/src/extensions/command/state.ts +2 -2
- package/src/extensions/dnd.ts +31 -9
- package/src/extensions/factories.ts +19 -16
- package/src/extensions/index.ts +0 -1
- package/src/extensions/markdown/action.ts +112 -0
- package/src/extensions/markdown/index.ts +1 -0
- package/src/hooks/useActionHandler.ts +4 -89
- 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
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from '@dxos/react-ui';
|
|
43
43
|
import { getSize } from '@dxos/react-ui-theme';
|
|
44
44
|
|
|
45
|
-
import { type Formatting } from '../../extensions';
|
|
45
|
+
import { type Action, type ActionType, type Formatting } from '../../extensions';
|
|
46
46
|
import { translationKey } from '../../translations';
|
|
47
47
|
|
|
48
48
|
const tooltipProps = { side: 'top' as const, classNames: 'z-10' };
|
|
@@ -57,30 +57,6 @@ const HeadingIcons: { [key: string]: Icon } = {
|
|
|
57
57
|
'6': TextHSix,
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
// TODO(burdon): Revert to string to make extensible?
|
|
61
|
-
export type ActionType =
|
|
62
|
-
| 'blockquote'
|
|
63
|
-
| 'strong'
|
|
64
|
-
| 'codeblock'
|
|
65
|
-
| 'comment'
|
|
66
|
-
| 'heading'
|
|
67
|
-
| 'image'
|
|
68
|
-
| 'emphasis'
|
|
69
|
-
| 'code'
|
|
70
|
-
| 'link'
|
|
71
|
-
| 'list-bullet'
|
|
72
|
-
| 'list-ordered'
|
|
73
|
-
| 'list-task'
|
|
74
|
-
| 'mention'
|
|
75
|
-
| 'prompt'
|
|
76
|
-
| 'strikethrough'
|
|
77
|
-
| 'table';
|
|
78
|
-
|
|
79
|
-
export type Action = {
|
|
80
|
-
type: ActionType;
|
|
81
|
-
data?: any;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
60
|
export type ToolbarProps = ThemedClassName<
|
|
85
61
|
PropsWithChildren<{
|
|
86
62
|
state: Formatting | undefined;
|
|
@@ -358,7 +334,7 @@ const MarkdownCustom = ({ onUpload }: MarkdownCustomOptions = {}) => {
|
|
|
358
334
|
|
|
359
335
|
useEffect(() => {
|
|
360
336
|
if (onUpload && acceptedFiles.length) {
|
|
361
|
-
|
|
337
|
+
requestAnimationFrame(async () => {
|
|
362
338
|
// NOTE: Clone file since react-dropzone patches in a non-standard `path` property, which confuses IPFS.
|
|
363
339
|
const f = acceptedFiles[0];
|
|
364
340
|
const file = new File([f], f.name, {
|
|
@@ -59,8 +59,8 @@ export const commandState = StateField.define<CommandState>({
|
|
|
59
59
|
selection: { anchor: pos + action.insert.length },
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
-
//
|
|
63
|
-
|
|
62
|
+
// NOTE: Truncates text if set focus immediately.
|
|
63
|
+
requestAnimationFrame(() => view.focus());
|
|
64
64
|
});
|
|
65
65
|
},
|
|
66
66
|
};
|
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
|
};
|
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
|
6
|
-
import { history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
|
|
6
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
|
|
7
7
|
import { bracketMatching } from '@codemirror/language';
|
|
8
8
|
import { searchKeymap } from '@codemirror/search';
|
|
9
9
|
import { EditorState, type Extension } from '@codemirror/state';
|
|
10
10
|
import {
|
|
11
11
|
EditorView,
|
|
12
|
-
|
|
12
|
+
type KeyBinding,
|
|
13
13
|
drawSelection,
|
|
14
14
|
dropCursor,
|
|
15
15
|
highlightActiveLine,
|
|
16
|
-
highlightSpecialChars,
|
|
17
16
|
keymap,
|
|
18
17
|
lineNumbers,
|
|
19
18
|
placeholder,
|
|
@@ -48,37 +47,46 @@ export type BasicExtensionsOptions = {
|
|
|
48
47
|
allowMultipleSelections?: boolean;
|
|
49
48
|
bracketMatching?: boolean;
|
|
50
49
|
closeBrackets?: boolean;
|
|
51
|
-
crosshairCursor?: boolean;
|
|
52
50
|
dropCursor?: boolean;
|
|
53
51
|
drawSelection?: boolean;
|
|
54
52
|
editable?: boolean;
|
|
55
53
|
highlightActiveLine?: boolean;
|
|
56
54
|
history?: boolean;
|
|
57
55
|
indentWithTab?: boolean;
|
|
56
|
+
keymap?: null | 'default' | 'standard';
|
|
58
57
|
lineNumbers?: boolean;
|
|
59
58
|
lineWrapping?: boolean;
|
|
60
59
|
placeholder?: string;
|
|
61
60
|
readonly?: boolean;
|
|
62
61
|
search?: boolean;
|
|
63
62
|
scrollPastEnd?: boolean;
|
|
63
|
+
standardKeymap?: boolean;
|
|
64
64
|
tabSize?: number;
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
const
|
|
67
|
+
const defaultBasicOptions: BasicExtensionsOptions = {
|
|
68
68
|
allowMultipleSelections: true,
|
|
69
69
|
bracketMatching: true,
|
|
70
70
|
closeBrackets: true,
|
|
71
71
|
drawSelection: true,
|
|
72
72
|
editable: true,
|
|
73
73
|
history: true,
|
|
74
|
+
keymap: 'standard',
|
|
74
75
|
lineWrapping: true,
|
|
75
76
|
search: true,
|
|
76
77
|
};
|
|
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,
|
|
84
|
+
};
|
|
85
|
+
|
|
78
86
|
export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extension => {
|
|
79
|
-
const props: BasicExtensionsOptions = defaultsDeep({}, _props,
|
|
87
|
+
const props: BasicExtensionsOptions = defaultsDeep({}, _props, defaultBasicOptions);
|
|
80
88
|
return [
|
|
81
|
-
//
|
|
89
|
+
// NOTE: Doesn't catch errors in keymap functions.
|
|
82
90
|
EditorView.exceptionSink.of((err) => {
|
|
83
91
|
log.catch(err);
|
|
84
92
|
}),
|
|
@@ -86,11 +94,9 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
86
94
|
props.allowMultipleSelections && EditorState.allowMultipleSelections.of(true),
|
|
87
95
|
props.bracketMatching && bracketMatching(),
|
|
88
96
|
props.closeBrackets && closeBrackets(),
|
|
89
|
-
props.crosshairCursor && crosshairCursor(),
|
|
90
97
|
props.dropCursor && dropCursor(),
|
|
91
98
|
props.drawSelection && drawSelection(),
|
|
92
99
|
props.highlightActiveLine && highlightActiveLine(),
|
|
93
|
-
highlightSpecialChars(),
|
|
94
100
|
props.history && history(),
|
|
95
101
|
props.lineNumbers && lineNumbers(),
|
|
96
102
|
props.lineWrapping && EditorView.lineWrapping,
|
|
@@ -102,12 +108,9 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
|
|
|
102
108
|
// https://codemirror.net/docs/ref/#view.KeyBinding
|
|
103
109
|
keymap.of(
|
|
104
110
|
[
|
|
105
|
-
|
|
106
|
-
...standardKeymap,
|
|
107
|
-
|
|
111
|
+
...((props.keymap && keymaps[props.keymap]) ?? []),
|
|
108
112
|
// https://codemirror.net/docs/ref/#commands.indentWithTab
|
|
109
|
-
props.indentWithTab
|
|
110
|
-
|
|
113
|
+
...(props.indentWithTab ? [indentWithTab] : []),
|
|
111
114
|
// https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
|
|
112
115
|
...(props.closeBrackets ? closeBracketsKeymap : []),
|
|
113
116
|
// https://codemirror.net/docs/ref/#commands.historyKeymap
|
|
@@ -136,14 +139,14 @@ export type ThemeExtensionsOptions = {
|
|
|
136
139
|
};
|
|
137
140
|
};
|
|
138
141
|
|
|
139
|
-
const
|
|
142
|
+
const defaultThemeSlots = {
|
|
140
143
|
editor: {
|
|
141
144
|
className: 'w-full bs-full',
|
|
142
145
|
},
|
|
143
146
|
};
|
|
144
147
|
|
|
145
148
|
export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
|
|
146
|
-
const slots = defaultsDeep({}, _slots,
|
|
149
|
+
const slots = defaultsDeep({}, _slots, defaultThemeSlots);
|
|
147
150
|
return [
|
|
148
151
|
EditorView.baseTheme(defaultTheme),
|
|
149
152
|
EditorView.darkTheme.of(themeMode === 'dark'),
|
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,96 +2,11 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import type
|
|
5
|
+
import { type EditorView } from '@codemirror/view';
|
|
6
6
|
|
|
7
|
-
import type
|
|
8
|
-
import {
|
|
9
|
-
Inline,
|
|
10
|
-
List,
|
|
11
|
-
addBlockquote,
|
|
12
|
-
addCodeblock,
|
|
13
|
-
addLink,
|
|
14
|
-
addList,
|
|
15
|
-
createComment,
|
|
16
|
-
insertTable,
|
|
17
|
-
removeBlockquote,
|
|
18
|
-
removeCodeblock,
|
|
19
|
-
removeLink,
|
|
20
|
-
removeList,
|
|
21
|
-
setHeading,
|
|
22
|
-
setStyle,
|
|
23
|
-
toggleBlockquote,
|
|
24
|
-
toggleList,
|
|
25
|
-
toggleStyle,
|
|
26
|
-
} from '../extensions';
|
|
7
|
+
import { type ToolbarProps } from '../components';
|
|
8
|
+
import { processAction } from '../extensions';
|
|
27
9
|
|
|
28
10
|
export const useActionHandler = (view?: EditorView | null): ToolbarProps['onAction'] => {
|
|
29
|
-
return (action) =>
|
|
30
|
-
if (!view) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
let inlineType, listType;
|
|
35
|
-
switch (action.type) {
|
|
36
|
-
case 'heading':
|
|
37
|
-
setHeading(parseInt(action.data))(view);
|
|
38
|
-
break;
|
|
39
|
-
|
|
40
|
-
case 'strong':
|
|
41
|
-
case 'emphasis':
|
|
42
|
-
case 'strikethrough':
|
|
43
|
-
case 'code':
|
|
44
|
-
inlineType =
|
|
45
|
-
action.type === 'strong'
|
|
46
|
-
? Inline.Strong
|
|
47
|
-
: action.type === 'emphasis'
|
|
48
|
-
? Inline.Emphasis
|
|
49
|
-
: action.type === 'strikethrough'
|
|
50
|
-
? Inline.Strikethrough
|
|
51
|
-
: Inline.Code;
|
|
52
|
-
(typeof action.data === 'boolean' ? setStyle(inlineType, action.data) : toggleStyle(inlineType))(view);
|
|
53
|
-
break;
|
|
54
|
-
|
|
55
|
-
case 'list-ordered':
|
|
56
|
-
case 'list-bullet':
|
|
57
|
-
case 'list-task':
|
|
58
|
-
listType =
|
|
59
|
-
action.type === 'list-ordered' ? List.Ordered : action.type === 'list-bullet' ? List.Bullet : List.Task;
|
|
60
|
-
(action.data === false
|
|
61
|
-
? removeList(listType)
|
|
62
|
-
: action.data === true
|
|
63
|
-
? addList(listType)
|
|
64
|
-
: toggleList(listType))(view);
|
|
65
|
-
break;
|
|
66
|
-
|
|
67
|
-
case 'blockquote':
|
|
68
|
-
(action.data === false ? removeBlockquote : action.data === true ? addBlockquote : toggleBlockquote)(view);
|
|
69
|
-
break;
|
|
70
|
-
case 'codeblock':
|
|
71
|
-
(action.data === false ? removeCodeblock : addCodeblock)(view);
|
|
72
|
-
break;
|
|
73
|
-
case 'table':
|
|
74
|
-
insertTable(view);
|
|
75
|
-
break;
|
|
76
|
-
|
|
77
|
-
case 'link':
|
|
78
|
-
(action.data === false ? removeLink : addLink())(view);
|
|
79
|
-
break;
|
|
80
|
-
|
|
81
|
-
case 'image':
|
|
82
|
-
addLink({ url: action.data, image: true })(view);
|
|
83
|
-
break;
|
|
84
|
-
|
|
85
|
-
case 'comment':
|
|
86
|
-
createComment(view);
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// TODO(burdon): Hack otherwise remains on heading selector.
|
|
91
|
-
setTimeout(() => {
|
|
92
|
-
if (!view.hasFocus) {
|
|
93
|
-
view.focus();
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
};
|
|
11
|
+
return (action) => view && processAction(view, action);
|
|
97
12
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cursor-line-margin.d.ts","sourceRoot":"","sources":["../../../../src/extensions/cursor-line-margin.ts"],"names":[],"mappings":"AAOA,OAAO,EAAoB,KAAK,SAAS,EAAe,MAAM,mBAAmB,CAAC;AAQlF,KAAK,OAAO,GAAG;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAGF,eAAO,MAAM,gBAAgB,aAAa,OAAO,KAAkB,SAyElE,CAAC"}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// Based upon @acheronfail’s implementation, fetched 16 Feb 2024
|
|
6
|
-
// https://discuss.codemirror.net/t/cursorscrollmargin-for-v6/7448/5
|
|
7
|
-
|
|
8
|
-
import { type EditorState, type Extension, Transaction } from '@codemirror/state';
|
|
9
|
-
import { EditorView } from '@codemirror/view';
|
|
10
|
-
|
|
11
|
-
import { log } from '@dxos/log';
|
|
12
|
-
|
|
13
|
-
const annotation = 'cursorLineMargin';
|
|
14
|
-
const lineAtPos = (s: EditorState, pos: number) => s.doc.lineAt(pos).number;
|
|
15
|
-
|
|
16
|
-
type Options = {
|
|
17
|
-
lines?: number;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// TODO(burdon): Remove. Buggy and creates scrolling problems. Find better (more general) way to deal with HALO notch.
|
|
21
|
-
export const cursorLineMargin = (options: Options = { lines: 3 }): Extension => {
|
|
22
|
-
const { lines } = options;
|
|
23
|
-
if (!lines || lines <= 0) {
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return EditorView.updateListener.of(({ transactions, state, selectionSet, startState, view }) => {
|
|
28
|
-
// make sure we don't trigger an infinite loop and ignore our own changes
|
|
29
|
-
if (transactions.length < 1 || transactions.some((tr) => tr.isUserEvent(annotation))) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const s = state;
|
|
34
|
-
const { main } = s.selection;
|
|
35
|
-
|
|
36
|
-
// editor rect
|
|
37
|
-
const rect = view.dom.getBoundingClientRect();
|
|
38
|
-
// top and bottom pixel positions of the visible region of the editor
|
|
39
|
-
const viewportTop = rect.top - view.documentTop;
|
|
40
|
-
const viewportBottom = rect.bottom - view.documentTop;
|
|
41
|
-
|
|
42
|
-
// line with main selection
|
|
43
|
-
const mainLine = lineAtPos(s, main.head);
|
|
44
|
-
// top most visible line
|
|
45
|
-
const _visTopLine = lineAtPos(s, view.lineBlockAtHeight(viewportTop).from);
|
|
46
|
-
|
|
47
|
-
// bottom most visible line
|
|
48
|
-
// NOTE: calculating this is slightly more complex - if the editor is
|
|
49
|
-
// larger than all the lines in it, and the `scrollPastEnd()` extension is
|
|
50
|
-
// enabled, then we need to calculate where the bottom most visible line
|
|
51
|
-
// should be if it extended all the way to the bottom of the editor
|
|
52
|
-
const botLineBlk = view.lineBlockAtHeight(viewportBottom);
|
|
53
|
-
const visBotLineDoc = lineAtPos(s, Math.min(botLineBlk.to, s.doc.length));
|
|
54
|
-
const visBotLine =
|
|
55
|
-
botLineBlk.bottom >= viewportBottom
|
|
56
|
-
? visBotLineDoc
|
|
57
|
-
: visBotLineDoc + Math.floor((viewportBottom - botLineBlk.bottom) / view.defaultLineHeight);
|
|
58
|
-
|
|
59
|
-
// const needsScrollTop = mainLine <= visTopLine + cursorLineMargin;
|
|
60
|
-
const needsScrollTop = false;
|
|
61
|
-
const needsScrollBot = mainLine >= visBotLine - lines;
|
|
62
|
-
|
|
63
|
-
// the scroll margins are overlapping
|
|
64
|
-
if (needsScrollTop && needsScrollBot) {
|
|
65
|
-
log.warn('is cursorScrollMargin too large for the editor size?');
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// no need to scroll, we're in between scroll regions
|
|
70
|
-
if (!needsScrollTop && !needsScrollBot) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// if we're within the margin, but moving the other way, then don't scroll
|
|
75
|
-
if (selectionSet) {
|
|
76
|
-
const { head } = startState.selection.main;
|
|
77
|
-
const oldMainLine = lineAtPos(startState, head);
|
|
78
|
-
if (needsScrollTop && oldMainLine < mainLine) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (needsScrollBot && oldMainLine > mainLine) {
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
view.dispatch({
|
|
87
|
-
annotations: Transaction.userEvent.of(annotation),
|
|
88
|
-
effects: EditorView.scrollIntoView(s.selection.main.head, {
|
|
89
|
-
y: needsScrollTop ? 'start' : 'end',
|
|
90
|
-
yMargin: view.defaultLineHeight * lines,
|
|
91
|
-
}),
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
};
|