@dxos/react-ui-editor 0.3.10-main.c5193cc → 0.3.10-main.c9b5e9b
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 +615 -380
- 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 +6 -3
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -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/basic.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts +1 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -1
- 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 +1 -1
- package/dist/types/src/components/TextEditor/extensions/comments.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/index.d.ts +2 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +8 -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 +3 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
- 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/hooks/yjs/yjs.stories.d.ts +1 -1
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -1
- 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 +48 -32
- package/src/components/TextEditor/TextEditor.stories.tsx +12 -1
- package/src/components/TextEditor/TextEditor.tsx +18 -1
- package/src/components/TextEditor/extensions/basic.ts +4 -3
- package/src/components/TextEditor/extensions/blast.ts +14 -28
- package/src/components/TextEditor/extensions/code.ts +99 -0
- package/src/components/TextEditor/extensions/comments.ts +22 -40
- package/src/components/TextEditor/extensions/experimental.tsx +15 -36
- package/src/components/TextEditor/extensions/highlight.ts +128 -0
- package/src/components/TextEditor/extensions/index.ts +2 -0
- package/src/components/TextEditor/extensions/link.ts +1 -2
- package/src/components/TextEditor/extensions/markdown/bundle.ts +19 -19
- package/src/components/TextEditor/extensions/markdown/highlight.ts +37 -11
- package/src/components/TextEditor/extensions/table.ts +44 -48
- package/src/components/TextEditor/extensions/tasklist.ts +9 -9
- package/src/components/TextEditor/themes/default.ts +43 -2
- package/src/hooks/automerge/automerge.stories.tsx +1 -0
- package/src/hooks/useTextModel.ts +9 -0
- package/src/hooks/yjs/yjs.stories.tsx +1 -1
- package/src/styles/markdown.ts +5 -4
- package/src/testing/replicator.ts +1 -0
|
@@ -32,9 +32,15 @@ import {
|
|
|
32
32
|
blast,
|
|
33
33
|
demo,
|
|
34
34
|
defaultOptions,
|
|
35
|
+
highlight,
|
|
36
|
+
code,
|
|
35
37
|
} from './extensions';
|
|
36
38
|
import { useTextModel } from '../../hooks';
|
|
37
39
|
|
|
40
|
+
// Extensions:
|
|
41
|
+
// TODO(burdon): Table of contents.
|
|
42
|
+
// TODO(burdon): Front-matter
|
|
43
|
+
|
|
38
44
|
const str = (...lines: string[]) => lines.join('\n');
|
|
39
45
|
|
|
40
46
|
const num = () => faker.number.int({ min: 0, max: 9999 }).toLocaleString();
|
|
@@ -78,6 +84,7 @@ const text = {
|
|
|
78
84
|
'```tsx',
|
|
79
85
|
'const Component = () => {',
|
|
80
86
|
' const x = 100;',
|
|
87
|
+
'',
|
|
81
88
|
' return () => <div>Test</div>;',
|
|
82
89
|
'};',
|
|
83
90
|
'',
|
|
@@ -93,24 +100,26 @@ const text = {
|
|
|
93
100
|
'',
|
|
94
101
|
),
|
|
95
102
|
|
|
96
|
-
headings: str(
|
|
97
|
-
...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
|
|
98
|
-
),
|
|
99
|
-
|
|
100
|
-
paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
|
|
101
|
-
|
|
102
103
|
table: str(
|
|
103
104
|
'# Table',
|
|
104
105
|
'',
|
|
105
|
-
`| ${faker.lorem.word()} | ${faker.lorem.word()} | ${faker.lorem.word()} |`,
|
|
106
|
-
'
|
|
107
|
-
`| ${num()} | ${num()} | ${num()} |`,
|
|
108
|
-
`| ${num()} | ${num()} | ${num()} |`,
|
|
109
|
-
`| ${num()} | ${num()} | ${num()} |`,
|
|
106
|
+
`| ${faker.lorem.word().padStart(8)} | ${faker.lorem.word().padStart(8)} | ${faker.lorem.word().padStart(8)} |`,
|
|
107
|
+
'|----------|----------|----------|',
|
|
108
|
+
`| ${num().padStart(8)} | ${num().padStart(8)} | ${num().padStart(8)} |`,
|
|
109
|
+
`| ${num().padStart(8)} | ${num().padStart(8)} | ${num().padStart(8)} |`,
|
|
110
|
+
`| ${num().padStart(8)} | ${num().padStart(8)} | ${num().padStart(8)} |`,
|
|
110
111
|
'', // TODO(burdon): Possible GFM parsing bug if no newline?
|
|
111
112
|
),
|
|
112
113
|
|
|
113
114
|
image: str('# Image', '', ''),
|
|
115
|
+
|
|
116
|
+
headings: str(
|
|
117
|
+
...[1, 2, 3, 4, 5, 6].map((level) => ['#'.repeat(level) + ` Heading ${level}`, faker.lorem.sentences(), '']).flat(),
|
|
118
|
+
),
|
|
119
|
+
|
|
120
|
+
paragraphs: str(...faker.helpers.multiple(() => [faker.lorem.paragraph(), ''], { count: 3 }).flat()),
|
|
121
|
+
|
|
122
|
+
footer: str('', '', '', '', '')
|
|
114
123
|
};
|
|
115
124
|
|
|
116
125
|
const document = str(
|
|
@@ -138,6 +147,8 @@ const document = str(
|
|
|
138
147
|
text.table,
|
|
139
148
|
'---',
|
|
140
149
|
text.image,
|
|
150
|
+
'',
|
|
151
|
+
text.footer,
|
|
141
152
|
);
|
|
142
153
|
|
|
143
154
|
const links = [
|
|
@@ -173,6 +184,7 @@ const onRender: LinkOptions['onRender'] = (el, url) => {
|
|
|
173
184
|
);
|
|
174
185
|
};
|
|
175
186
|
|
|
187
|
+
// TODO(burdon): Pass in model.
|
|
176
188
|
const Story = ({
|
|
177
189
|
text,
|
|
178
190
|
automerge,
|
|
@@ -205,14 +217,15 @@ export default {
|
|
|
205
217
|
};
|
|
206
218
|
|
|
207
219
|
const extensions = [
|
|
208
|
-
link({ onRender }),
|
|
209
|
-
tooltip({ onHover }),
|
|
210
|
-
tasklist(),
|
|
211
|
-
table(),
|
|
212
|
-
image(),
|
|
213
220
|
autocomplete({
|
|
214
221
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
215
222
|
}),
|
|
223
|
+
code(),
|
|
224
|
+
image(),
|
|
225
|
+
link({ onRender }),
|
|
226
|
+
table(),
|
|
227
|
+
tasklist(),
|
|
228
|
+
tooltip({ onHover }),
|
|
216
229
|
];
|
|
217
230
|
|
|
218
231
|
export const Default = {
|
|
@@ -223,32 +236,30 @@ export const Readonly = {
|
|
|
223
236
|
render: () => <Story text={document} extensions={extensions} readonly />,
|
|
224
237
|
};
|
|
225
238
|
|
|
226
|
-
export const Simple = {
|
|
227
|
-
render: () => <Story text={document} />,
|
|
228
|
-
};
|
|
229
|
-
|
|
230
239
|
export const Tooltips = {
|
|
231
|
-
render: () => <Story text={text.links} extensions={[tooltip({ onHover })]} />,
|
|
240
|
+
render: () => <Story text={str(text.links, text.footer)} extensions={[tooltip({ onHover })]} />,
|
|
232
241
|
};
|
|
233
242
|
|
|
234
243
|
export const Links = {
|
|
235
|
-
render: () => <Story text={text.links} extensions={[link({ onRender })]} />,
|
|
244
|
+
render: () => <Story text={str(text.links, text.footer)} extensions={[link({ onRender })]} />,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const Code = {
|
|
248
|
+
render: () => <Story text={str(text.code, text.footer)} extensions={[code()]} readonly />,
|
|
236
249
|
};
|
|
237
250
|
|
|
238
251
|
export const Table = {
|
|
239
|
-
render: () => (
|
|
240
|
-
<Story text={str('This is a table', '', text.table, '', text.table, '', '')} readonly extensions={[table()]} />
|
|
241
|
-
),
|
|
252
|
+
render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
|
|
242
253
|
};
|
|
243
254
|
|
|
244
255
|
export const Image = {
|
|
245
|
-
render: () => <Story text={str(
|
|
256
|
+
render: () => <Story text={str(text.image, text.footer)} readonly extensions={[image()]} />,
|
|
246
257
|
};
|
|
247
258
|
|
|
248
259
|
export const TaskList = {
|
|
249
260
|
render: () => (
|
|
250
261
|
<Story
|
|
251
|
-
text={str(text.tasks, '', text.list)}
|
|
262
|
+
text={str(text.tasks, '', text.list, text.footer)}
|
|
252
263
|
extensions={[
|
|
253
264
|
tasklist(),
|
|
254
265
|
listener({
|
|
@@ -264,7 +275,7 @@ export const TaskList = {
|
|
|
264
275
|
export const Autocomplete = {
|
|
265
276
|
render: () => (
|
|
266
277
|
<Story
|
|
267
|
-
text={str('# Autocomplete', '', '
|
|
278
|
+
text={str('# Autocomplete', '', 'Press CTRL-SPACE', text.footer)}
|
|
268
279
|
extensions={[
|
|
269
280
|
link({ onRender }),
|
|
270
281
|
autocomplete({
|
|
@@ -280,7 +291,7 @@ const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', '
|
|
|
280
291
|
export const Mention = {
|
|
281
292
|
render: () => (
|
|
282
293
|
<Story
|
|
283
|
-
text={str('# Mention', '', '
|
|
294
|
+
text={str('# Mention', '', 'Type @...', text.footer)}
|
|
284
295
|
extensions={[
|
|
285
296
|
mention({
|
|
286
297
|
onSearch: (text) => names.filter((name) => name.toLowerCase().startsWith(text.toLowerCase())),
|
|
@@ -294,14 +305,15 @@ const mark = () => `[^${PublicKey.random().toHex()}]`;
|
|
|
294
305
|
export const Comments = {
|
|
295
306
|
render: () => (
|
|
296
307
|
<Story
|
|
297
|
-
text={str(
|
|
308
|
+
text={str(text.paragraphs, mark(), '', mark(), text.footer)}
|
|
298
309
|
extensions={[
|
|
299
310
|
comments({
|
|
300
311
|
onCreate: () => PublicKey.random().toHex(),
|
|
301
312
|
onUpdate: (info) => {
|
|
302
|
-
console.log('update', info);
|
|
313
|
+
// console.log('update', info);
|
|
303
314
|
},
|
|
304
315
|
}),
|
|
316
|
+
highlight(),
|
|
305
317
|
]}
|
|
306
318
|
/>
|
|
307
319
|
),
|
|
@@ -322,7 +334,7 @@ export const Diagnostics = {
|
|
|
322
334
|
};
|
|
323
335
|
|
|
324
336
|
export const Demo = {
|
|
325
|
-
render: () => <Story text={text.paragraphs} extensions={[demo()]} />,
|
|
337
|
+
render: () => <Story text={str(text.paragraphs, text.footer)} extensions={[demo()]} />,
|
|
326
338
|
};
|
|
327
339
|
|
|
328
340
|
export const Blast = {
|
|
@@ -349,3 +361,7 @@ export const Blast = {
|
|
|
349
361
|
/>
|
|
350
362
|
),
|
|
351
363
|
};
|
|
364
|
+
|
|
365
|
+
export const NoExtensions = {
|
|
366
|
+
render: () => <Story text={document} />,
|
|
367
|
+
};
|
|
@@ -57,6 +57,17 @@ export default {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
export const Default = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<Story
|
|
62
|
+
slots={defaultsDeep(
|
|
63
|
+
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
64
|
+
defaultSlots,
|
|
65
|
+
)}
|
|
66
|
+
/>
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Text = {
|
|
60
71
|
render: () => (
|
|
61
72
|
<Story
|
|
62
73
|
text={initialText}
|
|
@@ -71,12 +82,12 @@ export const Default = {
|
|
|
71
82
|
export const Automerge = {
|
|
72
83
|
render: () => (
|
|
73
84
|
<Story
|
|
85
|
+
automerge
|
|
74
86
|
text={initialText}
|
|
75
87
|
slots={defaultsDeep(
|
|
76
88
|
{ editor: { theme: textTheme, placeholder: 'Enter text...' } } satisfies TextEditorSlots,
|
|
77
89
|
defaultSlots,
|
|
78
90
|
)}
|
|
79
|
-
automerge
|
|
80
91
|
/>
|
|
81
92
|
),
|
|
82
93
|
};
|
|
@@ -88,6 +88,12 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
88
88
|
}
|
|
89
89
|
}, [awareness, peer, themeMode]);
|
|
90
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
|
+
|
|
91
97
|
useEffect(() => {
|
|
92
98
|
if (!root) {
|
|
93
99
|
return;
|
|
@@ -102,6 +108,7 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
102
108
|
editorMode === 'vim' && vim(),
|
|
103
109
|
|
|
104
110
|
// Theme.
|
|
111
|
+
// TODO(burdon): Make optional.
|
|
105
112
|
EditorView.baseTheme(defaultTheme),
|
|
106
113
|
EditorView.theme(slots?.editor?.theme ?? {}),
|
|
107
114
|
// TODO(burdon): themeMode doesn't change in storybooks.
|
|
@@ -119,7 +126,17 @@ export const BaseTextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
119
126
|
// If the new state is derived from the old state, it will likely not be visible other than the cursor resetting.
|
|
120
127
|
// Ideally this should not be hit except when changing between text objects.
|
|
121
128
|
view?.destroy();
|
|
122
|
-
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
|
+
);
|
|
123
140
|
setState(state);
|
|
124
141
|
|
|
125
142
|
return () => {
|
|
@@ -6,7 +6,7 @@ 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
|
|
|
@@ -18,12 +18,13 @@ export type BasicBundleOptions = {
|
|
|
18
18
|
|
|
19
19
|
export const basicBundle = ({ readonly, themeMode, placeholder: _placeholder }: BasicBundleOptions): Extension[] =>
|
|
20
20
|
[
|
|
21
|
+
EditorView.lineWrapping,
|
|
22
|
+
|
|
21
23
|
// TODO(burdon): Compare with minimalSetup.
|
|
22
24
|
// https://codemirror.net/docs/ref/#codemirror.minimalSetup
|
|
23
25
|
bracketMatching(),
|
|
24
26
|
closeBrackets(),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
drawSelection(),
|
|
27
28
|
_placeholder && placeholder(_placeholder),
|
|
28
29
|
|
|
29
30
|
// TODO(burdon): Is this required?
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// https://github.com/chinchang/code-blast-codemirror/blob/master/code-blast.js
|
|
6
6
|
//
|
|
7
7
|
|
|
8
|
-
import { type Extension
|
|
8
|
+
import { type Extension } from '@codemirror/state';
|
|
9
9
|
import { EditorView, keymap } from '@codemirror/view';
|
|
10
10
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
11
11
|
|
|
@@ -36,7 +36,7 @@ export const defaultOptions: BlastOptions = {
|
|
|
36
36
|
shakeIntensity: 5,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
export const blast = (options: Partial<BlastOptions>): Extension => {
|
|
39
|
+
export const blast = (options: Partial<BlastOptions> = defaultOptions): Extension => {
|
|
40
40
|
let blaster: Blaster | undefined;
|
|
41
41
|
let last = 0;
|
|
42
42
|
|
|
@@ -77,39 +77,25 @@ export const blast = (options: Partial<BlastOptions>): Extension => {
|
|
|
77
77
|
blaster.resize();
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
const current = update.
|
|
80
|
+
const current = update.state.selection.main.head;
|
|
81
81
|
if (current !== last) {
|
|
82
82
|
last = current;
|
|
83
83
|
// TODO(burdon): Null if end of line.
|
|
84
|
-
const { element, point } = getPoint(update.view, current);
|
|
84
|
+
const { element, point } = getPoint(update.view, current - 1);
|
|
85
85
|
if (element && point) {
|
|
86
86
|
blaster.spawn({ element, point });
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
}),
|
|
90
90
|
|
|
91
|
-
// Document changed.
|
|
92
|
-
StateField.define({
|
|
93
|
-
create: () => null,
|
|
94
|
-
update: (_value, transaction) => {
|
|
95
|
-
if (blaster && transaction.docChanged) {
|
|
96
|
-
// TODO(burdon): Only on delete.
|
|
97
|
-
const view = EditorView.findFromDOM(blaster.node)!;
|
|
98
|
-
const { element, point } = getPoint(view, transaction.selection!.main.head);
|
|
99
|
-
if (element && point) {
|
|
100
|
-
blaster.spawn({ element, point });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return null;
|
|
105
|
-
},
|
|
106
|
-
}),
|
|
107
|
-
|
|
108
91
|
keymap.of([
|
|
109
92
|
{
|
|
110
|
-
any: (
|
|
111
|
-
if (blaster
|
|
112
|
-
|
|
93
|
+
any: (_, event) => {
|
|
94
|
+
if (blaster) {
|
|
95
|
+
if (event.key === 'Enter' && event.shiftKey) {
|
|
96
|
+
blaster.shake({ time: 0.8 });
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
113
99
|
}
|
|
114
100
|
|
|
115
101
|
return false;
|
|
@@ -124,11 +110,11 @@ export const blast = (options: Partial<BlastOptions>): Extension => {
|
|
|
124
110
|
//
|
|
125
111
|
|
|
126
112
|
class Blaster {
|
|
113
|
+
private readonly _effect: Effect;
|
|
114
|
+
|
|
127
115
|
_canvas: HTMLCanvasElement | undefined;
|
|
128
116
|
_ctx: CanvasRenderingContext2D | undefined | null;
|
|
129
117
|
|
|
130
|
-
private readonly _effect: Effect;
|
|
131
|
-
|
|
132
118
|
_running = false;
|
|
133
119
|
_lastTime: number | undefined;
|
|
134
120
|
_shakeTime = 0;
|
|
@@ -156,7 +142,7 @@ class Blaster {
|
|
|
156
142
|
this._canvas.style.position = 'absolute';
|
|
157
143
|
this._canvas.style.zIndex = '0';
|
|
158
144
|
this._canvas.style.pointerEvents = 'none';
|
|
159
|
-
// this._canvas.style.border = '2px
|
|
145
|
+
// this._canvas.style.border = '2px dashed red';
|
|
160
146
|
|
|
161
147
|
this._ctx = this._canvas.getContext('2d');
|
|
162
148
|
|
|
@@ -226,7 +212,7 @@ class Blaster {
|
|
|
226
212
|
}
|
|
227
213
|
|
|
228
214
|
shake = throttle<{ time: number }>(({ time }) => {
|
|
229
|
-
this._shakeTime = this._shakeTimeMax;
|
|
215
|
+
this._shakeTime = this._shakeTimeMax || time;
|
|
230
216
|
this._shakeTimeMax = time;
|
|
231
217
|
}, 100);
|
|
232
218
|
|
|
@@ -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
|
+
};
|
|
@@ -18,22 +18,18 @@ import {
|
|
|
18
18
|
import { debounce } from '@dxos/async';
|
|
19
19
|
import { invariant } from '@dxos/invariant';
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// TODO(burdon): Import note (performance):
|
|
35
|
-
// Easily track & remove content with decorations
|
|
36
|
-
// https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
|
|
21
|
+
// TODO(burdon): Reconcile with theme.
|
|
22
|
+
const styles = EditorView.baseTheme({
|
|
23
|
+
'& .cm-bookmark': {
|
|
24
|
+
cursor: 'pointer',
|
|
25
|
+
margin: '4px',
|
|
26
|
+
padding: '4px',
|
|
27
|
+
backgroundColor: 'yellow',
|
|
28
|
+
},
|
|
29
|
+
'& .cm-bookmark-selected': {
|
|
30
|
+
backgroundColor: 'orange',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
37
33
|
|
|
38
34
|
class BookmarkWidget extends WidgetType {
|
|
39
35
|
constructor(private readonly _pos: number, private readonly _id: string, private readonly _handleClick: () => void) {
|
|
@@ -48,27 +44,12 @@ class BookmarkWidget extends WidgetType {
|
|
|
48
44
|
override toDOM() {
|
|
49
45
|
const span = document.createElement('span');
|
|
50
46
|
span.className = 'cm-bookmark';
|
|
51
|
-
|
|
52
|
-
// https://emojifinder.com/comment
|
|
53
|
-
span.textContent = '💬';
|
|
47
|
+
span.textContent = '§';
|
|
54
48
|
span.onclick = () => this._handleClick();
|
|
55
49
|
return span;
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
// TODO(burdon): Reconcile with theme.
|
|
60
|
-
const styles = EditorView.baseTheme({
|
|
61
|
-
'& .cm-bookmark': {
|
|
62
|
-
cursor: 'pointer',
|
|
63
|
-
margin: '4px',
|
|
64
|
-
padding: '4px',
|
|
65
|
-
backgroundColor: 'yellow',
|
|
66
|
-
},
|
|
67
|
-
'& .cm-bookmark-selected': {
|
|
68
|
-
backgroundColor: 'orange',
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
53
|
type CommentsInfo = {
|
|
73
54
|
active?: string;
|
|
74
55
|
items?: { id: string; pos: number; location: Rect | null }[];
|
|
@@ -76,7 +57,7 @@ type CommentsInfo = {
|
|
|
76
57
|
|
|
77
58
|
export type CommentsOptions = {
|
|
78
59
|
key?: string;
|
|
79
|
-
onCreate?: () => string | void;
|
|
60
|
+
onCreate?: (from: number, to: number) => string | void;
|
|
80
61
|
onUpdate?: (info: CommentsInfo) => void;
|
|
81
62
|
};
|
|
82
63
|
|
|
@@ -127,13 +108,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
127
108
|
key: options?.key ?? 'shift-meta-c',
|
|
128
109
|
run: (view) => {
|
|
129
110
|
// Insert footnote.
|
|
130
|
-
const
|
|
111
|
+
const { from, to, head } = view.state.selection.main;
|
|
112
|
+
const id = options.onCreate?.(from, to);
|
|
131
113
|
if (id) {
|
|
132
|
-
const pos = view.state.selection.main.head;
|
|
133
114
|
const tag = `[^${id}]`;
|
|
115
|
+
// TODO(burdon): Add selection.
|
|
134
116
|
view.dispatch({
|
|
135
|
-
changes: { from:
|
|
136
|
-
selection: { anchor:
|
|
117
|
+
changes: { from: head, insert: tag },
|
|
118
|
+
selection: { anchor: head + tag.length },
|
|
137
119
|
});
|
|
138
120
|
|
|
139
121
|
return true;
|
|
@@ -153,14 +135,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
153
135
|
return;
|
|
154
136
|
}
|
|
155
137
|
|
|
156
|
-
const view = update
|
|
157
|
-
const pos =
|
|
138
|
+
const { view, state } = update;
|
|
139
|
+
const pos = state.selection.main.head;
|
|
158
140
|
|
|
159
141
|
const decorations: { from: number; to: number; value: Decoration }[] = [];
|
|
160
142
|
const rangeSet = view.plugin(bookmarks)?.bookmarks;
|
|
161
143
|
let closest = Infinity;
|
|
162
144
|
// TODO(burdon): Handle multiple visible ranges in large documents.
|
|
163
|
-
const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to:
|
|
145
|
+
const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: state.doc.length };
|
|
164
146
|
rangeSet?.between(from, to, (from, to, value) => {
|
|
165
147
|
decorations.push({ from, to, value });
|
|
166
148
|
const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
|
|
@@ -13,28 +13,21 @@ import {
|
|
|
13
13
|
type ViewUpdate,
|
|
14
14
|
} from '@codemirror/view';
|
|
15
15
|
|
|
16
|
-
// TODO(burdon):
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Notes: Can't edit widget content (or cursor nav through them).
|
|
32
|
-
// - https://discuss.codemirror.net/t/focusing-inputs-within-widgets/5178/7
|
|
33
|
-
// Widgets intentionally always get set to contenteditable=false,
|
|
34
|
-
// or they would become part of CodeMirror’s editable content element.
|
|
35
|
-
// You should be able to introduce new contenteditable=true child elements inside of them.
|
|
16
|
+
// TODO(burdon): Reconcile with theme.
|
|
17
|
+
export const styles = EditorView.baseTheme({
|
|
18
|
+
'.cm-hyperlink-label': {
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
textDecoration: 'underline',
|
|
21
|
+
},
|
|
22
|
+
'.cm-hyperlink-bracket': {
|
|
23
|
+
opacity: 0.2,
|
|
24
|
+
},
|
|
25
|
+
'.cm-hyperlink-url': {
|
|
26
|
+
opacity: 0.5,
|
|
27
|
+
fontFamily: 'monospace',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
36
30
|
|
|
37
|
-
// TODO(burdon): URL match?
|
|
38
31
|
const markdownLinkRegexp = /\[([^\]]+)]\(([^)]+)\)(!?)/gi;
|
|
39
32
|
|
|
40
33
|
/**
|
|
@@ -99,7 +92,7 @@ export function hyperLinkExtension() {
|
|
|
99
92
|
}
|
|
100
93
|
},
|
|
101
94
|
{
|
|
102
|
-
decorations: (
|
|
95
|
+
decorations: (value) => value.decorations,
|
|
103
96
|
eventHandlers: {
|
|
104
97
|
// TODO(burdon): CLick to expand link.
|
|
105
98
|
// https://discuss.codemirror.net/t/decorator-iteration-on-click-event/4226/2
|
|
@@ -118,18 +111,4 @@ export function hyperLinkExtension() {
|
|
|
118
111
|
);
|
|
119
112
|
}
|
|
120
113
|
|
|
121
|
-
export const styles = EditorView.baseTheme({
|
|
122
|
-
'.cm-hyperlink-label': {
|
|
123
|
-
cursor: 'pointer',
|
|
124
|
-
textDecoration: 'underline',
|
|
125
|
-
},
|
|
126
|
-
'.cm-hyperlink-bracket': {
|
|
127
|
-
opacity: 0.2,
|
|
128
|
-
},
|
|
129
|
-
'.cm-hyperlink-url': {
|
|
130
|
-
opacity: 0.5,
|
|
131
|
-
fontFamily: 'monospace',
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
114
|
export const hyperlinkWidget: Extension = [hyperLinkExtension(), styles];
|