@dxos/react-ui-editor 0.3.10-next.6fe28e7 → 0.3.10-next.86bde39
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 +868 -738
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +38 -0
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +5 -2
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +9 -4
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts +10 -0
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/{basic/bundle.d.ts → basic.d.ts} +1 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +14 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.d.ts → experimental.d.ts} +2 -2
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +6 -2
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts +11 -0
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts +7 -0
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +31 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts +12 -0
- package/dist/types/src/components/TextEditor/extensions/tooltip.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/index.d.ts +2 -0
- package/dist/types/src/components/TextEditor/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts +20 -0
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/themes/index.d.ts +2 -0
- package/dist/types/src/components/TextEditor/themes/index.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/handle.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/index.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +0 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useTextModel.d.ts +4 -4
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/styles/markdown.d.ts +12 -10
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/dist/types/src/testing/replicator.d.ts.map +1 -1
- package/package.json +20 -32
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +273 -0
- package/src/components/TextEditor/TextEditor.stories.tsx +46 -43
- package/src/components/TextEditor/TextEditor.tsx +65 -44
- package/src/components/TextEditor/extensions/autocomplete.ts +74 -0
- package/src/components/TextEditor/extensions/{basic/bundle.ts → basic.ts} +4 -2
- package/src/components/TextEditor/extensions/comments.ts +173 -0
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx} +18 -2
- package/src/components/TextEditor/extensions/index.ts +6 -2
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkDecoration.ts → link.ts} +76 -31
- package/src/components/TextEditor/extensions/{change.ts → listener.ts} +4 -3
- package/src/components/TextEditor/extensions/markdown/bundle.ts +21 -25
- package/src/components/TextEditor/extensions/markdown/highlight.ts +167 -0
- package/src/components/TextEditor/extensions/markdown/index.ts +1 -1
- package/src/components/TextEditor/extensions/tasklist.ts +107 -0
- package/src/components/TextEditor/extensions/{hyperlink/hyperlinkTooltip.tsx → tooltip.tsx} +9 -9
- package/src/components/TextEditor/index.ts +3 -0
- package/src/components/TextEditor/themes/default.ts +185 -0
- package/src/components/TextEditor/themes/index.ts +5 -0
- package/src/components/TextEditor/yjs.stories.tsx +1 -0
- package/src/{automerge → hooks/automerge}/automerge.stories.tsx +2 -1
- package/src/hooks/index.ts +0 -1
- package/src/hooks/useTextModel.ts +52 -39
- package/src/styles/markdown.ts +26 -24
- package/src/testing/replicator.ts +6 -6
- package/dist/types/src/automerge/automerge-plugin/PatchSemaphore.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/amToCodemirror.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/codeMirrorToAm.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/handle.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/index.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge-plugin/plugin.d.ts.map +0 -1
- package/dist/types/src/automerge/automerge.stories.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic/bundle.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts +0 -3
- package/dist/types/src/components/TextEditor/extensions/basic/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts +0 -10
- package/dist/types/src/components/TextEditor/extensions/basic/theme.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/change.d.ts +0 -7
- package/dist/types/src/components/TextEditor/extensions/change.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts +0 -11
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkDecoration.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts +0 -8
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkTooltip.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/hyperlinkWidget.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts +0 -4
- package/dist/types/src/components/TextEditor/extensions/hyperlink/index.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts +0 -17
- package/dist/types/src/components/TextEditor/extensions/markdown/tags.d.ts.map +0 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts +0 -21
- package/dist/types/src/components/TextEditor/extensions/markdown/theme.d.ts.map +0 -1
- package/dist/types/src/hooks/useCollaboration.d.ts +0 -9
- package/dist/types/src/hooks/useCollaboration.d.ts.map +0 -1
- package/src/components/TextEditor/extensions/basic/index.ts +0 -6
- package/src/components/TextEditor/extensions/basic/theme.ts +0 -49
- package/src/components/TextEditor/extensions/hyperlink/index.ts +0 -7
- package/src/components/TextEditor/extensions/markdown/tags.ts +0 -38
- package/src/components/TextEditor/extensions/markdown/theme.ts +0 -265
- package/src/hooks/useCollaboration.ts +0 -45
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.d.ts +0 -0
- /package/dist/types/src/{automerge → hooks/automerge}/automerge.stories.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/handle.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/index.d.ts +0 -0
- /package/dist/types/src/{automerge/automerge-plugin → hooks/automerge}/plugin.d.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/PatchSemaphore.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/amToCodemirror.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/codeMirrorToAm.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/handle.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/index.ts +0 -0
- /package/src/{automerge/automerge-plugin → hooks/automerge}/plugin.ts +0 -0
|
@@ -7,23 +7,24 @@ import { EditorView } from '@codemirror/view';
|
|
|
7
7
|
import { useFocusableGroup } from '@fluentui/react-tabster';
|
|
8
8
|
import { vim } from '@replit/codemirror-vim';
|
|
9
9
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
10
|
-
import get from 'lodash.get';
|
|
11
10
|
import React, {
|
|
11
|
+
type ComponentProps,
|
|
12
12
|
type KeyboardEvent,
|
|
13
13
|
forwardRef,
|
|
14
14
|
useCallback,
|
|
15
15
|
useEffect,
|
|
16
16
|
useImperativeHandle,
|
|
17
17
|
useState,
|
|
18
|
-
type ComponentProps,
|
|
19
18
|
} from 'react';
|
|
20
19
|
|
|
21
|
-
import {
|
|
20
|
+
import { generateName } from '@dxos/display-name';
|
|
22
21
|
import { useThemeContext } from '@dxos/react-ui';
|
|
22
|
+
import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
|
|
23
23
|
|
|
24
|
-
import { basicBundle,
|
|
25
|
-
import {
|
|
26
|
-
import type
|
|
24
|
+
import { basicBundle, markdownBundle } from './extensions';
|
|
25
|
+
import { defaultTheme, markdownTheme, textTheme } from './themes';
|
|
26
|
+
import { type EditorModel } from '../../hooks';
|
|
27
|
+
import { type ThemeStyles } from '../../styles';
|
|
27
28
|
|
|
28
29
|
export const EditorModes = ['default', 'vim'] as const;
|
|
29
30
|
export type EditorMode = (typeof EditorModes)[number];
|
|
@@ -37,7 +38,7 @@ export type CursorInfo = {
|
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export type TextEditorRef = {
|
|
40
|
-
|
|
41
|
+
root: HTMLDivElement | null;
|
|
41
42
|
state?: EditorState;
|
|
42
43
|
view?: EditorView;
|
|
43
44
|
};
|
|
@@ -65,66 +66,65 @@ export type TextEditorProps = {
|
|
|
65
66
|
* NOTE: Rather than adding properties, try to create extensions that can be reused.
|
|
66
67
|
*/
|
|
67
68
|
export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
68
|
-
({ model, extensions = [], slots, editorMode }, forwardedRef) => {
|
|
69
|
+
({ model, extensions = [], slots = defaultSlots, editorMode }, forwardedRef) => {
|
|
69
70
|
const { themeMode } = useThemeContext();
|
|
70
71
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
71
72
|
|
|
72
|
-
const [
|
|
73
|
+
const [root, setRoot] = useState<HTMLDivElement | null>(null);
|
|
73
74
|
const [state, setState] = useState<EditorState>();
|
|
74
75
|
const [view, setView] = useState<EditorView>();
|
|
75
|
-
useImperativeHandle(
|
|
76
|
-
forwardedRef,
|
|
77
|
-
() => ({
|
|
78
|
-
editor: parent,
|
|
79
|
-
state,
|
|
80
|
-
view,
|
|
81
|
-
}),
|
|
82
|
-
[view, state, parent],
|
|
83
|
-
);
|
|
76
|
+
useImperativeHandle(forwardedRef, () => ({ root, state, view }), [view, state, root]);
|
|
84
77
|
|
|
85
|
-
// TODO(burdon):
|
|
86
|
-
const
|
|
78
|
+
// TODO(burdon): Factor out?
|
|
79
|
+
const { awareness, peer } = model;
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (awareness && peer) {
|
|
82
|
+
awareness.setLocalStateField('user', {
|
|
83
|
+
name: peer.name ?? generateName(peer.id),
|
|
84
|
+
color: getColorForValue({ value: peer.id, type: 'color' }),
|
|
85
|
+
colorLight: getColorForValue({ value: peer.id, themeMode, type: 'highlight' }),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}, [awareness, peer, themeMode]);
|
|
87
89
|
|
|
88
90
|
useEffect(() => {
|
|
89
|
-
if (!
|
|
91
|
+
if (!root) {
|
|
90
92
|
return;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
const initialText = isDocAccessor(model.content)
|
|
94
|
-
? get(model.content.handle.docSync(), model.content.path)
|
|
95
|
-
: model.content?.toString();
|
|
96
|
-
|
|
97
95
|
const state = EditorState.create({
|
|
98
|
-
doc:
|
|
96
|
+
doc: model.text(),
|
|
99
97
|
extensions: [
|
|
100
98
|
// TODO(burdon): Factor out VIM mode?
|
|
101
99
|
editorMode === 'vim' && vim(),
|
|
102
100
|
|
|
103
|
-
// Replication.
|
|
104
|
-
collaboration,
|
|
105
|
-
|
|
106
101
|
// Theme.
|
|
107
|
-
|
|
102
|
+
EditorView.baseTheme(defaultTheme),
|
|
103
|
+
EditorView.theme(slots?.editor?.theme ?? {}),
|
|
104
|
+
// TODO(burdon): themeMode doesn't change in storybooks.
|
|
105
|
+
EditorView.darkTheme.of(themeMode === 'dark'),
|
|
106
|
+
|
|
107
|
+
// Storage and replication.
|
|
108
|
+
model.extension,
|
|
108
109
|
|
|
109
110
|
// Custom.
|
|
110
111
|
...extensions,
|
|
111
112
|
].filter(Boolean) as Extension[],
|
|
112
113
|
});
|
|
113
114
|
|
|
114
|
-
setState(state);
|
|
115
|
-
|
|
116
115
|
// NOTE: This repaints the editor.
|
|
117
116
|
// If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
|
|
118
117
|
// Ideally this should not be hit except when changing between text objects.
|
|
119
118
|
view?.destroy();
|
|
120
|
-
setView(new EditorView({ state, parent }));
|
|
119
|
+
setView(new EditorView({ state, parent: root }));
|
|
120
|
+
setState(state);
|
|
121
121
|
|
|
122
122
|
return () => {
|
|
123
123
|
view?.destroy();
|
|
124
124
|
setView(undefined);
|
|
125
125
|
setState(undefined);
|
|
126
126
|
};
|
|
127
|
-
}, [
|
|
127
|
+
}, [root, model, themeMode, editorMode]);
|
|
128
128
|
|
|
129
129
|
const handleKeyUp = useCallback(
|
|
130
130
|
(event: KeyboardEvent) => {
|
|
@@ -136,7 +136,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
case 'Escape': {
|
|
139
|
-
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) &&
|
|
139
|
+
editorMode === 'vim' && (altKey || shiftKey || metaKey || ctrlKey) && root?.focus();
|
|
140
140
|
break;
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -147,7 +147,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
147
147
|
return (
|
|
148
148
|
<div
|
|
149
149
|
key={model.id}
|
|
150
|
-
ref={
|
|
150
|
+
ref={setRoot}
|
|
151
151
|
tabIndex={0}
|
|
152
152
|
{...slots?.root}
|
|
153
153
|
{...(editorMode !== 'vim' && tabsterDOMAttribute)}
|
|
@@ -157,15 +157,16 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
157
157
|
},
|
|
158
158
|
);
|
|
159
159
|
|
|
160
|
+
// TODO(burdon): Set default text theme?
|
|
160
161
|
export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
161
|
-
({ extensions
|
|
162
|
+
({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
|
|
162
163
|
const { themeMode } = useThemeContext();
|
|
163
|
-
const
|
|
164
|
+
const slots = defaultsDeep({}, _slots, defaultTextSlots);
|
|
164
165
|
return (
|
|
165
166
|
<BaseTextEditor
|
|
166
167
|
ref={forwardedRef}
|
|
167
|
-
extensions={extensions}
|
|
168
|
-
slots={
|
|
168
|
+
extensions={[basicBundle({ themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
|
|
169
|
+
slots={slots}
|
|
169
170
|
{...props}
|
|
170
171
|
/>
|
|
171
172
|
);
|
|
@@ -173,16 +174,36 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
173
174
|
);
|
|
174
175
|
|
|
175
176
|
export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
176
|
-
({ extensions
|
|
177
|
+
({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
|
|
177
178
|
const { themeMode } = useThemeContext();
|
|
178
|
-
const
|
|
179
|
+
const slots = defaultsDeep({}, _slots, defaultMarkdownSlots);
|
|
179
180
|
return (
|
|
180
181
|
<BaseTextEditor
|
|
181
182
|
ref={forwardedRef}
|
|
182
|
-
extensions={extensions}
|
|
183
|
-
slots={
|
|
183
|
+
extensions={[markdownBundle({ themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
|
|
184
|
+
slots={slots}
|
|
184
185
|
{...props}
|
|
185
186
|
/>
|
|
186
187
|
);
|
|
187
188
|
},
|
|
188
189
|
);
|
|
190
|
+
|
|
191
|
+
export const defaultSlots: TextEditorSlots = {
|
|
192
|
+
root: {
|
|
193
|
+
className: mx('p-2', inputSurface),
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const defaultTextSlots: TextEditorSlots = {
|
|
198
|
+
...defaultSlots,
|
|
199
|
+
editor: {
|
|
200
|
+
theme: textTheme,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export const defaultMarkdownSlots: TextEditorSlots = {
|
|
205
|
+
...defaultSlots,
|
|
206
|
+
editor: {
|
|
207
|
+
theme: markdownTheme,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// TODO(burdon): Automcomplete: https://codemirror.net/5/doc/manual.html#addon_runmode
|
|
6
|
+
// TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
|
|
7
|
+
// TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
|
|
8
|
+
// https://codemirror.net/5/doc/manual.html#addon_runmode
|
|
9
|
+
// TODO(burdon): Add-on: dialog.
|
|
10
|
+
// TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
|
|
11
|
+
// TODO(burdon): Split view: https://codemirror.net/examples/split
|
|
12
|
+
|
|
13
|
+
// https://codemirror.net/examples/autocompletion
|
|
14
|
+
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
15
|
+
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
autocompletion,
|
|
19
|
+
type Completion,
|
|
20
|
+
type CompletionContext,
|
|
21
|
+
completionKeymap,
|
|
22
|
+
type CompletionResult,
|
|
23
|
+
} from '@codemirror/autocomplete';
|
|
24
|
+
import { keymap } from '@codemirror/view';
|
|
25
|
+
|
|
26
|
+
export type AutocompleteResult = Completion;
|
|
27
|
+
|
|
28
|
+
export type AutocompleteOptions = {
|
|
29
|
+
onSearch: (text: string) => Completion[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Autocomplete extension.
|
|
34
|
+
*/
|
|
35
|
+
export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
36
|
+
return [
|
|
37
|
+
// https://codemirror.net/docs/ref/#view.keymap
|
|
38
|
+
// https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
|
|
39
|
+
keymap.of(completionKeymap),
|
|
40
|
+
|
|
41
|
+
// https://codemirror.net/examples/autocompletion
|
|
42
|
+
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
43
|
+
autocompletion({
|
|
44
|
+
// TODO(burdon): Set custom keymap.
|
|
45
|
+
// defaultKeymap: false,
|
|
46
|
+
|
|
47
|
+
// Don't start unless key press.
|
|
48
|
+
activateOnTyping: false,
|
|
49
|
+
|
|
50
|
+
// TODO(burdon): Option to create new page?
|
|
51
|
+
// TODO(burdon): Optional decoration via addToOptions
|
|
52
|
+
override: [
|
|
53
|
+
(context: CompletionContext): CompletionResult | null => {
|
|
54
|
+
const word = context.matchBefore(/\w*/);
|
|
55
|
+
if (!word || (word.from === word.to && !context.explicit)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// TODO(burdon): Option to convert to links?
|
|
60
|
+
return {
|
|
61
|
+
from: word.from,
|
|
62
|
+
options: onSearch(word.text.toLowerCase()),
|
|
63
|
+
// options: [
|
|
64
|
+
// { label: 'apple', type: 'keyword' },
|
|
65
|
+
// { label: 'amazon', type: 'keyword' },
|
|
66
|
+
// { label: 'hello', type: 'variable', info: '(World)' },
|
|
67
|
+
// { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
|
|
68
|
+
// ],
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
};
|
|
@@ -17,12 +17,14 @@ export type BasicBundleOptions = {
|
|
|
17
17
|
|
|
18
18
|
export const basicBundle = ({ themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
|
|
19
19
|
[
|
|
20
|
+
// TODO(burdon): Compare with minimalSetup.
|
|
21
|
+
// https://codemirror.net/docs/ref/#codemirror.minimalSetup
|
|
20
22
|
bracketMatching(),
|
|
21
23
|
closeBrackets(),
|
|
22
|
-
_placeholder && placeholder(_placeholder),
|
|
23
|
-
|
|
24
24
|
EditorView.lineWrapping,
|
|
25
25
|
|
|
26
|
+
_placeholder && placeholder(_placeholder),
|
|
27
|
+
|
|
26
28
|
// TODO(burdon): Is this required?
|
|
27
29
|
themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
|
|
28
30
|
].filter(Boolean) as Extension[];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import {
|
|
7
|
+
Decoration,
|
|
8
|
+
type DecorationSet,
|
|
9
|
+
EditorView,
|
|
10
|
+
keymap,
|
|
11
|
+
MatchDecorator,
|
|
12
|
+
type Rect,
|
|
13
|
+
ViewPlugin,
|
|
14
|
+
type ViewUpdate,
|
|
15
|
+
WidgetType,
|
|
16
|
+
} from '@codemirror/view';
|
|
17
|
+
|
|
18
|
+
import { invariant } from '@dxos/invariant';
|
|
19
|
+
|
|
20
|
+
// 1. TODO(burdon): Make atomic (for tasklist also).
|
|
21
|
+
// - https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
|
|
22
|
+
// - https://discuss.codemirror.net/t/creating-atomic-replace-decorations/2961
|
|
23
|
+
// - https://codemirror.net/docs/ref/#state.EditorState%5EtransactionFilter (transaction filter: move, delete).
|
|
24
|
+
// 2. TODO(burdon): Create/track threads in composer (change global state). Select/follow. Scroll with page.
|
|
25
|
+
// - Separate from chat/search.
|
|
26
|
+
// 3. TODO(burdon): Support multiple threads in sidebar?
|
|
27
|
+
// 4. TODO(burdon): Anchor AI thread when creating section.
|
|
28
|
+
// 5. TODO(burdon): Button to resolve thread to close comments.
|
|
29
|
+
|
|
30
|
+
// 6. TODO(burdon): Perf: Update existing rangeset.
|
|
31
|
+
// https://discuss.codemirror.net/t/rangeset-with-metadata-and-different-decorations/3874
|
|
32
|
+
|
|
33
|
+
// TODO(burdon): Import note (performance):
|
|
34
|
+
// Easily track & remove content with decorations
|
|
35
|
+
// https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
|
|
36
|
+
|
|
37
|
+
class BookmarkWidget extends WidgetType {
|
|
38
|
+
constructor(private readonly _pos: number, private readonly _id: string) {
|
|
39
|
+
super();
|
|
40
|
+
invariant(this._id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override eq(other: BookmarkWidget) {
|
|
44
|
+
return this._id === other._id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// TODO(burdon): Click to select.
|
|
48
|
+
override toDOM() {
|
|
49
|
+
const span = document.createElement('span');
|
|
50
|
+
span.className = 'cm-bookmark';
|
|
51
|
+
// https://emojifinder.com/comment
|
|
52
|
+
span.textContent = '💬';
|
|
53
|
+
return span;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// TODO(burdon): Reconcile with theme.
|
|
58
|
+
const styles = EditorView.baseTheme({
|
|
59
|
+
'& .cm-bookmark': {
|
|
60
|
+
cursor: 'pointer',
|
|
61
|
+
margin: '4px',
|
|
62
|
+
padding: '4px',
|
|
63
|
+
backgroundColor: 'yellow',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type CommentsOptions = {
|
|
68
|
+
key?: string;
|
|
69
|
+
onCreate?: () => string | void;
|
|
70
|
+
onUpdate?: (info: { items: string[]; active: string; pos: number; location: Rect }) => void;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// https://www.markdownguide.org/extended-syntax/#footnotes
|
|
74
|
+
// TODO(burdon): Record span in automerge model?
|
|
75
|
+
// TODO(burdon): Extended markdown: https://www.markdownguide.org/extended-syntax
|
|
76
|
+
export const comments = (options: CommentsOptions = {}): Extension => {
|
|
77
|
+
const bookmarkMatcher = new MatchDecorator({
|
|
78
|
+
regexp: /\[\^(\w+)\]/g,
|
|
79
|
+
decoration: (match, view, pos) => {
|
|
80
|
+
const id = match[1];
|
|
81
|
+
return Decoration.replace({
|
|
82
|
+
id,
|
|
83
|
+
widget: new BookmarkWidget(pos, id),
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const bookmarks = ViewPlugin.fromClass(
|
|
89
|
+
class {
|
|
90
|
+
bookmarks: DecorationSet;
|
|
91
|
+
constructor(view: EditorView) {
|
|
92
|
+
this.bookmarks = bookmarkMatcher.createDeco(view);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
update(update: ViewUpdate) {
|
|
96
|
+
this.bookmarks = bookmarkMatcher.updateDeco(update, this.bookmarks);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
decorations: (instance) => instance.bookmarks,
|
|
101
|
+
provide: (plugin) =>
|
|
102
|
+
EditorView.atomicRanges.of((view) => {
|
|
103
|
+
return view.plugin(plugin)?.bookmarks || Decoration.none;
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return [
|
|
109
|
+
keymap.of([
|
|
110
|
+
{
|
|
111
|
+
key: options?.key ?? 'alt-meta-c',
|
|
112
|
+
run: (view) => {
|
|
113
|
+
// Insert footnote.
|
|
114
|
+
const id = options.onCreate?.();
|
|
115
|
+
if (id) {
|
|
116
|
+
const pos = view.state.selection.main.head;
|
|
117
|
+
const tag = `[^${id}]`;
|
|
118
|
+
view.dispatch({
|
|
119
|
+
changes: { from: pos, insert: tag },
|
|
120
|
+
selection: { anchor: pos + tag.length },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false;
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
]),
|
|
130
|
+
|
|
131
|
+
bookmarks,
|
|
132
|
+
|
|
133
|
+
// Monitor cursor movement.
|
|
134
|
+
EditorView.updateListener.of((update) => {
|
|
135
|
+
const view = update.view;
|
|
136
|
+
const pos = view.state.selection.main.head;
|
|
137
|
+
|
|
138
|
+
const decorations: { from: number; to: number; value: Decoration }[] = [];
|
|
139
|
+
const rangeSet = view.plugin(bookmarks)?.bookmarks;
|
|
140
|
+
rangeSet?.between(pos, pos + 1, (from, to, value) => {
|
|
141
|
+
if (value.spec.widget) {
|
|
142
|
+
decorations.push({ from, to, value });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (decorations.length) {
|
|
147
|
+
const {
|
|
148
|
+
from,
|
|
149
|
+
value: {
|
|
150
|
+
spec: { id },
|
|
151
|
+
},
|
|
152
|
+
} = decorations[0];
|
|
153
|
+
const location = view.coordsAtPos(from);
|
|
154
|
+
if (location) {
|
|
155
|
+
options.onUpdate?.({
|
|
156
|
+
items: decorations.map(
|
|
157
|
+
({
|
|
158
|
+
value: {
|
|
159
|
+
spec: { id },
|
|
160
|
+
},
|
|
161
|
+
}) => id,
|
|
162
|
+
),
|
|
163
|
+
active: id,
|
|
164
|
+
pos,
|
|
165
|
+
location,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
styles,
|
|
172
|
+
];
|
|
173
|
+
};
|
package/src/components/TextEditor/extensions/{hyperlink/hyperlinkWidget.tsx → experimental.tsx}
RENAMED
|
@@ -13,12 +13,28 @@ import {
|
|
|
13
13
|
type ViewUpdate,
|
|
14
14
|
} from '@codemirror/view';
|
|
15
15
|
|
|
16
|
+
// TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
|
|
17
|
+
// TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
|
|
18
|
+
// https://codemirror.net/5/doc/manual.html#addon_runmode
|
|
19
|
+
// TODO(burdon): Add-on: dialog.
|
|
20
|
+
// TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
|
|
21
|
+
// TODO(burdon): Split view: https://codemirror.net/examples/split
|
|
22
|
+
// TODO(burdon): https://codemirror.net/5/demo/simplemode.html
|
|
23
|
+
|
|
24
|
+
// https://codemirror.net/examples/autocompletion
|
|
25
|
+
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
26
|
+
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
|
27
|
+
|
|
28
|
+
// TODO(burdon): Hint to customize?
|
|
29
|
+
// https://codemirror.net/examples/autocompletion
|
|
30
|
+
|
|
16
31
|
// Notes: Can't edit widget content (or cursor nav through them).
|
|
17
32
|
// - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
|
|
18
33
|
// Widgets intentionally always get set to contenteditable=false,
|
|
19
34
|
// or they would become part of CodeMirror’s editable content element.
|
|
20
35
|
// You should be able to introduce new contenteditable=true child elements inside of them.
|
|
21
36
|
|
|
37
|
+
// TODO(burdon): URL match?
|
|
22
38
|
const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
|
|
23
39
|
|
|
24
40
|
/**
|
|
@@ -102,7 +118,7 @@ export function hyperLinkExtension() {
|
|
|
102
118
|
);
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
export const
|
|
121
|
+
export const styles = EditorView.baseTheme({
|
|
106
122
|
'.cm-hyperlink-label': {
|
|
107
123
|
cursor: 'pointer',
|
|
108
124
|
textDecoration: 'underline',
|
|
@@ -116,4 +132,4 @@ export const hyperlinkStyle = EditorView.baseTheme({
|
|
|
116
132
|
},
|
|
117
133
|
});
|
|
118
134
|
|
|
119
|
-
export const hyperlinkWidget: Extension = [hyperLinkExtension(),
|
|
135
|
+
export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
export * from './autocomplete';
|
|
5
6
|
export * from './basic';
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
7
|
+
export * from './comments';
|
|
8
|
+
export * from './link';
|
|
9
|
+
export * from './listener';
|
|
8
10
|
export * from './markdown';
|
|
11
|
+
export * from './tasklist';
|
|
12
|
+
export * from './tooltip';
|