@dxos/react-ui-editor 0.3.10-main.90b298e → 0.3.10-main.c5193cc
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 +729 -153
- 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 +19 -1
- package/dist/types/src/components/TextEditor/MarkdownEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts +2 -1
- package/dist/types/src/components/TextEditor/TextEditor.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/autocomplete.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/basic.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts +25 -0
- package/dist/types/src/components/TextEditor/extensions/blast.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/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/demo.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/demo.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/image.d.ts +4 -0
- package/dist/types/src/components/TextEditor/extensions/image.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts +4 -0
- package/dist/types/src/components/TextEditor/extensions/index.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/link.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/listener.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/bundle.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts +6 -2
- package/dist/types/src/components/TextEditor/extensions/markdown/highlight.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts +6 -0
- package/dist/types/src/components/TextEditor/extensions/mention.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/table.d.ts +8 -0
- package/dist/types/src/components/TextEditor/extensions/table.d.ts.map +1 -0
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts +2 -1
- package/dist/types/src/components/TextEditor/extensions/tasklist.d.ts.map +1 -1
- package/dist/types/src/components/TextEditor/themes/default.d.ts +5 -0
- package/dist/types/src/components/TextEditor/themes/default.d.ts.map +1 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts +8 -1
- package/dist/types/src/hooks/automerge/automerge.stories.d.ts.map +1 -1
- package/dist/types/src/{components/TextEditor → hooks/yjs}/yjs.stories.d.ts +1 -1
- package/dist/types/src/hooks/yjs/yjs.stories.d.ts.map +1 -0
- package/dist/types/src/styles/markdown.d.ts +3 -2
- package/dist/types/src/styles/markdown.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/components/TextEditor/MarkdownEditor.stories.tsx +101 -25
- package/src/components/TextEditor/TextEditor.tsx +13 -23
- package/src/components/TextEditor/extensions/autocomplete.ts +5 -20
- package/src/components/TextEditor/extensions/basic.ts +2 -1
- package/src/components/TextEditor/extensions/blast.ts +387 -0
- package/src/components/TextEditor/extensions/comments.ts +8 -7
- package/src/components/TextEditor/extensions/demo.ts +4 -1
- package/src/components/TextEditor/extensions/image.ts +67 -0
- package/src/components/TextEditor/extensions/index.ts +4 -0
- package/src/components/TextEditor/extensions/link.ts +16 -23
- package/src/components/TextEditor/extensions/listener.ts +3 -3
- package/src/components/TextEditor/extensions/markdown/bundle.ts +28 -17
- package/src/components/TextEditor/extensions/markdown/highlight.ts +118 -104
- package/src/components/TextEditor/extensions/mention.ts +31 -0
- package/src/components/TextEditor/extensions/table.ts +123 -0
- package/src/components/TextEditor/extensions/tasklist.ts +34 -20
- package/src/components/TextEditor/themes/default.ts +54 -2
- package/src/hooks/automerge/automerge.stories.tsx +3 -2
- package/src/{components/TextEditor → hooks/yjs}/yjs.stories.tsx +1 -1
- package/src/styles/markdown.ts +14 -8
- 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
|
+
};
|
|
@@ -36,7 +36,7 @@ import { invariant } from '@dxos/invariant';
|
|
|
36
36
|
// https://discuss.codemirror.net/t/easily-track-remove-content-with-decorations/4606
|
|
37
37
|
|
|
38
38
|
class BookmarkWidget extends WidgetType {
|
|
39
|
-
constructor(private readonly _pos: number, private readonly _id: string) {
|
|
39
|
+
constructor(private readonly _pos: number, private readonly _id: string, private readonly _handleClick: () => void) {
|
|
40
40
|
super();
|
|
41
41
|
invariant(this._id);
|
|
42
42
|
}
|
|
@@ -51,6 +51,7 @@ class BookmarkWidget extends WidgetType {
|
|
|
51
51
|
// TODO(burdon): Call out to react?
|
|
52
52
|
// https://emojifinder.com/comment
|
|
53
53
|
span.textContent = '💬';
|
|
54
|
+
span.onclick = () => this._handleClick();
|
|
54
55
|
return span;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -70,7 +71,7 @@ const styles = EditorView.baseTheme({
|
|
|
70
71
|
|
|
71
72
|
type CommentsInfo = {
|
|
72
73
|
active?: string;
|
|
73
|
-
items
|
|
74
|
+
items?: { id: string; pos: number; location: Rect | null }[];
|
|
74
75
|
};
|
|
75
76
|
|
|
76
77
|
export type CommentsOptions = {
|
|
@@ -91,7 +92,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
91
92
|
const id = match[1];
|
|
92
93
|
return Decoration.replace({
|
|
93
94
|
id,
|
|
94
|
-
widget: new BookmarkWidget(pos, id),
|
|
95
|
+
widget: new BookmarkWidget(pos, id, () => handleUpdate({ active: id })),
|
|
95
96
|
});
|
|
96
97
|
},
|
|
97
98
|
});
|
|
@@ -116,7 +117,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
116
117
|
},
|
|
117
118
|
);
|
|
118
119
|
|
|
119
|
-
const
|
|
120
|
+
const handleUpdate = debounce((data: CommentsInfo) => {
|
|
120
121
|
options.onUpdate?.(data);
|
|
121
122
|
}, 200);
|
|
122
123
|
|
|
@@ -158,8 +159,8 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
158
159
|
const decorations: { from: number; to: number; value: Decoration }[] = [];
|
|
159
160
|
const rangeSet = view.plugin(bookmarks)?.bookmarks;
|
|
160
161
|
let closest = Infinity;
|
|
161
|
-
// TODO(burdon):
|
|
162
|
-
const { from, to } = view.visibleRanges?.[0] ?? { from: 0, to: view.state.doc.length };
|
|
162
|
+
// TODO(burdon): Handle multiple visible ranges in large documents.
|
|
163
|
+
const { from, to } = /* view.visibleRanges?.[0] ?? */ { from: 0, to: view.state.doc.length };
|
|
163
164
|
rangeSet?.between(from, to, (from, to, value) => {
|
|
164
165
|
decorations.push({ from, to, value });
|
|
165
166
|
const d = Math.min(Math.abs(pos - from), Math.abs(pos - to));
|
|
@@ -171,7 +172,7 @@ export const comments = (options: CommentsOptions = {}): Extension => {
|
|
|
171
172
|
});
|
|
172
173
|
|
|
173
174
|
if (decorations.length) {
|
|
174
|
-
|
|
175
|
+
handleUpdate({
|
|
175
176
|
active,
|
|
176
177
|
items: decorations.map(
|
|
177
178
|
({
|
|
@@ -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,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';
|
|
@@ -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
|
-
|
|
21
|
+
export type LinkOptions = {
|
|
22
|
+
link?: boolean;
|
|
23
|
+
onRender?: (el: Element, url: string) => void;
|
|
24
|
+
};
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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(
|
|
@@ -85,6 +94,7 @@ class LinkText extends WidgetType {
|
|
|
85
94
|
const update = (state: EditorState, options: LinkOptions) => {
|
|
86
95
|
const builder = new RangeSetBuilder();
|
|
87
96
|
const cursor = state.selection.main.head;
|
|
97
|
+
|
|
88
98
|
syntaxTree(state).iterate({
|
|
89
99
|
enter: (node) => {
|
|
90
100
|
// Check if cursor is inside text.
|
|
@@ -125,20 +135,3 @@ const update = (state: EditorState, options: LinkOptions) => {
|
|
|
125
135
|
|
|
126
136
|
return builder.finish();
|
|
127
137
|
};
|
|
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
|
-
|
|
13
|
-
|
|
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
|
+
};
|