@dxos/react-ui-editor 0.3.10-main.90b298e → 0.3.10-main.92d1f2c

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 (65) hide show
  1. package/dist/lib/browser/index.mjs +897 -293
  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/comments.d.ts +1 -1
  14. package/dist/types/src/components/TextEditor/extensions/comments.d.ts.map +1 -1
  15. package/dist/types/src/components/TextEditor/extensions/demo.d.ts +2 -1
  16. package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -1
  17. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts +1 -1
  18. package/dist/types/src/components/TextEditor/extensions/experimental.d.ts.map +1 -1
  19. package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
  20. package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
  21. package/dist/types/src/components/TextEditor/extensions/index.d.ts +4 -0
  22. package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
  23. package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
  24. package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
  25. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +2 -1
  26. package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
  27. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +9 -3
  28. package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
  29. package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
  30. package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
  31. package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
  32. package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
  33. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -1
  34. package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
  35. package/dist/types/src/components/TextEditor/themes/default.d.ts +5 -0
  36. package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
  37. package/dist/types/src/hooks/automerge/automerge.stories.d.ts +8 -1
  38. package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
  39. package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +1 -1
  40. package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
  41. package/dist/types/src/styles/markdown.d.ts +3 -2
  42. package/dist/types/src/styles/markdown.d.ts.map +1 -1
  43. package/package.json +20 -20
  44. package/src/components/TextEditor/MarkdownEditor.stories.tsx +102 -27
  45. package/src/components/TextEditor/TextEditor.tsx +13 -23
  46. package/src/components/TextEditor/extensions/autocomplete.ts +5 -20
  47. package/src/components/TextEditor/extensions/basic.ts +2 -1
  48. package/src/components/TextEditor/extensions/blast.ts +387 -0
  49. package/src/components/TextEditor/extensions/comments.ts +21 -39
  50. package/src/components/TextEditor/extensions/demo.ts +4 -1
  51. package/src/components/TextEditor/extensions/experimental.tsx +14 -35
  52. package/src/components/TextEditor/extensions/image.ts +67 -0
  53. package/src/components/TextEditor/extensions/index.ts +4 -0
  54. package/src/components/TextEditor/extensions/link.ts +17 -25
  55. package/src/components/TextEditor/extensions/listener.ts +3 -3
  56. package/src/components/TextEditor/extensions/markdown/bundle.ts +30 -19
  57. package/src/components/TextEditor/extensions/markdown/highlight.ts +147 -108
  58. package/src/components/TextEditor/extensions/mention.ts +31 -0
  59. package/src/components/TextEditor/extensions/table.ts +122 -0
  60. package/src/components/TextEditor/extensions/tasklist.ts +41 -27
  61. package/src/components/TextEditor/themes/default.ts +54 -2
  62. package/src/hooks/automerge/automerge.stories.tsx +3 -2
  63. package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +1 -1
  64. package/src/styles/markdown.ts +14 -8
  65. package/dist/types/src/components/TextEditor/yjs.stories.d.ts.map +0 -1
