@dxos/react-ui-editor 0.3.10-main.92d1f2c → 0.3.10-main.9bbf73f
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 +381 -174
- 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/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.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 +41 -24
- 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 +9 -8
- package/src/components/TextEditor/extensions/experimental.tsx +1 -1
- package/src/components/TextEditor/extensions/highlight.ts +128 -0
- package/src/components/TextEditor/extensions/index.ts +2 -0
- package/src/components/TextEditor/extensions/markdown/bundle.ts +17 -17
- package/src/components/TextEditor/extensions/markdown/highlight.ts +1 -0
- package/src/components/TextEditor/extensions/table.ts +2 -5
- package/src/components/TextEditor/extensions/tasklist.ts +1 -1
- 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
|
'',
|
|
@@ -96,11 +103,11 @@ const text = {
|
|
|
96
103
|
table: str(
|
|
97
104
|
'# Table',
|
|
98
105
|
'',
|
|
99
|
-
`| ${faker.lorem.word()} | ${faker.lorem.word()} | ${faker.lorem.word()} |`,
|
|
100
|
-
'
|
|
101
|
-
`| ${num()} | ${num()} | ${num()} |`,
|
|
102
|
-
`| ${num()} | ${num()} | ${num()} |`,
|
|
103
|
-
`| ${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)} |`,
|
|
104
111
|
'', // TODO(burdon): Possible GFM parsing bug if no newline?
|
|
105
112
|
),
|
|
106
113
|
|
|
@@ -111,6 +118,8 @@ const text = {
|
|
|
111
118
|
),
|
|
112
119
|
|
|
113
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(
|
|
@@ -139,6 +148,7 @@ const document = str(
|
|
|
139
148
|
'---',
|
|
140
149
|
text.image,
|
|
141
150
|
'',
|
|
151
|
+
text.footer,
|
|
142
152
|
);
|
|
143
153
|
|
|
144
154
|
const links = [
|
|
@@ -174,6 +184,7 @@ const onRender: LinkOptions['onRender'] = (el, url) => {
|
|
|
174
184
|
);
|
|
175
185
|
};
|
|
176
186
|
|
|
187
|
+
// TODO(burdon): Pass in model.
|
|
177
188
|
const Story = ({
|
|
178
189
|
text,
|
|
179
190
|
automerge,
|
|
@@ -206,14 +217,15 @@ export default {
|
|
|
206
217
|
};
|
|
207
218
|
|
|
208
219
|
const extensions = [
|
|
209
|
-
link({ onRender }),
|
|
210
|
-
tooltip({ onHover }),
|
|
211
|
-
tasklist(),
|
|
212
|
-
table(),
|
|
213
|
-
image(),
|
|
214
220
|
autocomplete({
|
|
215
221
|
onSearch: (text) => links.filter(({ label }) => label.toLowerCase().includes(text.toLowerCase())),
|
|
216
222
|
}),
|
|
223
|
+
code(),
|
|
224
|
+
image(),
|
|
225
|
+
link({ onRender }),
|
|
226
|
+
table(),
|
|
227
|
+
tasklist(),
|
|
228
|
+
tooltip({ onHover }),
|
|
217
229
|
];
|
|
218
230
|
|
|
219
231
|
export const Default = {
|
|
@@ -224,30 +236,30 @@ export const Readonly = {
|
|
|
224
236
|
render: () => <Story text={document} extensions={extensions} readonly />,
|
|
225
237
|
};
|
|
226
238
|
|
|
227
|
-
export const Simple = {
|
|
228
|
-
render: () => <Story text={document} />,
|
|
229
|
-
};
|
|
230
|
-
|
|
231
239
|
export const Tooltips = {
|
|
232
|
-
render: () => <Story text={str(text.links,
|
|
240
|
+
render: () => <Story text={str(text.links, text.footer)} extensions={[tooltip({ onHover })]} />,
|
|
233
241
|
};
|
|
234
242
|
|
|
235
243
|
export const Links = {
|
|
236
|
-
render: () => <Story text={str(text.links,
|
|
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 />,
|
|
237
249
|
};
|
|
238
250
|
|
|
239
251
|
export const Table = {
|
|
240
|
-
render: () => <Story text={str(text.table,
|
|
252
|
+
render: () => <Story text={str(text.table, text.footer)} extensions={[table()]} />,
|
|
241
253
|
};
|
|
242
254
|
|
|
243
255
|
export const Image = {
|
|
244
|
-
render: () => <Story text={str(text.image,
|
|
256
|
+
render: () => <Story text={str(text.image, text.footer)} readonly extensions={[image()]} />,
|
|
245
257
|
};
|
|
246
258
|
|
|
247
259
|
export const TaskList = {
|
|
248
260
|
render: () => (
|
|
249
261
|
<Story
|
|
250
|
-
text={str(text.tasks, '', text.list)}
|
|
262
|
+
text={str(text.tasks, '', text.list, text.footer)}
|
|
251
263
|
extensions={[
|
|
252
264
|
tasklist(),
|
|
253
265
|
listener({
|
|
@@ -263,7 +275,7 @@ export const TaskList = {
|
|
|
263
275
|
export const Autocomplete = {
|
|
264
276
|
render: () => (
|
|
265
277
|
<Story
|
|
266
|
-
text={str('# Autocomplete', '', '
|
|
278
|
+
text={str('# Autocomplete', '', 'Press CTRL-SPACE', text.footer)}
|
|
267
279
|
extensions={[
|
|
268
280
|
link({ onRender }),
|
|
269
281
|
autocomplete({
|
|
@@ -279,7 +291,7 @@ const names = ['adam', 'alice', 'alison', 'bob', 'carol', 'charlie', 'sayuri', '
|
|
|
279
291
|
export const Mention = {
|
|
280
292
|
render: () => (
|
|
281
293
|
<Story
|
|
282
|
-
text={str('# Mention', '', '
|
|
294
|
+
text={str('# Mention', '', 'Type @...', text.footer)}
|
|
283
295
|
extensions={[
|
|
284
296
|
mention({
|
|
285
297
|
onSearch: (text) => names.filter((name) => name.toLowerCase().startsWith(text.toLowerCase())),
|
|
@@ -293,14 +305,15 @@ const mark = () => `[^${PublicKey.random().toHex()}]`;
|
|
|
293
305
|
export const Comments = {
|
|
294
306
|
render: () => (
|
|
295
307
|
<Story
|
|
296
|
-
text={str(
|
|
308
|
+
text={str(text.paragraphs, mark(), '', mark(), text.footer)}
|
|
297
309
|
extensions={[
|
|
298
310
|
comments({
|
|
299
311
|
onCreate: () => PublicKey.random().toHex(),
|
|
300
312
|
onUpdate: (info) => {
|
|
301
|
-
console.log('update', info);
|
|
313
|
+
// console.log('update', info);
|
|
302
314
|
},
|
|
303
315
|
}),
|
|
316
|
+
highlight(),
|
|
304
317
|
]}
|
|
305
318
|
/>
|
|
306
319
|
),
|
|
@@ -321,7 +334,7 @@ export const Diagnostics = {
|
|
|
321
334
|
};
|
|
322
335
|
|
|
323
336
|
export const Demo = {
|
|
324
|
-
render: () => <Story text={text.paragraphs} extensions={[demo()]} />,
|
|
337
|
+
render: () => <Story text={str(text.paragraphs, text.footer)} extensions={[demo()]} />,
|
|
325
338
|
};
|
|
326
339
|
|
|
327
340
|
export const Blast = {
|
|
@@ -348,3 +361,7 @@ export const Blast = {
|
|
|
348
361
|
/>
|
|
349
362
|
),
|
|
350
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
|
+
};
|
|
@@ -57,7 +57,7 @@ type CommentsInfo = {
|
|
|
57
57
|
|
|
58
58
|
export type CommentsOptions = {
|
|
59
59
|
key?: string;
|
|
60
|
-
onCreate?: () => string | void;
|
|
60
|
+
onCreate?: (from: number, to: number) => string | void;
|
|
61
61
|
onUpdate?: (info: CommentsInfo) => void;
|
|
62
62
|
};
|
|
63
63
|
|
|
@@ -108,13 +108,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
108
108
|
key: options?.key ?? 'shift-meta-c',
|
|
109
109
|
run: (view) => {
|
|
110
110
|
// Insert footnote.
|
|
111
|
-
const
|
|
111
|
+
const { from, to, head } = view.state.selection.main;
|
|
112
|
+
const id = options.onCreate?.(from, to);
|
|
112
113
|
if (id) {
|
|
113
|
-
const pos = view.state.selection.main.head;
|
|
114
114
|
const tag = `[^${id}]`;
|
|
115
|
+
// TODO(burdon): Add selection.
|
|
115
116
|
view.dispatch({
|
|
116
|
-
changes: { from:
|
|
117
|
-
selection: { anchor:
|
|
117
|
+
changes: { from: head, insert: tag },
|
|
118
|
+
selection: { anchor: head + tag.length },
|
|
118
119
|
});
|
|
119
120
|
|
|
120
121
|
return true;
|
|
@@ -134,14 +135,14 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
134
135
|
return;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
const view = update
|
|
138
|
-
const pos =
|
|
138
|
+
const { view, state } = update;
|
|
139
|
+
const pos = state.selection.main.head;
|
|
139
140
|
|
|
140
141
|
const decorations: { from: number; to: number; value: Decoration }[] = [];
|
|
141
142
|
const rangeSet = view.plugin(bookmarks)?.bookmarks;
|
|
142
143
|
let closest = Infinity;
|
|
143
144
|
// TODO(burdon): Handle multiple visible ranges in large documents.
|
|
144
|
-
const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to:
|
|
145
|
+
const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: state.doc.length };
|
|
145
146
|
rangeSet?.between(from, to, (from, to, value) => {
|
|
146
147
|
decorations.push({ from, to, value });
|
|
147
148
|
const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
|
|
@@ -92,7 +92,7 @@ export function hyperLinkExtension() {
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
|
-
decorations: (
|
|
95
|
+
decorations: (value) => value.decorations,
|
|
96
96
|
eventHandlers: {
|
|
97
97
|
// TODO(burdon): CLick to expand link.
|
|
98
98
|
// https://discuss.codemirror.net/t/decorator-iteration-on-click-event/4226/2
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension, StateEffect, StateField } from '@codemirror/state';
|
|
6
|
+
import { Decoration, EditorView } from '@codemirror/view';
|
|
7
|
+
import sortBy from 'lodash.sortby';
|
|
8
|
+
import { useEffect } from 'react';
|
|
9
|
+
|
|
10
|
+
import { type DocumentRange } from '../../../hooks';
|
|
11
|
+
|
|
12
|
+
// TODO(burdon): Detect click, or cursor proximity.
|
|
13
|
+
|
|
14
|
+
// TODO(burdon): Reconcile with theme.
|
|
15
|
+
const styles = EditorView.baseTheme({
|
|
16
|
+
'& .cm-highlight': {
|
|
17
|
+
backgroundColor: 'yellow', // TODO(burdon): Reconcile with tailwind theme.
|
|
18
|
+
},
|
|
19
|
+
'& .cm-highlight::before': {
|
|
20
|
+
content: ']',
|
|
21
|
+
},
|
|
22
|
+
'& .cm-highlight-active': {
|
|
23
|
+
backgroundColor: 'lime',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const marks = {
|
|
28
|
+
highlight: Decoration.mark({ class: 'cm-highlight' }),
|
|
29
|
+
HighlightActive: Decoration.mark({ class: 'cm-highlight-active' }),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type HighlightRange = DocumentRange & { active?: boolean };
|
|
33
|
+
|
|
34
|
+
// TODO(burdon): Make configurable to support multiple layers.
|
|
35
|
+
const setHighlights = StateEffect.define<HighlightRange[]>();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Update field set.
|
|
39
|
+
*/
|
|
40
|
+
// TODO(burdon): Reconcile range from data model, vs. in-markdown characters.
|
|
41
|
+
export const useHighlights = (view: EditorView | undefined, ranges: HighlightRange[] = []) => {
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
view?.dispatch({
|
|
44
|
+
effects: setHighlights.of(ranges),
|
|
45
|
+
});
|
|
46
|
+
}, [view, ranges]);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* State field that tracks highlight ranges.
|
|
51
|
+
*/
|
|
52
|
+
export const highlightStateField = StateField.define<HighlightRange[]>({
|
|
53
|
+
create: () => [],
|
|
54
|
+
update: (threads, tr) => {
|
|
55
|
+
for (const effect of tr.effects) {
|
|
56
|
+
if (effect.is(setHighlights)) {
|
|
57
|
+
return effect.value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return threads;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Decorate ranges.
|
|
67
|
+
*/
|
|
68
|
+
// TODO(burdon): Pass in field to make reusable (e.g., comments, search).
|
|
69
|
+
export const highlightDecorations = EditorView.decorations.compute([highlightStateField], (state) => {
|
|
70
|
+
const selectionRanges = state.field(highlightStateField);
|
|
71
|
+
const decorations =
|
|
72
|
+
sortBy(selectionRanges ?? [], (selection) => selection.from)?.flatMap((selection) => {
|
|
73
|
+
const range = { from: selection.from, to: selection.to + 1 };
|
|
74
|
+
if (selection.to > selection.from) {
|
|
75
|
+
if (selection.active) {
|
|
76
|
+
return marks.HighlightActive.range(range.from, range.to);
|
|
77
|
+
} else {
|
|
78
|
+
return marks.highlight.range(range.from, range.to);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}) ?? [];
|
|
84
|
+
|
|
85
|
+
return Decoration.set(decorations);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// TODO(burdon): Use current range to create comment.
|
|
89
|
+
// TODO(burdon): Allow/prevent overlapping?
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Track cursor entering and existing ranges.
|
|
93
|
+
*/
|
|
94
|
+
const listener = (options: HighlightOptions) => {
|
|
95
|
+
return EditorView.updateListener.of((update) => {
|
|
96
|
+
const { view, state } = update;
|
|
97
|
+
const { /* from, to, */ head } = state.selection.main;
|
|
98
|
+
const ranges = state.field(highlightStateField);
|
|
99
|
+
|
|
100
|
+
// TODO(burdon): Modify range if editing.
|
|
101
|
+
// console.log('::::', update);
|
|
102
|
+
|
|
103
|
+
// Set active.
|
|
104
|
+
let mod = false;
|
|
105
|
+
const newRanges = ranges.map((range) => {
|
|
106
|
+
const active = head >= range.from && head <= range.to;
|
|
107
|
+
if (active !== range.active) {
|
|
108
|
+
mod = true;
|
|
109
|
+
return { ...range, active };
|
|
110
|
+
} else {
|
|
111
|
+
return range;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (mod) {
|
|
116
|
+
view.dispatch({ effects: setHighlights.of(newRanges) });
|
|
117
|
+
options?.onChange?.(newRanges.find((range) => range.active)?.id);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export type HighlightOptions = {
|
|
123
|
+
onChange?: (active?: string) => {};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const highlight = (options: HighlightOptions = {}): Extension => {
|
|
127
|
+
return [highlightStateField, highlightDecorations, listener(options), styles];
|
|
128
|
+
};
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
export * from './autocomplete';
|
|
6
6
|
export * from './basic';
|
|
7
7
|
export * from './blast';
|
|
8
|
+
export * from './code';
|
|
8
9
|
export * from './demo';
|
|
9
10
|
export * from './comments';
|
|
11
|
+
export * from './highlight';
|
|
10
12
|
export * from './image';
|
|
11
13
|
export * from './link';
|
|
12
14
|
export * from './listener';
|