@dxos/react-ui-editor 0.3.10-main.cb1d9e2 → 0.3.10-main.cc91b2d
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 +1185 -374
- 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 +23 -2
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts +3 -0
- package/dist/types/src/components/TextEditor/TextEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/code.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/code.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts +2 -2
- package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts +24 -0
- package/dist/types/src/components/TextEditor/extensions/highlight.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
- package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +6 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +10 -1
- 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 +9 -3
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
- package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts +5 -0
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts +3 -0
- package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts +8 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/index.d.ts +1 -1
- package/dist/types/src/hooks/automerge/index.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +5 -0
- package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
- package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
- package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
- package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useTextModel.d.ts +6 -0
- package/dist/types/src/hooks/useTextModel.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +2 -2
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
- package/dist/types/src/styles/markdown.d.ts +3 -2
- 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 +22 -20
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +124 -32
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -1
- package/src/components/TextEditor/TextEditor.tsx +31 -24
- package/src/components/TextEditor/extensions/autocomplete.ts +5 -20
- package/src/components/TextEditor/extensions/basic.ts +6 -4
- package/src/components/TextEditor/extensions/blast.ts +373 -0
- package/src/components/TextEditor/extensions/code.ts +99 -0
- package/src/components/TextEditor/extensions/comments.ts +29 -46
- package/src/components/TextEditor/extensions/demo.ts +4 -1
- package/src/components/TextEditor/extensions/experimental.tsx +15 -36
- package/src/components/TextEditor/extensions/highlight.ts +128 -0
- package/src/components/TextEditor/extensions/image.ts +67 -0
- package/src/components/TextEditor/extensions/index.ts +6 -0
- package/src/components/TextEditor/extensions/link.ts +17 -25
- package/src/components/TextEditor/extensions/listener.ts +3 -3
- package/src/components/TextEditor/extensions/markdown/bundle.ts +45 -34
- package/src/components/TextEditor/extensions/markdown/highlight.ts +148 -108
- package/src/components/TextEditor/extensions/mention.ts +31 -0
- package/src/components/TextEditor/extensions/table.ts +119 -0
- package/src/components/TextEditor/extensions/tasklist.ts +42 -28
- package/src/components/TextEditor/themes/default.ts +97 -4
- package/src/hooks/automerge/PatchSemaphore.ts +12 -9
- package/src/hooks/automerge/automerge.stories.tsx +4 -2
- package/src/hooks/automerge/index.ts +1 -1
- package/src/hooks/automerge/plugin.ts +1 -0
- package/src/hooks/automerge/{amToCodemirror.ts → translation/automerge-to-codemirror.ts} +10 -2
- package/src/hooks/automerge/{codeMirrorToAm.ts → translation/codemirror-to-automerge.ts} +7 -4
- package/src/hooks/automerge/translation/index.ts +6 -0
- package/src/hooks/useTextModel.ts +9 -0
- package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +2 -2
- package/src/styles/markdown.ts +15 -8
- package/src/testing/replicator.ts +1 -0
- package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/amToCodemirror.d.ts +0 -6
- package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +0 -1
- package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts +0 -7
- package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +0 -1
|
@@ -21,7 +21,7 @@ import { generateName } from '@dxos/display-name';
|
|
|
21
21
|
import { useThemeContext } from '@dxos/react-ui';
|
|
22
22
|
import { getColorForValue, inputSurface, mx } from '@dxos/react-ui-theme';
|
|
23
23
|
|
|
24
|
-
import { basicBundle,
|
|
24
|
+
import { basicBundle, markdownBundle } from './extensions';
|
|
25
25
|
import { defaultTheme, markdownTheme, textTheme } from './themes';
|
|
26
26
|
import { type EditorModel } from '../../hooks';
|
|
27
27
|
import { type ThemeStyles } from '../../styles';
|
|
@@ -56,9 +56,10 @@ export type TextEditorSlots = {
|
|
|
56
56
|
|
|
57
57
|
export type TextEditorProps = {
|
|
58
58
|
model: EditorModel;
|
|
59
|
+
readonly?: boolean;
|
|
60
|
+
editorMode?: EditorMode;
|
|
59
61
|
extensions?: Extension[];
|
|
60
62
|
slots?: TextEditorSlots;
|
|
61
|
-
editorMode?: EditorMode;
|
|
62
63
|
};
|
|
63
64
|
|
|
64
65
|
/**
|
|
@@ -66,7 +67,7 @@ export type TextEditorProps = {
|
|
|
66
67
|
* NOTE: Rather than adding properties, try to create extensions that can be reused.
|
|
67
68
|
*/
|
|
68
69
|
export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
69
|
-
({ model, extensions = [], slots = defaultSlots
|
|
70
|
+
({ model, readonly, editorMode, extensions = [], slots = defaultSlots }, forwardedRef) => {
|
|
70
71
|
const { themeMode } = useThemeContext();
|
|
71
72
|
const tabsterDOMAttribute = useFocusableGroup({ tabBehavior: 'limited' });
|
|
72
73
|
|
|
@@ -87,6 +88,12 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
87
88
|
}
|
|
88
89
|
}, [awareness, peer, themeMode]);
|
|
89
90
|
|
|
91
|
+
// TODO(burdon): Get from model.
|
|
92
|
+
// useHighlights(view, [
|
|
93
|
+
// { id: 'c-1', from: 0, to: 20 },
|
|
94
|
+
// { id: 'c-2', from: 100, to: 120 },
|
|
95
|
+
// ]);
|
|
96
|
+
|
|
90
97
|
useEffect(() => {
|
|
91
98
|
if (!root) {
|
|
92
99
|
return;
|
|
@@ -95,10 +102,13 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
95
102
|
const state = EditorState.create({
|
|
96
103
|
doc: model.text(),
|
|
97
104
|
extensions: [
|
|
105
|
+
readonly && EditorState.readOnly.of(readonly),
|
|
106
|
+
|
|
98
107
|
// TODO(burdon): Factor out VIM mode?
|
|
99
108
|
editorMode === 'vim' && vim(),
|
|
100
109
|
|
|
101
110
|
// Theme.
|
|
111
|
+
// TODO(burdon): Make optional.
|
|
102
112
|
EditorView.baseTheme(defaultTheme),
|
|
103
113
|
EditorView.theme(slots?.editor?.theme ?? {}),
|
|
104
114
|
// TODO(burdon): themeMode doesn't change in storybooks.
|
|
@@ -116,7 +126,17 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
116
126
|
// If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
|
|
117
127
|
// Ideally this should not be hit except when changing between text objects.
|
|
118
128
|
view?.destroy();
|
|
119
|
-
setView(
|
|
129
|
+
setView(
|
|
130
|
+
new EditorView({
|
|
131
|
+
state,
|
|
132
|
+
parent: root,
|
|
133
|
+
// NOTE: Uncomment to spy on all transactions.
|
|
134
|
+
// https://codemirror.net/docs/ref/#view.EditorView.dispatch
|
|
135
|
+
// dispatch: (transaction, view) => {
|
|
136
|
+
// view.update([transaction]);
|
|
137
|
+
// },
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
120
140
|
setState(state);
|
|
121
141
|
|
|
122
142
|
return () => {
|
|
@@ -124,7 +144,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
124
144
|
setView(undefined);
|
|
125
145
|
setState(undefined);
|
|
126
146
|
};
|
|
127
|
-
}, [root, model, themeMode, editorMode]);
|
|
147
|
+
}, [root, model, themeMode, readonly, editorMode]);
|
|
128
148
|
|
|
129
149
|
const handleKeyUp = useCallback(
|
|
130
150
|
(event: KeyboardEvent) => {
|
|
@@ -157,25 +177,15 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
157
177
|
},
|
|
158
178
|
);
|
|
159
179
|
|
|
160
|
-
// TODO(burdon): Allow plugins to set extensions (factory).
|
|
161
|
-
const maybeDebug = (): Extension => {
|
|
162
|
-
// TODO(burdon): Parse JSON script format (with key bindings?)
|
|
163
|
-
const items = localStorage.getItem('dxos.composer.demo');
|
|
164
|
-
if (items) {
|
|
165
|
-
return demo({ items: items.split(',') });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return [];
|
|
169
|
-
};
|
|
170
|
-
|
|
171
180
|
export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
172
|
-
({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
|
|
181
|
+
({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
|
|
173
182
|
const { themeMode } = useThemeContext();
|
|
174
183
|
const slots = defaultsDeep({}, _slots, defaultTextSlots);
|
|
175
184
|
return (
|
|
176
185
|
<BaseTextEditor
|
|
177
186
|
ref={forwardedRef}
|
|
178
|
-
|
|
187
|
+
readonly={readonly}
|
|
188
|
+
extensions={[basicBundle({ readonly, themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
|
|
179
189
|
slots={slots}
|
|
180
190
|
{...props}
|
|
181
191
|
/>
|
|
@@ -184,17 +194,14 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
184
194
|
);
|
|
185
195
|
|
|
186
196
|
export const MarkdownEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
187
|
-
({ extensions = [], slots: _slots, ...props }, forwardedRef) => {
|
|
197
|
+
({ readonly, extensions = [], slots: _slots, ...props }, forwardedRef) => {
|
|
188
198
|
const { themeMode } = useThemeContext();
|
|
189
199
|
const slots = defaultsDeep({}, _slots, defaultMarkdownSlots);
|
|
190
200
|
return (
|
|
191
201
|
<BaseTextEditor
|
|
192
202
|
ref={forwardedRef}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
maybeDebug(),
|
|
196
|
-
...extensions,
|
|
197
|
-
]}
|
|
203
|
+
readonly={readonly}
|
|
204
|
+
extensions={[markdownBundle({ readonly, themeMode, placeholder: slots?.editor?.placeholder }), ...extensions]}
|
|
198
205
|
slots={slots}
|
|
199
206
|
{...props}
|
|
200
207
|
/>
|
|
@@ -2,23 +2,15 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
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
5
|
// https://codemirror.net/examples/autocompletion
|
|
14
6
|
// https://codemirror.net/docs/ref/#autocomplete.autocompletion
|
|
15
7
|
// https://codemirror.net/docs/ref/#autocomplete.Completion
|
|
16
8
|
|
|
17
9
|
import {
|
|
18
10
|
autocompletion,
|
|
11
|
+
completionKeymap,
|
|
19
12
|
type Completion,
|
|
20
13
|
type CompletionContext,
|
|
21
|
-
completionKeymap,
|
|
22
14
|
type CompletionResult,
|
|
23
15
|
} from '@codemirror/autocomplete';
|
|
24
16
|
import { keymap } from '@codemirror/view';
|
|
@@ -51,21 +43,14 @@ export const autocomplete = ({ onSearch }: AutocompleteOptions) => {
|
|
|
51
43
|
// TODO(burdon): Optional decoration via addToOptions
|
|
52
44
|
override: [
|
|
53
45
|
(context: CompletionContext): CompletionResult | null => {
|
|
54
|
-
const
|
|
55
|
-
if (!
|
|
46
|
+
const match = context.matchBefore(/\w*/);
|
|
47
|
+
if (!match || (match.from === match.to && !context.explicit)) {
|
|
56
48
|
return null;
|
|
57
49
|
}
|
|
58
50
|
|
|
59
|
-
// TODO(burdon): Option to convert to links?
|
|
60
51
|
return {
|
|
61
|
-
from:
|
|
62
|
-
options: onSearch(
|
|
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
|
-
// ],
|
|
52
|
+
from: match.from,
|
|
53
|
+
options: onSearch(match.text.toLowerCase()),
|
|
69
54
|
};
|
|
70
55
|
},
|
|
71
56
|
],
|
|
@@ -6,23 +6,25 @@ import { closeBrackets } from '@codemirror/autocomplete';
|
|
|
6
6
|
import { bracketMatching, defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
|
7
7
|
import { type Extension } from '@codemirror/state';
|
|
8
8
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
9
|
-
import { EditorView, placeholder } from '@codemirror/view';
|
|
9
|
+
import { drawSelection, EditorView, placeholder } from '@codemirror/view';
|
|
10
10
|
|
|
11
11
|
import type { ThemeMode } from '@dxos/react-ui';
|
|
12
12
|
|
|
13
13
|
export type BasicBundleOptions = {
|
|
14
|
+
readonly?: boolean;
|
|
14
15
|
themeMode?: ThemeMode;
|
|
15
16
|
placeholder?: string;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
export const basicBundle = ({ themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
|
|
19
|
+
export const basicBundle = ({ readonly, themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
|
|
19
20
|
[
|
|
21
|
+
EditorView.lineWrapping,
|
|
22
|
+
|
|
20
23
|
// TODO(burdon): Compare with minimalSetup.
|
|
21
24
|
// https://codemirror.net/docs/ref/#codemirror.minimalSetup
|
|
22
25
|
bracketMatching(),
|
|
23
26
|
closeBrackets(),
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
drawSelection(),
|
|
26
28
|
_placeholder && placeholder(_placeholder),
|
|
27
29
|
|
|
28
30
|
// TODO(burdon): Is this required?
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
// Based on a demo by Joel Besada and Kushagra Gour.
|
|
4
|
+
// https://twitter.com/JoelBesada/status/670343885655293952
|
|
5
|
+
// https://github.com/chinchang/code-blast-codemirror/blob/master/code-blast.js
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import { type Extension } from '@codemirror/state';
|
|
9
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
10
|
+
import defaultsDeep from 'lodash.defaultsdeep';
|
|
11
|
+
|
|
12
|
+
import { invariant } from '@dxos/invariant';
|
|
13
|
+
|
|
14
|
+
export type BlastOptions = {
|
|
15
|
+
effect?: number;
|
|
16
|
+
maxParticles: number;
|
|
17
|
+
particleGravity: number;
|
|
18
|
+
particleAlphaFadeout: number;
|
|
19
|
+
particleSize: { min: number; max: number };
|
|
20
|
+
particleNumRange: { min: number; max: number };
|
|
21
|
+
particleVelocityRange: { x: [number, number]; y: [number, number] };
|
|
22
|
+
particleShrinkRate: number;
|
|
23
|
+
shakeIntensity: number;
|
|
24
|
+
color?: () => number[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const defaultOptions: BlastOptions = {
|
|
28
|
+
effect: 2,
|
|
29
|
+
maxParticles: 200,
|
|
30
|
+
particleGravity: 0.08,
|
|
31
|
+
particleAlphaFadeout: 0.996,
|
|
32
|
+
particleSize: { min: 2, max: 8 },
|
|
33
|
+
particleNumRange: { min: 5, max: 10 },
|
|
34
|
+
particleVelocityRange: { x: [-1, 1], y: [-3.5, -1.5] },
|
|
35
|
+
particleShrinkRate: 0.95,
|
|
36
|
+
shakeIntensity: 5,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const blast = (options: Partial<BlastOptions> = defaultOptions): Extension => {
|
|
40
|
+
let blaster: Blaster | undefined;
|
|
41
|
+
let last = 0;
|
|
42
|
+
|
|
43
|
+
const getPoint = (view: EditorView, pos: number) => {
|
|
44
|
+
const { left: x = 0, top: y = 0 } = view.coordsForChar(pos) ?? {};
|
|
45
|
+
const element = document.elementFromPoint(x, y);
|
|
46
|
+
|
|
47
|
+
const { offsetLeft: left = 0, offsetTop: top = 0 } = view.scrollDOM.parentElement ?? {};
|
|
48
|
+
const point = { x: x - left, y: y - top };
|
|
49
|
+
|
|
50
|
+
return { element, point };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return [
|
|
54
|
+
// Cursor moved.
|
|
55
|
+
EditorView.updateListener.of((update) => {
|
|
56
|
+
// NOTE: The MarkdownEditor may recreated the EditorView.
|
|
57
|
+
if (blaster?.node !== update.view.scrollDOM) {
|
|
58
|
+
if (blaster) {
|
|
59
|
+
blaster.destroy();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
blaster = new Blaster(
|
|
63
|
+
update.view.scrollDOM,
|
|
64
|
+
defaultsDeep(
|
|
65
|
+
{
|
|
66
|
+
particleGravity: 0.2,
|
|
67
|
+
particleShrinkRate: 0.995,
|
|
68
|
+
color: () => [100 + Math.random() * 100, 0, 0],
|
|
69
|
+
},
|
|
70
|
+
options,
|
|
71
|
+
defaultOptions,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
blaster.initialize();
|
|
75
|
+
blaster.start(); // TODO(burdon): Stop/clean-up.
|
|
76
|
+
} else {
|
|
77
|
+
blaster.resize();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const current = update.state.selection.main.head;
|
|
81
|
+
if (current !== last) {
|
|
82
|
+
last = current;
|
|
83
|
+
// TODO(burdon): Null if end of line.
|
|
84
|
+
const { element, point } = getPoint(update.view, current - 1);
|
|
85
|
+
if (element && point) {
|
|
86
|
+
blaster.spawn({ element, point });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
|
|
91
|
+
keymap.of([
|
|
92
|
+
{
|
|
93
|
+
any: (_, event) => {
|
|
94
|
+
if (blaster) {
|
|
95
|
+
if (event.key === 'Enter' && event.shiftKey) {
|
|
96
|
+
blaster.shake({ time: 0.8 });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return false;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
]),
|
|
105
|
+
];
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
//
|
|
109
|
+
// Blaster (no CM deps).
|
|
110
|
+
//
|
|
111
|
+
|
|
112
|
+
class Blaster {
|
|
113
|
+
private readonly _effect: Effect;
|
|
114
|
+
|
|
115
|
+
_canvas: HTMLCanvasElement | undefined;
|
|
116
|
+
_ctx: CanvasRenderingContext2D | undefined | null;
|
|
117
|
+
|
|
118
|
+
_running = false;
|
|
119
|
+
_lastTime: number | undefined;
|
|
120
|
+
_shakeTime = 0;
|
|
121
|
+
_shakeTimeMax = 0;
|
|
122
|
+
|
|
123
|
+
_particles: Particle[] = [];
|
|
124
|
+
_particlePointer = 0;
|
|
125
|
+
|
|
126
|
+
_lastPoint = { x: 0, y: 0 };
|
|
127
|
+
|
|
128
|
+
constructor(private readonly _node: HTMLElement, private readonly _options: BlastOptions) {
|
|
129
|
+
this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get node() {
|
|
133
|
+
return this._node;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
initialize() {
|
|
137
|
+
// console.log('initialize');
|
|
138
|
+
invariant(!this._canvas && !this._ctx);
|
|
139
|
+
|
|
140
|
+
this._canvas = document.createElement('canvas');
|
|
141
|
+
this._canvas.id = 'code-blast-canvas';
|
|
142
|
+
this._canvas.style.position = 'absolute';
|
|
143
|
+
this._canvas.style.zIndex = '0';
|
|
144
|
+
this._canvas.style.pointerEvents = 'none';
|
|
145
|
+
// this._canvas.style.border = '2px dashed red';
|
|
146
|
+
|
|
147
|
+
this._ctx = this._canvas.getContext('2d');
|
|
148
|
+
|
|
149
|
+
const parent = this._node.parentElement?.parentElement;
|
|
150
|
+
parent?.appendChild(this._canvas);
|
|
151
|
+
|
|
152
|
+
this.resize();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
destroy() {
|
|
156
|
+
this.stop();
|
|
157
|
+
// console.log('destroy');
|
|
158
|
+
if (this._canvas) {
|
|
159
|
+
this._canvas.remove();
|
|
160
|
+
this._canvas = undefined;
|
|
161
|
+
this._ctx = undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
resize() {
|
|
166
|
+
if (this._node.parentElement && this._canvas) {
|
|
167
|
+
const { offsetLeft: x, offsetTop: y, offsetWidth: width, offsetHeight: height } = this._node.parentElement;
|
|
168
|
+
this._canvas.style.top = `${y}px`;
|
|
169
|
+
this._canvas.style.left = `${x}px`;
|
|
170
|
+
this._canvas.width = width;
|
|
171
|
+
this._canvas.height = height;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
start() {
|
|
176
|
+
// console.log('start');
|
|
177
|
+
invariant(this._canvas && this._ctx);
|
|
178
|
+
this._running = true;
|
|
179
|
+
this.loop();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
stop() {
|
|
183
|
+
// console.log('stop');
|
|
184
|
+
this._running = false;
|
|
185
|
+
this._node.style.transform = 'translate(0px, 0px)';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
loop() {
|
|
189
|
+
if (!this._running || !this._canvas || !this._ctx) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this._ctx.clearRect(0, 0, this._canvas.width ?? 0, this._canvas.height ?? 0);
|
|
194
|
+
|
|
195
|
+
// Calculate the delta from the previous frame.
|
|
196
|
+
const now = new Date().getTime();
|
|
197
|
+
this._lastTime ??= now;
|
|
198
|
+
const dt = (now - this._lastTime) / 1000;
|
|
199
|
+
this._lastTime = now;
|
|
200
|
+
|
|
201
|
+
if (this._shakeTime > 0) {
|
|
202
|
+
this._shakeTime -= dt;
|
|
203
|
+
const magnitude = (this._shakeTime / this._shakeTimeMax) * this._options.shakeIntensity;
|
|
204
|
+
const shakeX = random(-magnitude, magnitude);
|
|
205
|
+
const shakeY = random(-magnitude, magnitude);
|
|
206
|
+
this._node!.style.transform = `translate(${shakeX}px,${shakeY}px)`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.drawParticles();
|
|
210
|
+
|
|
211
|
+
requestAnimationFrame(this.loop.bind(this));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
shake = throttle<{ time: number }>(({ time }) => {
|
|
215
|
+
this._shakeTime = this._shakeTimeMax || time;
|
|
216
|
+
this._shakeTimeMax = time;
|
|
217
|
+
}, 100);
|
|
218
|
+
|
|
219
|
+
spawn = throttle<{ element: Element; point: { x: number; y: number } }>(({ element, point }) => {
|
|
220
|
+
const color = getRGBComponents(element, this._options.color);
|
|
221
|
+
const numParticles = random(this._options.particleNumRange.min, this._options.particleNumRange.max);
|
|
222
|
+
const dir = this._lastPoint.x === point.x ? 0 : this._lastPoint.x < point.x ? 1 : -1;
|
|
223
|
+
this._lastPoint = point;
|
|
224
|
+
for (let i = numParticles; i--; i > 0) {
|
|
225
|
+
this._particles[this._particlePointer] = this._effect.create(point.x - dir * 16, point.y, color);
|
|
226
|
+
this._particlePointer = (this._particlePointer + 1) % this._options.maxParticles;
|
|
227
|
+
}
|
|
228
|
+
}, 100);
|
|
229
|
+
|
|
230
|
+
drawParticles() {
|
|
231
|
+
for (let i = this._particles.length; i--; i > 0) {
|
|
232
|
+
const particle = this._particles[i];
|
|
233
|
+
if (!particle) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (particle.alpha < 0.01 || particle.size <= 0.5) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this._effect.update(this._ctx!, particle);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//
|
|
247
|
+
// Effects
|
|
248
|
+
//
|
|
249
|
+
|
|
250
|
+
type Particle = {
|
|
251
|
+
x: number;
|
|
252
|
+
y: number;
|
|
253
|
+
vx: number;
|
|
254
|
+
vy: number;
|
|
255
|
+
size: number;
|
|
256
|
+
color: (string | number)[];
|
|
257
|
+
alpha: number;
|
|
258
|
+
theta?: number;
|
|
259
|
+
drag?: number;
|
|
260
|
+
wander?: number;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
abstract class Effect {
|
|
264
|
+
constructor(protected readonly _options: BlastOptions) {}
|
|
265
|
+
abstract create(x: number, y: number, color: Particle['color']): Particle;
|
|
266
|
+
abstract update(ctx: CanvasRenderingContext2D, particle: Particle): void;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
class Effect1 extends Effect {
|
|
270
|
+
create(x: number, y: number, color: Particle['color']) {
|
|
271
|
+
return {
|
|
272
|
+
x,
|
|
273
|
+
y: y + 10,
|
|
274
|
+
vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
|
|
275
|
+
vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
|
|
276
|
+
size: random(this._options.particleSize.min, this._options.particleSize.max),
|
|
277
|
+
color,
|
|
278
|
+
alpha: 1,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
update(ctx: CanvasRenderingContext2D, particle: Particle) {
|
|
283
|
+
particle.vy += this._options.particleGravity;
|
|
284
|
+
particle.x += particle.vx;
|
|
285
|
+
particle.y += particle.vy;
|
|
286
|
+
particle.alpha *= this._options.particleAlphaFadeout;
|
|
287
|
+
|
|
288
|
+
ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
|
|
289
|
+
ctx.fillRect(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, particle.size);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Based on Soulwire's demo.
|
|
295
|
+
* http://codepen.io/soulwire/pen/foktm
|
|
296
|
+
*/
|
|
297
|
+
class Effect2 extends Effect {
|
|
298
|
+
create(x: number, y: number, color: Particle['color']) {
|
|
299
|
+
return {
|
|
300
|
+
x,
|
|
301
|
+
y: y + 10,
|
|
302
|
+
vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
|
|
303
|
+
vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
|
|
304
|
+
size: random(this._options.particleSize.min, this._options.particleSize.max),
|
|
305
|
+
color,
|
|
306
|
+
alpha: 1,
|
|
307
|
+
theta: (random(0, 360) * Math.PI) / 180,
|
|
308
|
+
drag: 0.92,
|
|
309
|
+
wander: 0.15,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
update(ctx: CanvasRenderingContext2D, particle: Particle) {
|
|
314
|
+
particle.vy += this._options.particleGravity;
|
|
315
|
+
particle.x += particle.vx;
|
|
316
|
+
particle.y += particle.vy;
|
|
317
|
+
particle.vx *= particle.drag!;
|
|
318
|
+
particle.vy *= particle.drag!;
|
|
319
|
+
particle.theta! += random(-0.5, 0.5);
|
|
320
|
+
particle.vx += Math.sin(particle.theta!) * 0.1;
|
|
321
|
+
particle.vy += Math.cos(particle.theta!) * 0.1;
|
|
322
|
+
particle.size *= this._options.particleShrinkRate;
|
|
323
|
+
particle.alpha *= this._options.particleAlphaFadeout;
|
|
324
|
+
|
|
325
|
+
ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
|
|
326
|
+
ctx.beginPath();
|
|
327
|
+
ctx.arc(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, 0, 2 * Math.PI);
|
|
328
|
+
ctx.fill();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
//
|
|
333
|
+
// Utils
|
|
334
|
+
//
|
|
335
|
+
|
|
336
|
+
function throttle<T>(callback: (arg: T) => void, limit: number): (arg: T) => void {
|
|
337
|
+
let wait = false;
|
|
338
|
+
return function (...args: any[]) {
|
|
339
|
+
if (!wait) {
|
|
340
|
+
// @ts-ignore
|
|
341
|
+
callback.apply(this, args);
|
|
342
|
+
wait = true;
|
|
343
|
+
setTimeout(() => {
|
|
344
|
+
wait = false;
|
|
345
|
+
}, limit);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const getRGBComponents = (node: Element, color: BlastOptions['color']): Particle['color'] => {
|
|
351
|
+
if (typeof color === 'function') {
|
|
352
|
+
return color();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const bgColor = getComputedStyle(node).color;
|
|
356
|
+
if (bgColor) {
|
|
357
|
+
const x = bgColor.match(/(\d+), (\d+), (\d+)/)?.slice(1);
|
|
358
|
+
if (x) {
|
|
359
|
+
return x;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return [50, 50, 50];
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const random = (min: number, max: number) => {
|
|
367
|
+
if (!max) {
|
|
368
|
+
max = min;
|
|
369
|
+
min = 0;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return min + ~~(Math.random() * (max - min + 1));
|
|
373
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Range } from '@codemirror/state';
|
|
6
|
+
import {
|
|
7
|
+
ViewPlugin,
|
|
8
|
+
type DecorationSet,
|
|
9
|
+
EditorView,
|
|
10
|
+
type ViewUpdate,
|
|
11
|
+
Decoration,
|
|
12
|
+
type BlockInfo,
|
|
13
|
+
} from '@codemirror/view';
|
|
14
|
+
import get from 'lodash.get';
|
|
15
|
+
|
|
16
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
17
|
+
|
|
18
|
+
import { tokens } from '../../../styles';
|
|
19
|
+
|
|
20
|
+
const CODE_REGEX = /```[\s\S]*?```/gs;
|
|
21
|
+
|
|
22
|
+
// TODO(burdon): Reconcile with theme.
|
|
23
|
+
const styles = EditorView.baseTheme({
|
|
24
|
+
'& .cm-code-block': {
|
|
25
|
+
display: 'block',
|
|
26
|
+
paddingInline: '4px !important',
|
|
27
|
+
},
|
|
28
|
+
'&light .cm-code-block': {
|
|
29
|
+
background: get(tokens, 'extend.colors.neutral.50'),
|
|
30
|
+
},
|
|
31
|
+
'&dark .cm-code-block': {
|
|
32
|
+
background: get(tokens, 'extend.colors.neutral.850'),
|
|
33
|
+
},
|
|
34
|
+
'& .cm-code-block-first': {
|
|
35
|
+
paddingTop: '4px !important',
|
|
36
|
+
borderTopLeftRadius: '4px',
|
|
37
|
+
borderTopRightRadius: '4px',
|
|
38
|
+
},
|
|
39
|
+
'& .cm-code-block-last': {
|
|
40
|
+
paddingBottom: '4px !important',
|
|
41
|
+
borderBottomLeftRadius: '4px',
|
|
42
|
+
borderBottomRightRadius: '4px',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const getLineRange = (lines: BlockInfo[], from: number, to: number) => {
|
|
47
|
+
const start = lines.findIndex((line) => line.from >= from);
|
|
48
|
+
const end = lines.findIndex((line) => line.to >= to);
|
|
49
|
+
return [start, end];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const code = () => {
|
|
53
|
+
const getDecorations = (view: EditorView) => {
|
|
54
|
+
const decorations: Range<Decoration>[] = [];
|
|
55
|
+
if (view.state.readOnly) {
|
|
56
|
+
const text = view.state.doc.sliceString(0);
|
|
57
|
+
const matches = text.matchAll(CODE_REGEX);
|
|
58
|
+
const blocks = view.viewportLineBlocks;
|
|
59
|
+
for (const match of matches) {
|
|
60
|
+
const range = getLineRange(blocks, match.index!, match.index! + match[0].length);
|
|
61
|
+
for (let i = range[0]; i <= range[1]; i++) {
|
|
62
|
+
const block = blocks[i];
|
|
63
|
+
decorations.push(
|
|
64
|
+
Decoration.line({
|
|
65
|
+
class: mx(
|
|
66
|
+
'cm-code-block',
|
|
67
|
+
i === range[0] && 'cm-code-block-first',
|
|
68
|
+
i === range[1] && 'cm-code-block-last',
|
|
69
|
+
),
|
|
70
|
+
}).range(block.from),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Decoration.set(decorations);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
ViewPlugin.fromClass(
|
|
81
|
+
class {
|
|
82
|
+
decorations: DecorationSet;
|
|
83
|
+
constructor(view: EditorView) {
|
|
84
|
+
this.decorations = getDecorations(view);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
update(update: ViewUpdate) {
|
|
88
|
+
if (update.docChanged || update.viewportChanged) {
|
|
89
|
+
this.decorations = getDecorations(update.view);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
decorations: (value) => value.decorations,
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
styles,
|
|
98
|
+
];
|
|
99
|
+
};
|