@@ -0,0 +1,387 @@
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
+ 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>): 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.view.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);
85
+ if (element && point) {
86
+ blaster.spawn({ element, point });
87
+ }
88
+ }
89
+ }),
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
+ keymap.of([
109
+ {
110
+ any: (view, event) => {
111
+ if (blaster && event.key === 'Enter' && event.shiftKey) {
112
+ blaster.shake({ time: 0.4 });
113
+ }
114
+
115
+ return false;
116
+ },
117
+ },
118
+ ]),
119
+ ];
120
+ };
121
+
122
+ //
123
+ // Blaster (no CM deps).
124
+ //
125
+
126
+ class Blaster {
127
+ _canvas: HTMLCanvasElement | undefined;
128
+ _ctx: CanvasRenderingContext2D | undefined | null;
129
+
130
+ private readonly _effect: Effect;
131
+
132
+ _running = false;
133
+ _lastTime: number | undefined;
134
+ _shakeTime = 0;
135
+ _shakeTimeMax = 0;
136
+
137
+ _particles: Particle[] = [];
138
+ _particlePointer = 0;
139
+
140
+ _lastPoint = { x: 0, y: 0 };
141
+
142
+ constructor(private readonly _node: HTMLElement, private readonly _options: BlastOptions) {
143
+ this._effect = this._options.effect === 1 ? new Effect1(_options) : new Effect2(_options);
144
+ }
145
+
146
+ get node() {
147
+ return this._node;
148
+ }
149
+
150
+ initialize() {
151
+ // console.log('initialize');
152
+ invariant(!this._canvas && !this._ctx);
153
+
154
+ this._canvas = document.createElement('canvas');
155
+ this._canvas.id = 'code-blast-canvas';
156
+ this._canvas.style.position = 'absolute';
157
+ this._canvas.style.zIndex = '0';
158
+ this._canvas.style.pointerEvents = 'none';
159
+ // this._canvas.style.border = '2px solid red';
160
+
161
+ this._ctx = this._canvas.getContext('2d');
162
+
163
+ const parent = this._node.parentElement?.parentElement;
164
+ parent?.appendChild(this._canvas);
165
+
166
+ this.resize();
167
+ }
168
+
169
+ destroy() {
170
+ this.stop();
171
+ // console.log('destroy');
172
+ if (this._canvas) {
173
+ this._canvas.remove();
174
+ this._canvas = undefined;
175
+ this._ctx = undefined;
176
+ }
177
+ }
178
+
179
+ resize() {
180
+ if (this._node.parentElement && this._canvas) {
181
+ const { offsetLeft: x, offsetTop: y, offsetWidth: width, offsetHeight: height } = this._node.parentElement;
182
+ this._canvas.style.top = `${y}px`;
183
+ this._canvas.style.left = `${x}px`;
184
+ this._canvas.width = width;
185
+ this._canvas.height = height;
186
+ }
187
+ }
188
+
189
+ start() {
190
+ // console.log('start');
191
+ invariant(this._canvas && this._ctx);
192
+ this._running = true;
193
+ this.loop();
194
+ }
195
+
196
+ stop() {
197
+ // console.log('stop');
198
+ this._running = false;
199
+ this._node.style.transform = 'translate(0px, 0px)';
200
+ }
201
+
202
+ loop() {
203
+ if (!this._running || !this._canvas || !this._ctx) {
204
+ return;
205
+ }
206
+
207
+ this._ctx.clearRect(0, 0, this._canvas.width ?? 0, this._canvas.height ?? 0);
208
+
209
+ // Calculate the delta from the previous frame.
210
+ const now = new Date().getTime();
211
+ this._lastTime ??= now;
212
+ const dt = (now - this._lastTime) / 1000;
213
+ this._lastTime = now;
214
+
215
+ if (this._shakeTime > 0) {
216
+ this._shakeTime -= dt;
217
+ const magnitude = (this._shakeTime / this._shakeTimeMax) * this._options.shakeIntensity;
218
+ const shakeX = random(-magnitude, magnitude);
219
+ const shakeY = random(-magnitude, magnitude);
220
+ this._node!.style.transform = `translate(${shakeX}px,${shakeY}px)`;
221
+ }
222
+
223
+ this.drawParticles();
224
+
225
+ requestAnimationFrame(this.loop.bind(this));
226
+ }
227
+
228
+ shake = throttle<{ time: number }>(({ time }) => {
229
+ this._shakeTime = this._shakeTimeMax;
230
+ this._shakeTimeMax = time;
231
+ }, 100);
232
+
233
+ spawn = throttle<{ element: Element; point: { x: number; y: number } }>(({ element, point }) => {
234
+ const color = getRGBComponents(element, this._options.color);
235
+ const numParticles = random(this._options.particleNumRange.min, this._options.particleNumRange.max);
236
+ const dir = this._lastPoint.x === point.x ? 0 : this._lastPoint.x < point.x ? 1 : -1;
237
+ this._lastPoint = point;
238
+ for (let i = numParticles; i--; i > 0) {
239
+ this._particles[this._particlePointer] = this._effect.create(point.x - dir * 16, point.y, color);
240
+ this._particlePointer = (this._particlePointer + 1) % this._options.maxParticles;
241
+ }
242
+ }, 100);
243
+
244
+ drawParticles() {
245
+ for (let i = this._particles.length; i--; i > 0) {
246
+ const particle = this._particles[i];
247
+ if (!particle) {
248
+ continue;
249
+ }
250
+
251
+ if (particle.alpha < 0.01 || particle.size <= 0.5) {
252
+ continue;
253
+ }
254
+
255
+ this._effect.update(this._ctx!, particle);
256
+ }
257
+ }
258
+ }
259
+
260
+ //
261
+ // Effects
262
+ //
263
+
264
+ type Particle = {
265
+ x: number;
266
+ y: number;
267
+ vx: number;
268
+ vy: number;
269
+ size: number;
270
+ color: (string | number)[];
271
+ alpha: number;
272
+ theta?: number;
273
+ drag?: number;
274
+ wander?: number;
275
+ };
276
+
277
+ abstract class Effect {
278
+ constructor(protected readonly _options: BlastOptions) {}
279
+ abstract create(x: number, y: number, color: Particle['color']): Particle;
280
+ abstract update(ctx: CanvasRenderingContext2D, particle: Particle): void;
281
+ }
282
+
283
+ class Effect1 extends Effect {
284
+ create(x: number, y: number, color: Particle['color']) {
285
+ return {
286
+ x,
287
+ y: y + 10,
288
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
289
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
290
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
291
+ color,
292
+ alpha: 1,
293
+ };
294
+ }
295
+
296
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
297
+ particle.vy += this._options.particleGravity;
298
+ particle.x += particle.vx;
299
+ particle.y += particle.vy;
300
+ particle.alpha *= this._options.particleAlphaFadeout;
301
+
302
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
303
+ ctx.fillRect(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, particle.size);
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Based on Soulwire's demo.
309
+ * http://codepen.io/soulwire/pen/foktm
310
+ */
311
+ class Effect2 extends Effect {
312
+ create(x: number, y: number, color: Particle['color']) {
313
+ return {
314
+ x,
315
+ y: y + 10,
316
+ vx: random(this._options.particleVelocityRange.x[0], this._options.particleVelocityRange.x[1]),
317
+ vy: random(this._options.particleVelocityRange.y[0], this._options.particleVelocityRange.y[1]),
318
+ size: random(this._options.particleSize.min, this._options.particleSize.max),
319
+ color,
320
+ alpha: 1,
321
+ theta: (random(0, 360) * Math.PI) / 180,
322
+ drag: 0.92,
323
+ wander: 0.15,
324
+ };
325
+ }
326
+
327
+ update(ctx: CanvasRenderingContext2D, particle: Particle) {
328
+ particle.vy += this._options.particleGravity;
329
+ particle.x += particle.vx;
330
+ particle.y += particle.vy;
331
+ particle.vx *= particle.drag!;
332
+ particle.vy *= particle.drag!;
333
+ particle.theta! += random(-0.5, 0.5);
334
+ particle.vx += Math.sin(particle.theta!) * 0.1;
335
+ particle.vy += Math.cos(particle.theta!) * 0.1;
336
+ particle.size *= this._options.particleShrinkRate;
337
+ particle.alpha *= this._options.particleAlphaFadeout;
338
+
339
+ ctx.fillStyle = `rgba(${particle.color[0]},${particle.color[1]},${particle.color[2]},${particle.alpha})`;
340
+ ctx.beginPath();
341
+ ctx.arc(Math.round(particle.x - 1), Math.round(particle.y - 1), particle.size, 0, 2 * Math.PI);
342
+ ctx.fill();
343
+ }
344
+ }
345
+
346
+ //
347
+ // Utils
348
+ //
349
+
350
+ function throttle<T>(callback: (arg: T) => void, limit: number): (arg: T) => void {
351
+ let wait = false;
352
+ return function (...args: any[]) {
353
+ if (!wait) {
354
+ // @ts-ignore
355
+ callback.apply(this, args);
356
+ wait = true;
357
+ setTimeout(() => {
358
+ wait = false;
359
+ }, limit);
360
+ }
361
+ };
362
+ }
363
+
364
+ const getRGBComponents = (node: Element, color: BlastOptions['color']): Particle['color'] => {
365
+ if (typeof color === 'function') {
366
+ return color();
367
+ }
368
+
369
+ const bgColor = getComputedStyle(node).color;
370
+ if (bgColor) {
371
+ const x = bgColor.match(/(\d+), (\d+), (\d+)/)?.slice(1);
372
+ if (x) {
373
+ return x;
374
+ }
375
+ }
376
+
377
+ return [50, 50, 50];
378
+ };
379
+
380
+ const random = (min: number, max: number) => {
381
+ if (!max) {
382
+ max = min;
383
+ min = 0;
384
+ }
385
+
386
+ return min + ~~(Math.random() * (max - min + 1));
387
+ };
@@ -18,25 +18,21 @@ import {
18
18
  import { debounce } from '@dxos/async';
19
19
  import { invariant } from '@dxos/invariant';
20
20
 
21
- // 1. TODO(burdon): Make atomic (for tasklist also).
22
- // - https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
23
- // - https://discuss.codemirror.net/t/creating-atomic-replace-decorations/2961
24
- // - https://codemirror.net/docs/ref/#state.EditorState%5EtransactionFilter (transaction filter: move, delete).
25
- // 2. TODO(burdon): Create/track threads in composer (change global state). Select/follow. Scroll with page.
26
- // - Separate from chat/search.
27
- // 3. TODO(burdon): Support multiple threads in sidebar?
28
- // 4. TODO(burdon): Anchor AI thread when creating section.
29
- // 5. TODO(burdon): Button to resolve thread to close comments.
30
-
31
- // 6. TODO(burdon): Perf: Update existing rangeset.
32
- // https://discuss.codemirror.net/t/rangeset-with-metadata-and-different-decorations/3874
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
- constructor(private readonly _pos: number, private readonly _id: string) {
35
+ constructor(private readonly _pos: number, private readonly _id: string, private readonly _handleClick: () => void) {
40
36
  super();
41
37
  invariant(this._id);
42
38
  }
@@ -48,29 +44,15 @@ class BookmarkWidget extends WidgetType {
48
44
  override toDOM() {
49
45
  const span = document.createElement('span');
50
46
  span.className = 'cm-bookmark';
51
- // TODO(burdon): Call out to react?
52
- // https://emojifinder.com/comment
53
- span.textContent = '💬';
47
+ span.textContent = '§';
48
+ span.onclick = () => this._handleClick();
54
49
  return span;
55
50
  }
56
51
  }
57
52
 
58
- // TODO(burdon): Reconcile with theme.
59
- const styles = EditorView.baseTheme({
60
- '& .cm-bookmark': {
61
- cursor: 'pointer',
62
- margin: '4px',
63
- padding: '4px',
64
- backgroundColor: 'yellow',
65
- },
66
- '& .cm-bookmark-selected': {
67
- backgroundColor: 'orange',
68
- },
69
- });
70
-
71
53
  type CommentsInfo = {
72
54
  active?: string;
73
- items: { id: string; pos: number; location: Rect | null }[];
55
+ items?: { id: string; pos: number; location: Rect | null }[];
74
56
  };
75
57
 
76
58
  export type CommentsOptions = {
@@ -91,7 +73,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
91
73
  const id = match[1];
92
74
  return Decoration.replace({
93
75
  id,
94
- widget: new BookmarkWidget(pos, id),
76
+ widget: new BookmarkWidget(pos, id, () => handleUpdate({ active: id })),
95
77
  });
96
78
  },
97
79
  });
@@ -116,7 +98,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
116
98
  },
