@dxos/react-ui-editor 0.3.10-main.cb1d9e2 → 0.3.10-main.cbe7692

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.
Files changed (80) hide show
  1. package/dist/lib/browser/index.mjs +704 -135
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts +19 -1
  5. package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
  6. package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -1
  7. package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
  8. package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -1
  9. package/dist/types/src/components/TextEditor/extensions/basic.d.ts +2 -1
  10. package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
  11. package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
  12. package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
  13. package/dist/types/src/components/TextEditor/extensions/demo.d.ts +2 -1
  14. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -1
  15. package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
  16. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
  17. package/dist/types/src/components/TextEditor/extensions/index.d.ts +4 -0
  18. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  19. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
  20. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
  21. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +2 -1
  22. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  23. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +6 -2
  24. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  25. package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
  26. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
  27. package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
  28. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
  29. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -1
  30. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
  31. package/dist/types/src/components/TextEditor/themes/default.d.ts +5 -0
  32. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  33. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts +3 -0
  34. package/dist/types/src/hooks/automerge/PatchSemaphore.d.ts.map +1 -1
  35. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +8 -1
  36. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
  37. package/dist/types/src/hooks/automerge/index.d.ts +1 -1
  38. package/dist/types/src/hooks/automerge/index.d.ts.map +1 -1
  39. package/dist/types/src/hooks/automerge/plugin.d.ts.map +1 -1
  40. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts +5 -0
  41. package/dist/types/src/hooks/automerge/translation/automerge-to-codemirror.d.ts.map +1 -0
  42. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts +6 -0
  43. package/dist/types/src/hooks/automerge/translation/codemirror-to-automerge.d.ts.map +1 -0
  44. package/dist/types/src/hooks/automerge/translation/index.d.ts +3 -0
  45. package/dist/types/src/hooks/automerge/translation/index.d.ts.map +1 -0
  46. package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +1 -1
  47. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
  48. package/dist/types/src/styles/markdown.d.ts +3 -2
  49. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  50. package/package.json +20 -20
  51. package/src/components/TextEditor/MarkdownEditor.stories.tsx +98 -25
  52. package/src/components/TextEditor/TextEditor.tsx +16 -7
  53. package/src/components/TextEditor/extensions/autocomplete.ts +5 -20
  54. package/src/components/TextEditor/extensions/basic.ts +2 -1
  55. package/src/components/TextEditor/extensions/blast.ts +375 -0
  56. package/src/components/TextEditor/extensions/demo.ts +4 -1
  57. package/src/components/TextEditor/extensions/image.ts +68 -0
  58. package/src/components/TextEditor/extensions/index.ts +4 -0
  59. package/src/components/TextEditor/extensions/link.ts +15 -23
  60. package/src/components/TextEditor/extensions/listener.ts +3 -3
  61. package/src/components/TextEditor/extensions/markdown/bundle.ts +19 -11
  62. package/src/components/TextEditor/extensions/markdown/highlight.ts +118 -104
  63. package/src/components/TextEditor/extensions/mention.ts +31 -0
  64. package/src/components/TextEditor/extensions/table.ts +123 -0
  65. package/src/components/TextEditor/extensions/tasklist.ts +34 -20
  66. package/src/components/TextEditor/themes/default.ts +54 -2
  67. package/src/hooks/automerge/PatchSemaphore.ts +12 -9
  68. package/src/hooks/automerge/automerge.stories.tsx +3 -2
  69. package/src/hooks/automerge/index.ts +1 -1
  70. package/src/hooks/automerge/plugin.ts +1 -0
  71. package/src/hooks/automerge/{amToCodemirror.ts → translation/automerge-to-codemirror.ts} +10 -2
  72. package/src/hooks/automerge/{codeMirrorToAm.ts → translation/codemirror-to-automerge.ts} +7 -4
  73. package/src/hooks/automerge/translation/index.ts +6 -0
  74. package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +1 -1
  75. package/src/styles/markdown.ts +14 -8
  76. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
  77. package/dist/types/src/hooks/automerge/amToCodemirror.d.ts +0 -6
  78. package/dist/types/src/hooks/automerge/amToCodemirror.d.ts.map +0 -1
  79. package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts +0 -7
  80. package/dist/types/src/hooks/automerge/codeMirrorToAm.d.ts.map +0 -1