117
99
  );
118
100
 
119
- const doUpdate = debounce((data: CommentsInfo) => {
101
+ const handleUpdate = debounce((data: CommentsInfo) => {
120
102
  options.onUpdate?.(data);
121
103
  }, 200);
122
104
 
@@ -158,8 +140,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
158
140
  const decorations: { from: number; to: number; value: Decoration }[] = [];
159
141
  const rangeSet = view.plugin(bookmarks)?.bookmarks;
160
142
  let closest = Infinity;
161
- // TODO(burdon): Always shows entire document?
162
- const { from, to } = view.visibleRanges?.[0] ?? { from: 0, to: view.state.doc.length };
143
+ // TODO(burdon): Handle multiple visible ranges in large documents.
144
+ const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: view.state.doc.length };
163
145
  rangeSet?.between(from, to, (from, to, value) => {
164
146
  decorations.push({ from, to, value });
165
147
  const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
@@ -171,7 +153,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
171
153
  });
172
154
 
173
155
  if (decorations.length) {
174
- doUpdate({
156
+ handleUpdate({
175
157
  active,
176
158
  items: decorations.map(
177
159
  ({
@@ -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
 
@@ -13,28 +13,21 @@ import {
13
13
  type ViewUpdate,
14
14
  } from '@codemirror/view';
15
15
 
16
- // TODO(burdon): Modes: parallel parsing and decoration (e.g., associated with language).
17
- // TODO(burdon): Add-on: runmode: run lexer over content (with rendering codemirror).
18
- // https://codemirror.net/5/doc/manual.html#addon_runmode
19
- // TODO(burdon): Add-on: dialog.
20
- // TODO(burdon): Comments: https://codemirror.net/5/doc/manual.html#setBookmark
21
- // TODO(burdon): Split view: https://codemirror.net/examples/split
22
- // TODO(burdon): https://codemirror.net/5/demo/simplemode.html
23
-
24
- // https://codemirror.net/examples/autocompletion
25
- // https://codemirror.net/docs/ref/#autocomplete.autocompletion
26
- // https://codemirror.net/docs/ref/#autocomplete.Completion
27
-
28
- // TODO(burdon): Hint to customize?
29
- // https://codemirror.net/examples/autocompletion
30
-
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
  /**
@@ -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];
@@ -0,0 +1,67 @@
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
+ const cursor = state.selection.main.head;
29
+
30
+ syntaxTree(state).iterate({
31
+ enter: (node) => {
32
+ if (node.name === 'Image') {
33
+ const urlNode = node.node.getChild('URL');
34
+ if (urlNode) {
35
+ const hide = state.readOnly || cursor < node.from || cursor > node.to;
36
+ const url = state.sliceDoc(urlNode.from, urlNode.to);
37
+ builder.add(
38
+ hide ? node.from : node.to,
39
+ node.to,
40
+ Decoration.replace({
41
+ widget: new ImageWidget(url),
42
+ }),
43
+ );
44
+ }
45
+ }
46
+ },
47
+ });
48
+
49
+ return builder.finish();
50
+ };
51
+
52
+ class ImageWidget extends WidgetType {
53
+ constructor(readonly _url: string) {
54
+ super();
55
+ }
56
+
57
+ override eq(other: WidgetType) {
58
+ return this._url === (other as any as ImageWidget)?._url;
59
+ }
60
+
61
+ toDOM(view: EditorView) {
62
+ const img = document.createElement('img');
63
+ img.setAttribute('src', this._url);
64
+ img.setAttribute('class', 'cm-image');
65
+ return img;
66
+ }
67
+ }
@@ -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';