@@ -0,0 +1,375 @@
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, StateField } 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
+ maxParticles: 200,
29
+ particleGravity: 0.08,
30
+ particleAlphaFadeout: 0.996,
31
+ particleSize: { min: 2, max: 8 },
32
+ particleNumRange: { min: 5, max: 10 },
33
+ particleVelocityRange: { x: [-1, 1], y: [-3.5, -1.5] },
34
+ particleShrinkRate: 0.95,
35
+ shakeIntensity: 5,
36
+ };
37
+
38
+ export const blast = (options: Partial<BlastOptions>): Extension => {
39
+ let blaster: Blaster | undefined;
40
+ let last = 0;
41
+
42
+ const getPoint = (view: EditorView, pos: number) => {
43
+ const { left: x = 0, top: y = 0 } = view.coordsForChar(pos) ?? {};
44
+ const element = document.elementFromPoint(x, y);
45
+
46
+ const { offsetLeft: left = 0, offsetTop: top = 0 } = view.scrollDOM.parentElement ?? {};
47
+ const point = { x: x - left, y: y - top };
48
+
49
+ return { element, point };
50
+ };
51
+
52
+ return [
53
+ // Cursor moved.
54
+ EditorView.updateListener.of((update) => {
55
+ // NOTE: The MarkdownEditor may recreated the EditorView.
56
+ if (blaster?.node !== update.view.scrollDOM) {
57
+ if (blaster) {
58
+ blaster.destroy();
59
+ }
60
+
61
+ blaster = new Blaster(update.view.scrollDOM, defaultsDeep({}, options, defaultOptions));
62
+ blaster.initialize();
63
+ blaster.start(); // TODO(burdon): Stop/clean-up.
64
+ } else {
65
+ blaster.resize();
66
+ }
67
+
68
+ const current = update.view.state.selection.main.head;
69
+ if (current !== last) {
70
+ last = current;
71
+ // TODO(burdon): Null if end of line.
72
+ const { element, point } = getPoint(update.view, current);
73
+ if (element && point) {
74
+ blaster.spawn({ element, point });
75
+ }
76
+ }
77
+ }),
78
+
79
+ // Document changed.
80
+ StateField.define({
81
+ create: () => null,
82
+ update: (_value, transaction) => {
83
+ if (blaster && transaction.docChanged) {
84
+ // TODO(burdon): Only on delete.
85
+ const view = EditorView.findFromDOM(blaster.node)!;
86
+ const { element, point } = getPoint(view, transaction.selection!.main.head);
87
+ if (element && point) {
88
+ blaster.spawn({ element, point });
89
+ }
90
+ }
91
+
92
+ return null;
93
+ },
94
+ }),
95
+
96
+ keymap.of([
97
+ {
98
+ any: (view, event) => {
99
+ if (blaster && event.key === 'Enter' && event.shiftKey) {
100
+ blaster.shake({ time: 0.4 });
101
+ }
102
+
103
+ return false;
104
+ },
105
+ },
106
+ ]),
107
+ ];
108
+ };
109
+
110
+ //
111
+ // Blaster (no CM deps).
112
+ //
113
+
114
+ class Blaster {
115
+ _canvas: HTMLCanvasElement | undefined;
116
+ _ctx: CanvasRenderingContext2D | undefined | null;
117
+
118
+ private readonly _effect: Effect;
119
+
120
+ _running = false;
121
+ _lastTime: number | undefined;
122
+ _shakeTime = 0;
123
+ _shakeTimeMax = 0;
124
+
125
+ _particles: Particle[] = [];
126
+ _particlePointer = 0;
127
+
128
+ _lastPoint = { x: 0, y: 0 };
129
+
130
+ constructor(private readonly _node: HTMLElement, private readonly _options: BlastOptions) {
131
+ this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
132
+ }
133
+
134
+ get node() {
135
+ return this._node;
136
+ }
137
+
138
+ initialize() {
139
+ // console.log('initialize');
140
+ invariant(!this._canvas && !this._ctx);
141
+
142
+ this._canvas = document.createElement('canvas');
143
+ this._canvas.id = 'code-blast-canvas';
144
+ this._canvas.style.position = 'absolute';
145
+ this._canvas.style.zIndex = '0';
146
+ this._canvas.style.pointerEvents = 'none';
147
+ // this._canvas.style.border = '2px solid red';
148
+
149
+ this._ctx = this._canvas.getContext('2d');
150
+
151
+ const parent = this._node.parentElement?.parentElement;
152
+ parent?.appendChild(this._canvas);
153
+
154
+ this.resize();
155
+ }
156
+
157
+ destroy() {
158
+ this.stop();
159
+ // console.log('destroy');
160
+ if (this._canvas) {
161
+ this._canvas.remove();
162
+ this._canvas = undefined;
163
+ this._ctx = undefined;
164
+ }
165
+ }
166
+
167
+ resize() {
168
+ if (this._node.parentElement && this._canvas) {
169
+ const { offsetLeft: x, offsetTop: y, offsetWidth: width, offsetHeight: height } = this._node.parentElement;
170
+ this._canvas.style.top = `${y}px`;
171
+ this._canvas.style.left = `${x}px`;
172
+ this._canvas.width = width;
173
+ this._canvas.height = height;
174
+ }
175
+ }
176
+
177
+ start() {
178
+ // console.log('start');
179
+ invariant(this._canvas && this._ctx);
180
+ this._running = true;
181
+ this.loop();
182
+ }
183
+
184
+ stop() {
185
+ // console.log('stop');
186
+ this._running = false;
187
+ this._node.style.transform = 'translate(0px, 0px)';
188
+ }
189
+
190
+ loop() {
191
+ if (!this._running || !this._canvas || !this._ctx) {
192
+ return;
193
+ }
194
+
195
+ this._ctx.clearRect(0, 0, this._canvas.width ?? 0, this._canvas.height ?? 0);
196
+
197
+ // Calculate the delta from the previous frame.
198
+ const now = new Date().getTime();
199
+ this._lastTime ??= now;
200
+ const dt = (now - this._lastTime) / 1000;
201
+ this._lastTime = now;
202
+
203
+ if (this._shakeTime > 0) {
204
+ this._shakeTime -= dt;
205
+ const magnitude = (this._shakeTime / this._shakeTimeMax) * this._options.shakeIntensity;
206
+ const shakeX = random(-magnitude, magnitude);
207
+ const shakeY = random(-magnitude, magnitude);
208
+ this._node!.style.transform = `translate(${shakeX}px,${shakeY}px)`;
209
+ }
210
+
211
+ this.drawParticles();
212
+
213
+ requestAnimationFrame(this.loop.bind(this));
214
+ }
215
+
216
+ shake = throttle<{ time: number }>(({ time }) => {
217
+ this._shakeTime = this._shakeTimeMax;
218
+ this._shakeTimeMax = time;
219
+ }, 100);
220
+
221
+ spawn = throttle<{ element: Element; point: { x: number; y: number } }>(({ element, point }) => {
222
+ const color = getRGBComponents(element, this._options.color);
223
+ const numParticles = random(this._options.particleNumRange.min, this._options.particleNumRange.max);
224
+ const dir = this._lastPoint.x === point.x ? 0 : this._lastPoint.x < point.x ? 1 : -1;
225
+ this._lastPoint = point;
226
+ for (let i = numParticles; i--; i > 0) {
227
+ this._particles[this._particlePointer] = this._effect.create(point.x - dir * 16, point.y, color);
228
+ this._particlePointer = (this._particlePointer + 1) % this._options.maxParticles;
229
+ }
230
+ }, 100);
231
+
232
+ drawParticles() {
233
+ for (let i = this._particles.length; i--; i > 0) {
234
+ const particle = this._particles[i];
235
+ if (!particle) {
236
+ continue;
237
+ }
238
+
239
+ if (particle.alpha < 0.01 || particle.size <= 0.5) {
240
+ continue;
241
+ }
242
+
243
+ this._effect.update(this._ctx!, particle);
244
+ }
245
+ }
246
+ }
247
+
248
+ //
249
+ // Effects
250
+ //
251
+
252
+ type Particle = {
253
+ x: number;
254
+ y: number;
255
+ vx: number;
256
+ vy: number;
257
+ size: number;
258
+ color: (string | number)[];
259
+ alpha: number;
260
+ theta?: number;
261
+ drag?: number;
262
+ wander?: number;
263
+ };
264
+
265
+ abstract class Effect {
266
+ constructor(protected readonly _options: BlastOptions) {}
267
+ abstract create(x: number, y: number, color: Particle['color']): Particle;
268
+ abstract update(ctx: CanvasRenderingContext2D, particle: Particle): void;
269
+ }
270
+
271
+ class Effect1 extends Effect {
272
+ create(x: number, y: number, color: Particle['color']) {
273
+ return {
274
+ x,
275
+ y: y + 10,
276
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
277
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
278
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
279
+ color,
280
+ alpha: 1,
281
+ };
282
+ }
283
+
284
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
285
+ particle.vy += this._options.particleGravity;
286
+ particle.x += particle.vx;
287
+ particle.y += particle.vy;
288
+ particle.alpha *= this._options.particleAlphaFadeout;
289
+
290
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
291
+ ctx.fillRect(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, particle.size);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Based on Soulwire's demo.
297
+ * http://codepen.io/soulwire/pen/foktm
298
+ */
299
+ class Effect2 extends Effect {
300
+ create(x: number, y: number, color: Particle['color']) {
301
+ return {
302
+ x,
303
+ y: y + 10,
304
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
305
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
306
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
307
+ color,
308
+ alpha: 1,
309
+ theta: (random(0, 360) * Math.PI) / 180,
310
+ drag: 0.92,
311
+ wander: 0.15,
312
+ };
313
+ }
314
+
315
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
316
+ particle.vy += this._options.particleGravity;
317
+ particle.x += particle.vx;
318
+ particle.y += particle.vy;
319
+ particle.vx *= particle.drag!;
320
+ particle.vy *= particle.drag!;
321
+ particle.theta! += random(-0.5, 0.5);
322
+ particle.vx += Math.sin(particle.theta!) * 0.1;
323
+ particle.vy += Math.cos(particle.theta!) * 0.1;
324
+ particle.size *= this._options.particleShrinkRate;
325
+ particle.alpha *= this._options.particleAlphaFadeout;
326
+
327
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
328
+ ctx.beginPath();
329
+ ctx.arc(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, 0, 2 * Math.PI);
330
+ ctx.fill();
331
+ }
332
+ }
333
+
334
+ //
335
+ // Utils
336
+ //
337
+
338
+ function throttle<T>(callback: (arg: T) => void, limit: number): (arg: T) => void {
339
+ let wait = false;
340
+ return function (...args: any[]) {
341
+ if (!wait) {
342
+ // @ts-ignore
343
+ callback.apply(this, args);
344
+ wait = true;
345
+ setTimeout(() => {
346
+ wait = false;
347
+ }, limit);
348
+ }
349
+ };
350
+ }
351
+
352
+ const getRGBComponents = (node: Element, color: BlastOptions['color']): Particle['color'] => {
353
+ if (typeof color === 'function') {
354
+ return color();
355
+ }
356
+
357
+ const bgColor = getComputedStyle(node).color;
358
+ if (bgColor) {
359
+ const x = bgColor.match(/(\d+), (\d+), (\d+)/)?.slice(1);
360
+ if (x) {
361
+ return x;
362
+ }
363
+ }
364
+
365
+ return [50, 50, 50];
366
+ };
367
+
368
+ const random = (min: number, max: number) => {
369
+ if (!max) {
370
+ max = min;
371
+ min = 0;
372
+ }
373
+
374
+ return min + ~~(Math.random() * (max - min + 1));
375
+ };
@@ -2,8 +2,11 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { type Extension } from '@codemirror/state';
5
6
  import { keymap } from '@codemirror/view';
6
7
 
8
+ // TODO(burdon): Review https://github.com/sergeche/codemirror-movie?tab=readme-ov-file
9
+
7
10
  export type DemoOptions = {
8
11
  delay?: number;
9
12
  items?: string[];
@@ -15,7 +18,7 @@ const defaultItems = ['hello world!', 'this is a test.', 'this is [DXOS](https:/
15
18
  * Demo script.
16
19
  * Configurable plugin that let's user cycle through pre-configured input script.
17
20
  */
18
- export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}) => {
21
+ export const demo = ({ delay = 75, items = defaultItems }: DemoOptions = {}): Extension => {
19
22
  let t: any;
20
23
  let idx = 0;
21
24
 
@@ -0,0 +1,68 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import {
7
+ type EditorState,
8
+ type Extension,
9
+ type RangeSet,
10
+ RangeSetBuilder,
11
+ StateField,
12
+ type Transaction,
13
+ } from '@codemirror/state';
14
+ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
15
+
16
+ export type ImageOptions = {};
17
+
18
+ export const image = (options: ImageOptions = {}): Extension => {
19
+ return StateField.define<RangeSet<any>>({
20
+ create: (state) => update(state, options),
21
+ update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
22
+ provide: (field) => EditorView.decorations.from(field),
23
+ });
24
+ };
25
+
26
+ const update = (state: EditorState, options: ImageOptions) => {
27
+ const builder = new RangeSetBuilder();
28
+
29
+ // TODO(burdon): If not-readonly then show text.
30
+ // if (state.readOnly) {
31
+ syntaxTree(state).iterate({
32
+ enter: (node) => {
33
+ if (node.name === 'Image') {
34
+ const urlNode = node.node.getChild('URL');
35
+ if (urlNode) {
36
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
37
+ builder.add(
38
+ state.readOnly ? node.from : node.to,
39
+ node.to,
40
+ Decoration.widget({
41
+ widget: new ImageWidget(url),
42
+ }),
43
+ );
44
+ }
45
+ }
46
+ },
47
+ });
48
+ // }
49
+
50
+ return builder.finish();
51
+ };
52
+
53
+ class ImageWidget extends WidgetType {
54
+ constructor(readonly _url: string) {
55
+ super();
56
+ }
57
+
58
+ override eq(other: WidgetType) {
59
+ return this._url === (other as any as ImageWidget)?._url;
60
+ }
61
+
62
+ toDOM(view: EditorView) {
63
+ const img = document.createElement('img');
64
+ img.setAttribute('src', this._url);
65
+ img.setAttribute('class', 'cm-image');
66
+ return img;
67
+ }
68
+ }
@@ -4,10 +4,14 @@
4
4
 
5
5
  export * from './autocomplete';
6
6
  export * from './basic';
7
+ export * from './blast';
7
8
  export * from './demo';
8
9
  export * from './comments';
10
+ export * from './image';
9
11
  export * from './link';
10
12
  export * from './listener';
11
13
  export * from './markdown';
14
+ export * from './mention';
15
+ export * from './table';
12
16
  export * from './tasklist';
13
17
  export * from './tooltip';
@@ -18,13 +18,22 @@ import { Decoration, EditorView, WidgetType } from '@codemirror/view';
18
18
  // https://codemirror.net/try/?c=aW1wb3J0IHsgYmFzaWNTZXR1cCwgRWRpdG9yVmlldyB9IGZyb20gImNvZGVtaXJyb3IiCmltcG9ydCB7IG1hcmtkb3duIH0gZnJvbSAiQGNvZGVtaXJyb3IvbGFuZy1tYXJrZG93biIKaW1wb3J0IHsgc3ludGF4VHJlZSB9IGZyb20gIkBjb2RlbWlycm9yL2xhbmd1YWdlIjsKaW1wb3J0IHsgRWRpdG9yU3RhdGUsIFJhbmdlU2V0QnVpbGRlciwgU3RhdGVGaWVsZCB9IGZyb20gIkBjb2RlbWlycm9yL3N0YXRlIjsKaW1wb3J0IHsgRGVjb3JhdGlvbiwgV2lkZ2V0VHlwZSB9IGZyb20gIkBjb2RlbWlycm9yL3ZpZXciOwoKY2xhc3MgTGluayBleHRlbmRzIFdpZGdldFR5cGUgewogIGNvbnN0cnVjdG9yKHRleHQsIHVybCkgewogICAgc3VwZXIoKQogICAgdGhpcy50ZXh0ID0gdGV4dAogICAgdGhpcy51cmwgPSB1cmwKICB9CiAgZXEob3RoZXIpIHsgcmV0dXJuIHRoaXMudGV4dCA9PSBvdGhlci50ZXh0ICYmIHRoaXMudXJsID09IG90aGVyLnVybCB9CiAgdG9ET00oKSB7CiAgICBsZXQgbGluayA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoImEiKQogICAgbGluay50ZXh0Q29udGVudCA9IHRoaXMudGV4dAogICAgbGluay5ocmVmID0gdGhpcy51cmwKICAgIHJldHVybiBsaW5rOwogIH0KfQoKbGV0IGRlY29yYXRpb25zRmllbGQgPSBTdGF0ZUZpZWxkLmRlZmluZSh7CiAgY3JlYXRlKCkgewogICAgcmV0dXJuIERlY29yYXRpb24ubm9uZTsKICB9LAogIHVwZGF0ZShfLCB0cikgewogICAgY29uc3QgYnVpbGRlciA9IG5ldyBSYW5nZVNldEJ1aWxkZXIoKTsKICAgIGxldCBjdXJzb3IgPSB0ci5zdGF0ZS5zZWxlY3Rpb24ubWFpbi5oZWFkOwogICAgc3ludGF4VHJlZSh0ci5zdGF0ZSkuaXRlcmF0ZSh7CiAgICAgIGVudGVyOiAobm9kZSkgPT4gewogICAgICAgIGlmICgoY3Vyc29yIDwgbm9kZS5mcm9tIHx8IGN1cnNvciA+IG5vZGUudG8pICYmIG5vZGUubmFtZSA9PSAiTGluayIpIHsKICAgICAgICAgIGxldCBtYXJrcyA9IG5vZGUubm9kZS5nZXRDaGlsZHJlbigiTGlua01hcmsiKTsKICAgICAgICAgIGxldCB0ZXh0ID0gbWFya3MubGVuZ3RoID49IDIgPyB0ci5zdGF0ZS5zbGljZURvYyhtYXJrc1swXS50bywgbWFya3NbMV0uZnJvbSkgOiAiIgoKICAgICAgICAgIGxldCB1cmxOb2RlID0gbm9kZS5ub2RlLmdldENoaWxkKCJVUkwiKTsKICAgICAgICAgIGxldCB1cmwgPSB1cmxOb2RlID8gdHIuc3RhdGUuc2xpY2VEb2ModXJsTm9kZS5mcm9tLCB1cmxOb2RlLnRvKSA6ICIiCiAgICAgICAgICBidWlsZGVyLmFkZChub2RlLmZyb20sIG5vZGUudG8sIERlY29yYXRpb24ucmVwbGFjZSh7CiAgICAgICAgICAgIHdpZGdldDogbmV3IExpbmsodGV4dCwgdXJsKSwKICAgICAgICAgIH0pLAogICAgICAgICAgICAgICAgICAgICApOwogICAgICAgICAgcmV0dXJuIGZhbHNlOwogICAgICAgIH0KICAgICAgICByZXR1cm4gdHJ1ZTsKICAgICAgfSwKICAgIH0pOwogICAgcmV0dXJuIGJ1aWxkZXIuZmluaXNoKCk7CiAgfSwKICBwcm92aWRlOiAoZikgPT4gRWRpdG9yVmlldy5kZWNvcmF0aW9ucy5mcm9tKGYpLAp9KTsKCmxldCB2aWV3ID0gbmV3IEVkaXRvclZpZXcoewogIGRvYzogIiMgSGVsbG8gV29ybGRcblxuW1Rlc3RdKGh0dHBzOi8vZXhhbXBsZS5jb20pXG5cblxuIiwKICBleHRlbnNpb25zOiBbZGVjb3JhdGlvbnNGaWVsZCwgbWFya2Rvd24oKV0sCiAgcGFyZW50OiBkb2N1bWVudC5ib2R5LAp9KTsKCnZpZXcuZm9jdXMoKTs=
19
19
  // https://discuss.codemirror.net/t/link-widget-is-clicked-only-if-editor-is-out-of-focus/7433
20
20
 
21
- // TODO(burdon): Adapt to make all markdown editable?
21
+ export type LinkOptions = {
22
+ link?: boolean;
23
+ onRender?: (el: Element, url: string) => void;
24
+ };
22
25
 
23
- // TODO(burdon): Research overlays.
24
- // https://stackoverflow.com/questions/26933344/codemirror-detect-and-create-links-inside-editor
25
- // https://codemirror.net/5/doc/manual.html#addOverlay
26
- // https://codemirror.net/5/demo/simplemode.html
27
- // https://codemirror.net/5/demo/search.html (Search dialog).
26
+ /**
27
+ * Creates a state field to replace AST elements with a hyperlink widget.
28
+ * https://codemirror.net/docs/ref/#state.StateField
29
+ */
30
+ export const link = (options: LinkOptions = {}): Extension => {
31
+ return StateField.define<RangeSet<any>>({
32
+ create: (state) => update(state, options),
33
+ update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
34
+ provide: (field) => EditorView.decorations.from(field),
35
+ });
36
+ };
28
37
 
29
38
  class LinkButton extends WidgetType {
30
39
  constructor(
@@ -125,20 +134,3 @@ const update = (state: EditorState, options: LinkOptions) => {
125
134
 
126
135
  return builder.finish();
127
136
  };
128
-
129
- export type LinkOptions = {
130
- link?: boolean;
131
- onRender?: (el: Element, url: string) => void;
132
- };
133
-
134
- /**
135
- * Creates a state field to replace AST elements with a hyperlink widget.
136
- * https://codemirror.net/docs/ref/#state.StateField
137
- */
138
- export const link = (options: LinkOptions = {}): Extension => {
139
- return StateField.define<RangeSet<any>>({
140
- create: (state) => update(state, options),
141
- update: (_: RangeSet<any>, tr: Transaction) => update(tr.state, options),
142
- provide: (field) => EditorView.decorations.from(field),
143
- });
144
- };
@@ -9,9 +9,8 @@ export type ListenerOptions = { onChange: (text: string) => void };
9
9
  /**
10
10
  * Based on https://github.com/codemirror/dev/issues/44#issuecomment-789093799
11
11
  */
12
- // TODO(burdon): Expose options.
13
- export const listener = ({ onChange }: ListenerOptions) =>
14
- StateField.define({
12
+ export const listener = ({ onChange }: ListenerOptions) => {
13
+ return StateField.define({
15
14
  create: () => null,
16
15
  update: (_value, transaction) => {
17
16
  if (transaction.docChanged && onChange) {
@@ -21,3 +20,4 @@ export const listener = ({ onChange }: ListenerOptions) =>
21
20
  return null;
22
21
  },
23
22
  });
23
+ };
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { defaultKeymap, history, indentWithTab } from '@codemirror/commands';
6
+ import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
7
7
  import { markdownLanguage, markdown } from '@codemirror/lang-markdown';
8
8
  import { bracketMatching, defaultHighlightStyle, indentOnInput, syntaxHighlighting } from '@codemirror/language';
9
9
  import { languages } from '@codemirror/language-data';
@@ -28,11 +28,16 @@ import type { ThemeMode } from '@dxos/react-ui';
28
28
  import { markdownHighlightStyle, markdownTagsExtension } from './highlight';
29
29
 
30
30
  export type MarkdownBundleOptions = {
31
+ readonly?: boolean;
31
32
  themeMode?: ThemeMode;
32
33
  placeholder?: string;
33
34
  };
34
35
 
35
- export const markdownBundle = ({ themeMode, placeholder: _placeholder }: MarkdownBundleOptions): Extension[] => {
36
+ export const markdownBundle = ({
37
+ readonly,
38
+ themeMode,
39
+ placeholder: _placeholder,
40
+ }: MarkdownBundleOptions): Extension[] => {
36
41
  // All of https://github.com/codemirror/basic-setup minus line numbers and fold gutter.
37
42
  // https://codemirror.net/docs/ref/#codemirror.basicSetup
38
43
  return [
@@ -54,14 +59,17 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
54
59
  indentOnInput(),
55
60
  rectangularSelection(),
56
61
 
57
- // TODO(burdon): Review.
62
+ // https://codemirror.net/docs/ref/#view.keymap
58
63
  keymap.of([
59
- ...closeBracketsKeymap,
64
+ // https://codemirror.net/docs/ref/#commands.defaultKeymap
60
65
  ...defaultKeymap,
61
- // ...foldKeymap,
62
- // ...historyKeymap,
63
- // ...lintKeymap,
66
+ // https://codemirror.net/docs/ref/#commands.historyKeymap
67
+ ...historyKeymap,
68
+ // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
69
+ ...closeBracketsKeymap,
70
+ // https://codemirror.net/docs/ref/#search.searchKeymap
64
71
  ...searchKeymap,
72
+ // https://codemirror.net/docs/ref/#commands.indentWithTab
65
73
  indentWithTab,
66
74
  ]),
67
75
 
@@ -86,10 +94,10 @@ export const markdownBundle = ({ themeMode, placeholder: _placeholder }: Markdow
86
94
  ],
87
95
  }),
88
96
 
89
- // Custom styles.
90
- syntaxHighlighting(markdownHighlightStyle),
91
-
92
- // TODO(thure): All but one rule here apply to both themes; rename or refactor.
97
+ // https://github.com/codemirror/theme-one-dark
93
98
  themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
99
+
100
+ // Custom styles.
101
+ syntaxHighlighting(markdownHighlightStyle(readonly)),
94
102
  ].filter(Boolean) as Extension[];
95
103
  };