wavesurfer.js 7.11.1 → 7.12.1
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/README.md +1 -1
- package/dist/__tests__/memory-leaks.test.d.ts +7 -0
- package/dist/__tests__/memory-leaks.test.js +281 -0
- package/dist/__tests__/player.test.js +107 -0
- package/dist/__tests__/renderer-utils.test.js +55 -2
- package/dist/draggable.d.ts +4 -0
- package/dist/draggable.js +4 -0
- package/dist/player.d.ts +21 -0
- package/dist/player.js +86 -0
- package/dist/plugins/envelope.cjs +1 -1
- package/dist/plugins/envelope.esm.js +1 -1
- package/dist/plugins/envelope.js +1 -1
- package/dist/plugins/envelope.min.js +1 -1
- package/dist/plugins/hover.cjs +1 -1
- package/dist/plugins/hover.d.ts +0 -3
- package/dist/plugins/hover.esm.js +1 -1
- package/dist/plugins/hover.js +1 -1
- package/dist/plugins/hover.min.js +1 -1
- package/dist/plugins/minimap.cjs +1 -1
- package/dist/plugins/minimap.esm.js +1 -1
- package/dist/plugins/minimap.js +1 -1
- package/dist/plugins/minimap.min.js +1 -1
- package/dist/plugins/record.cjs +1 -1
- package/dist/plugins/record.esm.js +1 -1
- package/dist/plugins/record.js +1 -1
- package/dist/plugins/record.min.js +1 -1
- package/dist/plugins/regions.cjs +1 -1
- package/dist/plugins/regions.esm.js +1 -1
- package/dist/plugins/regions.js +1 -1
- package/dist/plugins/regions.min.js +1 -1
- package/dist/plugins/timeline.cjs +1 -1
- package/dist/plugins/timeline.d.ts +1 -1
- package/dist/plugins/timeline.esm.js +1 -1
- package/dist/plugins/timeline.js +1 -1
- package/dist/plugins/timeline.min.js +1 -1
- package/dist/plugins/zoom.cjs +1 -1
- package/dist/plugins/zoom.esm.js +1 -1
- package/dist/plugins/zoom.js +1 -1
- package/dist/plugins/zoom.min.js +1 -1
- package/dist/reactive/__tests__/drag-stream.test.d.ts +1 -0
- package/dist/reactive/__tests__/drag-stream.test.js +199 -0
- package/dist/reactive/__tests__/event-stream-emitter.test.d.ts +1 -0
- package/dist/reactive/__tests__/event-stream-emitter.test.js +230 -0
- package/dist/reactive/__tests__/event-streams.test.d.ts +1 -0
- package/dist/reactive/__tests__/event-streams.test.js +278 -0
- package/dist/reactive/__tests__/media-event-bridge.test.d.ts +1 -0
- package/dist/reactive/__tests__/media-event-bridge.test.js +202 -0
- package/dist/reactive/__tests__/render-scheduler.test.d.ts +1 -0
- package/dist/reactive/__tests__/render-scheduler.test.js +216 -0
- package/dist/reactive/__tests__/scroll-stream.test.d.ts +1 -0
- package/dist/reactive/__tests__/scroll-stream.test.js +191 -0
- package/dist/reactive/__tests__/state-event-emitter.test.d.ts +1 -0
- package/dist/reactive/__tests__/state-event-emitter.test.js +234 -0
- package/dist/reactive/__tests__/store.test.d.ts +1 -0
- package/dist/reactive/__tests__/store.test.js +271 -0
- package/dist/reactive/drag-stream.d.ts +48 -0
- package/dist/reactive/drag-stream.js +146 -0
- package/dist/reactive/event-stream-emitter.d.ts +104 -0
- package/dist/reactive/event-stream-emitter.js +151 -0
- package/dist/reactive/event-streams.d.ts +65 -0
- package/dist/reactive/event-streams.js +146 -0
- package/dist/reactive/media-event-bridge.d.ts +51 -0
- package/dist/reactive/media-event-bridge.js +152 -0
- package/dist/reactive/render-scheduler.d.ts +43 -0
- package/dist/reactive/render-scheduler.js +75 -0
- package/dist/reactive/scroll-stream.d.ts +93 -0
- package/dist/reactive/scroll-stream.js +131 -0
- package/dist/reactive/state-event-emitter.d.ts +105 -0
- package/dist/reactive/state-event-emitter.js +213 -0
- package/dist/reactive/store.d.ts +66 -0
- package/dist/reactive/store.js +115 -0
- package/dist/renderer-utils.d.ts +13 -4
- package/dist/renderer-utils.js +35 -15
- package/dist/renderer.d.ts +2 -1
- package/dist/renderer.js +51 -48
- package/dist/state/__tests__/wavesurfer-state.test.d.ts +1 -0
- package/dist/state/__tests__/wavesurfer-state.test.js +268 -0
- package/dist/state/wavesurfer-state.d.ts +90 -0
- package/dist/state/wavesurfer-state.js +124 -0
- package/dist/types.d.ts +165 -1
- package/dist/wavesurfer.cjs +1 -1
- package/dist/wavesurfer.d.ts +16 -0
- package/dist/wavesurfer.esm.js +1 -1
- package/dist/wavesurfer.js +32 -0
- package/dist/wavesurfer.min.js +1 -1
- package/package.json +2 -3
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { signal, computed, effect } from '../store';
|
|
2
|
+
describe('signal', () => {
|
|
3
|
+
it('should create a signal with initial value', () => {
|
|
4
|
+
const count = signal(0);
|
|
5
|
+
expect(count.value).toBe(0);
|
|
6
|
+
});
|
|
7
|
+
it('should update value with set()', () => {
|
|
8
|
+
const count = signal(0);
|
|
9
|
+
count.set(5);
|
|
10
|
+
expect(count.value).toBe(5);
|
|
11
|
+
});
|
|
12
|
+
it('should update value with update()', () => {
|
|
13
|
+
const count = signal(0);
|
|
14
|
+
count.update((n) => n + 1);
|
|
15
|
+
expect(count.value).toBe(1);
|
|
16
|
+
});
|
|
17
|
+
it('should notify subscribers when value changes', () => {
|
|
18
|
+
const count = signal(0);
|
|
19
|
+
const callback = jest.fn();
|
|
20
|
+
count.subscribe(callback);
|
|
21
|
+
count.set(5);
|
|
22
|
+
expect(callback).toHaveBeenCalledWith(5);
|
|
23
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
24
|
+
});
|
|
25
|
+
it('should not notify if value does not change', () => {
|
|
26
|
+
const count = signal(0);
|
|
27
|
+
const callback = jest.fn();
|
|
28
|
+
count.subscribe(callback);
|
|
29
|
+
count.set(0); // Same value
|
|
30
|
+
expect(callback).not.toHaveBeenCalled();
|
|
31
|
+
});
|
|
32
|
+
it('should support multiple subscribers', () => {
|
|
33
|
+
const count = signal(0);
|
|
34
|
+
const callback1 = jest.fn();
|
|
35
|
+
const callback2 = jest.fn();
|
|
36
|
+
count.subscribe(callback1);
|
|
37
|
+
count.subscribe(callback2);
|
|
38
|
+
count.set(5);
|
|
39
|
+
expect(callback1).toHaveBeenCalledWith(5);
|
|
40
|
+
expect(callback2).toHaveBeenCalledWith(5);
|
|
41
|
+
});
|
|
42
|
+
it('should unsubscribe correctly', () => {
|
|
43
|
+
const count = signal(0);
|
|
44
|
+
const callback = jest.fn();
|
|
45
|
+
const unsubscribe = count.subscribe(callback);
|
|
46
|
+
count.set(1);
|
|
47
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
48
|
+
unsubscribe();
|
|
49
|
+
count.set(2);
|
|
50
|
+
expect(callback).toHaveBeenCalledTimes(1); // Should not be called again
|
|
51
|
+
});
|
|
52
|
+
it('should work with object values', () => {
|
|
53
|
+
const state = signal({ count: 0 });
|
|
54
|
+
const callback = jest.fn();
|
|
55
|
+
state.subscribe(callback);
|
|
56
|
+
state.set({ count: 1 });
|
|
57
|
+
expect(callback).toHaveBeenCalledWith({ count: 1 });
|
|
58
|
+
});
|
|
59
|
+
it('should detect reference equality for objects', () => {
|
|
60
|
+
const obj = { count: 0 };
|
|
61
|
+
const state = signal(obj);
|
|
62
|
+
const callback = jest.fn();
|
|
63
|
+
state.subscribe(callback);
|
|
64
|
+
state.set(obj); // Same reference
|
|
65
|
+
expect(callback).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('computed', () => {
|
|
69
|
+
it('should compute initial value', () => {
|
|
70
|
+
const count = signal(5);
|
|
71
|
+
const doubled = computed(() => count.value * 2, [count]);
|
|
72
|
+
expect(doubled.value).toBe(10);
|
|
73
|
+
});
|
|
74
|
+
it('should recompute when dependency changes', () => {
|
|
75
|
+
const count = signal(5);
|
|
76
|
+
const doubled = computed(() => count.value * 2, [count]);
|
|
77
|
+
count.set(10);
|
|
78
|
+
expect(doubled.value).toBe(20);
|
|
79
|
+
});
|
|
80
|
+
it('should work with multiple dependencies', () => {
|
|
81
|
+
const a = signal(2);
|
|
82
|
+
const b = signal(3);
|
|
83
|
+
const sum = computed(() => a.value + b.value, [a, b]);
|
|
84
|
+
expect(sum.value).toBe(5);
|
|
85
|
+
a.set(5);
|
|
86
|
+
expect(sum.value).toBe(8);
|
|
87
|
+
b.set(7);
|
|
88
|
+
expect(sum.value).toBe(12);
|
|
89
|
+
});
|
|
90
|
+
it('should notify subscribers when computed value changes', () => {
|
|
91
|
+
const count = signal(5);
|
|
92
|
+
const doubled = computed(() => count.value * 2, [count]);
|
|
93
|
+
const callback = jest.fn();
|
|
94
|
+
doubled.subscribe(callback);
|
|
95
|
+
count.set(10);
|
|
96
|
+
expect(callback).toHaveBeenCalledWith(20);
|
|
97
|
+
});
|
|
98
|
+
it('should not notify if computed value does not change', () => {
|
|
99
|
+
const count = signal(5);
|
|
100
|
+
const isPositive = computed(() => count.value > 0, [count]);
|
|
101
|
+
const callback = jest.fn();
|
|
102
|
+
isPositive.subscribe(callback);
|
|
103
|
+
count.set(10); // Still positive
|
|
104
|
+
expect(callback).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
it('should support nested computed values', () => {
|
|
107
|
+
const count = signal(5);
|
|
108
|
+
const doubled = computed(() => count.value * 2, [count]);
|
|
109
|
+
const quadrupled = computed(() => doubled.value * 2, [doubled]);
|
|
110
|
+
expect(quadrupled.value).toBe(20);
|
|
111
|
+
count.set(10);
|
|
112
|
+
expect(quadrupled.value).toBe(40);
|
|
113
|
+
});
|
|
114
|
+
it('should cleanup subscriptions when computed is unsubscribed', () => {
|
|
115
|
+
const count = signal(5);
|
|
116
|
+
const doubled = computed(() => count.value * 2, [count]);
|
|
117
|
+
const callback = jest.fn();
|
|
118
|
+
const unsubscribe = doubled.subscribe(callback);
|
|
119
|
+
count.set(10);
|
|
120
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
121
|
+
unsubscribe();
|
|
122
|
+
count.set(15);
|
|
123
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
124
|
+
});
|
|
125
|
+
it('should be read-only (no set method)', () => {
|
|
126
|
+
const count = signal(5);
|
|
127
|
+
const doubled = computed(() => count.value * 2, [count]);
|
|
128
|
+
expect(doubled.set).toBeUndefined();
|
|
129
|
+
expect(doubled.update).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe('effect', () => {
|
|
133
|
+
it('should run immediately', () => {
|
|
134
|
+
const fn = jest.fn();
|
|
135
|
+
const count = signal(0);
|
|
136
|
+
effect(fn, [count]);
|
|
137
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
138
|
+
});
|
|
139
|
+
it('should run when dependency changes', () => {
|
|
140
|
+
const fn = jest.fn();
|
|
141
|
+
const count = signal(0);
|
|
142
|
+
effect(fn, [count]);
|
|
143
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
144
|
+
count.set(1);
|
|
145
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
146
|
+
count.set(2);
|
|
147
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
148
|
+
});
|
|
149
|
+
it('should run with multiple dependencies', () => {
|
|
150
|
+
const fn = jest.fn();
|
|
151
|
+
const a = signal(0);
|
|
152
|
+
const b = signal(0);
|
|
153
|
+
effect(fn, [a, b]);
|
|
154
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
155
|
+
a.set(1);
|
|
156
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
157
|
+
b.set(1);
|
|
158
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
159
|
+
});
|
|
160
|
+
it('should run cleanup before re-running effect', () => {
|
|
161
|
+
const cleanup = jest.fn();
|
|
162
|
+
const fn = jest.fn(() => cleanup);
|
|
163
|
+
const count = signal(0);
|
|
164
|
+
effect(fn, [count]);
|
|
165
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
166
|
+
expect(cleanup).not.toHaveBeenCalled();
|
|
167
|
+
count.set(1);
|
|
168
|
+
expect(cleanup).toHaveBeenCalledTimes(1); // Cleanup from first run
|
|
169
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
170
|
+
count.set(2);
|
|
171
|
+
expect(cleanup).toHaveBeenCalledTimes(2); // Cleanup from second run
|
|
172
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
173
|
+
});
|
|
174
|
+
it('should run cleanup on unsubscribe', () => {
|
|
175
|
+
const cleanup = jest.fn();
|
|
176
|
+
const fn = jest.fn(() => cleanup);
|
|
177
|
+
const count = signal(0);
|
|
178
|
+
const unsubscribe = effect(fn, [count]);
|
|
179
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
180
|
+
expect(cleanup).not.toHaveBeenCalled();
|
|
181
|
+
unsubscribe();
|
|
182
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
183
|
+
});
|
|
184
|
+
it('should stop running after unsubscribe', () => {
|
|
185
|
+
const fn = jest.fn();
|
|
186
|
+
const count = signal(0);
|
|
187
|
+
const unsubscribe = effect(fn, [count]);
|
|
188
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
189
|
+
count.set(1);
|
|
190
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
191
|
+
unsubscribe();
|
|
192
|
+
count.set(2);
|
|
193
|
+
expect(fn).toHaveBeenCalledTimes(2); // Should not increase
|
|
194
|
+
});
|
|
195
|
+
it('should work with computed dependencies', () => {
|
|
196
|
+
const fn = jest.fn();
|
|
197
|
+
const count = signal(0);
|
|
198
|
+
const doubled = computed(() => count.value * 2, [count]);
|
|
199
|
+
effect(fn, [doubled]);
|
|
200
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
201
|
+
count.set(1);
|
|
202
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
203
|
+
});
|
|
204
|
+
it('should handle effects without cleanup', () => {
|
|
205
|
+
const fn = jest.fn();
|
|
206
|
+
const count = signal(0);
|
|
207
|
+
const unsubscribe = effect(fn, [count]);
|
|
208
|
+
count.set(1);
|
|
209
|
+
expect(() => unsubscribe()).not.toThrow();
|
|
210
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
211
|
+
});
|
|
212
|
+
it('should handle accessing signal values in effect', () => {
|
|
213
|
+
const values = [];
|
|
214
|
+
const count = signal(0);
|
|
215
|
+
effect(() => {
|
|
216
|
+
values.push(count.value);
|
|
217
|
+
}, [count]);
|
|
218
|
+
count.set(1);
|
|
219
|
+
count.set(2);
|
|
220
|
+
expect(values).toEqual([0, 1, 2]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe('integration tests', () => {
|
|
224
|
+
it('should work with signal -> computed -> effect chain', () => {
|
|
225
|
+
const values = [];
|
|
226
|
+
const count = signal(0);
|
|
227
|
+
const doubled = computed(() => count.value * 2, [count]);
|
|
228
|
+
effect(() => {
|
|
229
|
+
values.push(doubled.value);
|
|
230
|
+
}, [doubled]);
|
|
231
|
+
count.set(5);
|
|
232
|
+
count.set(10);
|
|
233
|
+
expect(values).toEqual([0, 10, 20]);
|
|
234
|
+
});
|
|
235
|
+
it('should handle complex dependency graphs', () => {
|
|
236
|
+
const a = signal(1);
|
|
237
|
+
const b = signal(2);
|
|
238
|
+
const sum = computed(() => a.value + b.value, [a, b]);
|
|
239
|
+
const product = computed(() => a.value * b.value, [a, b]);
|
|
240
|
+
const combined = computed(() => sum.value + product.value, [sum, product]);
|
|
241
|
+
expect(combined.value).toBe(5); // (1+2) + (1*2) = 3 + 2 = 5
|
|
242
|
+
a.set(3);
|
|
243
|
+
expect(combined.value).toBe(11); // (3+2) + (3*2) = 5 + 6 = 11
|
|
244
|
+
b.set(4);
|
|
245
|
+
expect(combined.value).toBe(19); // (3+4) + (3*4) = 7 + 12 = 19
|
|
246
|
+
});
|
|
247
|
+
it('should not create memory leaks with many subscriptions', () => {
|
|
248
|
+
const count = signal(0);
|
|
249
|
+
const unsubscribes = [];
|
|
250
|
+
// Create 1000 subscriptions
|
|
251
|
+
for (let i = 0; i < 1000; i++) {
|
|
252
|
+
unsubscribes.push(count.subscribe(() => { }));
|
|
253
|
+
}
|
|
254
|
+
// Unsubscribe all
|
|
255
|
+
unsubscribes.forEach((unsub) => unsub());
|
|
256
|
+
// Signal should still work
|
|
257
|
+
count.set(5);
|
|
258
|
+
expect(count.value).toBe(5);
|
|
259
|
+
});
|
|
260
|
+
it('should handle rapid updates correctly', () => {
|
|
261
|
+
const count = signal(0);
|
|
262
|
+
const callback = jest.fn();
|
|
263
|
+
count.subscribe(callback);
|
|
264
|
+
// Rapid updates
|
|
265
|
+
for (let i = 1; i <= 100; i++) {
|
|
266
|
+
count.set(i);
|
|
267
|
+
}
|
|
268
|
+
expect(callback).toHaveBeenCalledTimes(100);
|
|
269
|
+
expect(count.value).toBe(100);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive drag stream utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides declarative drag handling using reactive streams.
|
|
5
|
+
* Automatically handles mouseup cleanup and supports constraints.
|
|
6
|
+
*/
|
|
7
|
+
import { type Signal } from './store.js';
|
|
8
|
+
export interface DragEvent {
|
|
9
|
+
type: 'start' | 'move' | 'end';
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
deltaX?: number;
|
|
13
|
+
deltaY?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DragStreamOptions {
|
|
16
|
+
/** Minimum distance to move before dragging starts (default: 3) */
|
|
17
|
+
threshold?: number;
|
|
18
|
+
/** Mouse button to listen for (default: 0 = left button) */
|
|
19
|
+
mouseButton?: number;
|
|
20
|
+
/** Delay before touch drag starts in ms (default: 100) */
|
|
21
|
+
touchDelay?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create a reactive drag stream from an element
|
|
25
|
+
*
|
|
26
|
+
* Emits drag events (start, move, end) as the user drags the element.
|
|
27
|
+
* Automatically handles pointer capture, multi-touch prevention, and cleanup.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const dragSignal = createDragStream(element)
|
|
32
|
+
*
|
|
33
|
+
* effect(() => {
|
|
34
|
+
* const drag = dragSignal.value
|
|
35
|
+
* if (drag?.type === 'move') {
|
|
36
|
+
* console.log('Dragging:', drag.deltaX, drag.deltaY)
|
|
37
|
+
* }
|
|
38
|
+
* }, [dragSignal])
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @param element - Element to make draggable
|
|
42
|
+
* @param options - Drag configuration options
|
|
43
|
+
* @returns Signal emitting drag events and cleanup function
|
|
44
|
+
*/
|
|
45
|
+
export declare function createDragStream(element: HTMLElement, options?: DragStreamOptions): {
|
|
46
|
+
signal: Signal<DragEvent | null>;
|
|
47
|
+
cleanup: () => void;
|
|
48
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive drag stream utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides declarative drag handling using reactive streams.
|
|
5
|
+
* Automatically handles mouseup cleanup and supports constraints.
|
|
6
|
+
*/
|
|
7
|
+
import { signal } from './store.js';
|
|
8
|
+
import { cleanup } from './event-streams.js';
|
|
9
|
+
/**
|
|
10
|
+
* Create a reactive drag stream from an element
|
|
11
|
+
*
|
|
12
|
+
* Emits drag events (start, move, end) as the user drags the element.
|
|
13
|
+
* Automatically handles pointer capture, multi-touch prevention, and cleanup.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const dragSignal = createDragStream(element)
|
|
18
|
+
*
|
|
19
|
+
* effect(() => {
|
|
20
|
+
* const drag = dragSignal.value
|
|
21
|
+
* if (drag?.type === 'move') {
|
|
22
|
+
* console.log('Dragging:', drag.deltaX, drag.deltaY)
|
|
23
|
+
* }
|
|
24
|
+
* }, [dragSignal])
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @param element - Element to make draggable
|
|
28
|
+
* @param options - Drag configuration options
|
|
29
|
+
* @returns Signal emitting drag events and cleanup function
|
|
30
|
+
*/
|
|
31
|
+
export function createDragStream(element, options = {}) {
|
|
32
|
+
const { threshold = 3, mouseButton = 0, touchDelay = 100 } = options;
|
|
33
|
+
const dragSignal = signal(null);
|
|
34
|
+
const activePointers = new Map();
|
|
35
|
+
const isTouchDevice = matchMedia('(pointer: coarse)').matches;
|
|
36
|
+
let unsubscribeDocument = () => void 0;
|
|
37
|
+
const onPointerDown = (event) => {
|
|
38
|
+
if (event.button !== mouseButton)
|
|
39
|
+
return;
|
|
40
|
+
activePointers.set(event.pointerId, event);
|
|
41
|
+
if (activePointers.size > 1) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let startX = event.clientX;
|
|
45
|
+
let startY = event.clientY;
|
|
46
|
+
let isDragging = false;
|
|
47
|
+
const touchStartTime = Date.now();
|
|
48
|
+
const rect = element.getBoundingClientRect();
|
|
49
|
+
const { left, top } = rect;
|
|
50
|
+
const onPointerMove = (event) => {
|
|
51
|
+
if (event.defaultPrevented || activePointers.size > 1) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (isTouchDevice && Date.now() - touchStartTime < touchDelay)
|
|
55
|
+
return;
|
|
56
|
+
const x = event.clientX;
|
|
57
|
+
const y = event.clientY;
|
|
58
|
+
const dx = x - startX;
|
|
59
|
+
const dy = y - startY;
|
|
60
|
+
if (isDragging || Math.abs(dx) > threshold || Math.abs(dy) > threshold) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
event.stopPropagation();
|
|
63
|
+
if (!isDragging) {
|
|
64
|
+
// Emit start event
|
|
65
|
+
dragSignal.set({
|
|
66
|
+
type: 'start',
|
|
67
|
+
x: startX - left,
|
|
68
|
+
y: startY - top,
|
|
69
|
+
});
|
|
70
|
+
isDragging = true;
|
|
71
|
+
}
|
|
72
|
+
// Emit move event
|
|
73
|
+
dragSignal.set({
|
|
74
|
+
type: 'move',
|
|
75
|
+
x: x - left,
|
|
76
|
+
y: y - top,
|
|
77
|
+
deltaX: dx,
|
|
78
|
+
deltaY: dy,
|
|
79
|
+
});
|
|
80
|
+
startX = x;
|
|
81
|
+
startY = y;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const onPointerUp = (event) => {
|
|
85
|
+
activePointers.delete(event.pointerId);
|
|
86
|
+
if (isDragging) {
|
|
87
|
+
const x = event.clientX;
|
|
88
|
+
const y = event.clientY;
|
|
89
|
+
// Emit end event
|
|
90
|
+
dragSignal.set({
|
|
91
|
+
type: 'end',
|
|
92
|
+
x: x - left,
|
|
93
|
+
y: y - top,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
unsubscribeDocument();
|
|
97
|
+
};
|
|
98
|
+
const onPointerLeave = (e) => {
|
|
99
|
+
activePointers.delete(e.pointerId);
|
|
100
|
+
if (!e.relatedTarget || e.relatedTarget === document.documentElement) {
|
|
101
|
+
onPointerUp(e);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const onClick = (event) => {
|
|
105
|
+
if (isDragging) {
|
|
106
|
+
event.stopPropagation();
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
const onTouchMove = (event) => {
|
|
111
|
+
if (event.defaultPrevented || activePointers.size > 1) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (isDragging) {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
document.addEventListener('pointermove', onPointerMove);
|
|
119
|
+
document.addEventListener('pointerup', onPointerUp);
|
|
120
|
+
document.addEventListener('pointerout', onPointerLeave);
|
|
121
|
+
document.addEventListener('pointercancel', onPointerLeave);
|
|
122
|
+
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
123
|
+
document.addEventListener('click', onClick, { capture: true });
|
|
124
|
+
unsubscribeDocument = () => {
|
|
125
|
+
document.removeEventListener('pointermove', onPointerMove);
|
|
126
|
+
document.removeEventListener('pointerup', onPointerUp);
|
|
127
|
+
document.removeEventListener('pointerout', onPointerLeave);
|
|
128
|
+
document.removeEventListener('pointercancel', onPointerLeave);
|
|
129
|
+
document.removeEventListener('touchmove', onTouchMove);
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
document.removeEventListener('click', onClick, { capture: true });
|
|
132
|
+
}, 10);
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
element.addEventListener('pointerdown', onPointerDown);
|
|
136
|
+
const cleanupFn = () => {
|
|
137
|
+
unsubscribeDocument();
|
|
138
|
+
element.removeEventListener('pointerdown', onPointerDown);
|
|
139
|
+
activePointers.clear();
|
|
140
|
+
cleanup(dragSignal);
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
signal: dragSignal,
|
|
144
|
+
cleanup: cleanupFn,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event stream emitter - bridges EventEmitter to reactive streams
|
|
3
|
+
*
|
|
4
|
+
* Provides reactive stream API on top of traditional EventEmitter.
|
|
5
|
+
* This allows users to choose between callback-based and stream-based APIs.
|
|
6
|
+
*/
|
|
7
|
+
import { type Signal } from './store.js';
|
|
8
|
+
import type EventEmitter from '../event-emitter.js';
|
|
9
|
+
/**
|
|
10
|
+
* Convert an EventEmitter event to a reactive signal/stream
|
|
11
|
+
*
|
|
12
|
+
* Creates a signal that updates whenever the event is emitted.
|
|
13
|
+
* Returns both the signal (for reading values) and cleanup function.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const { stream, cleanup } = toStream(wavesurfer, 'play')
|
|
18
|
+
*
|
|
19
|
+
* // Subscribe to play events
|
|
20
|
+
* stream.subscribe(() => console.log('Playing!'))
|
|
21
|
+
*
|
|
22
|
+
* // Cleanup when done
|
|
23
|
+
* cleanup()
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @param emitter - EventEmitter instance
|
|
27
|
+
* @param eventName - Name of the event to stream
|
|
28
|
+
* @returns Object with stream signal and cleanup function
|
|
29
|
+
*/
|
|
30
|
+
export declare function toStream<T extends Record<string, any[]>, K extends keyof T>(emitter: EventEmitter<T>, eventName: K): {
|
|
31
|
+
stream: Signal<T[K] | null>;
|
|
32
|
+
cleanup: () => void;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Create multiple event streams from an emitter
|
|
36
|
+
*
|
|
37
|
+
* Helper to create streams for multiple events at once.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const streams = toStreams(wavesurfer, ['play', 'pause', 'timeupdate'])
|
|
42
|
+
*
|
|
43
|
+
* streams.play.subscribe(() => console.log('Play'))
|
|
44
|
+
* streams.pause.subscribe(() => console.log('Pause'))
|
|
45
|
+
* streams.timeupdate.subscribe(([time]) => console.log('Time:', time))
|
|
46
|
+
*
|
|
47
|
+
* // Cleanup all
|
|
48
|
+
* streams.cleanup()
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @param emitter - EventEmitter instance
|
|
52
|
+
* @param eventNames - Array of event names to stream
|
|
53
|
+
* @returns Object with streams for each event and cleanup function
|
|
54
|
+
*/
|
|
55
|
+
export declare function toStreams<T extends Record<string, any[]>, K extends keyof T>(emitter: EventEmitter<T>, eventNames: K[]): {
|
|
56
|
+
[P in K]: Signal<T[P] | null>;
|
|
57
|
+
} & {
|
|
58
|
+
cleanup: () => void;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Create a stream that combines multiple events into one
|
|
62
|
+
*
|
|
63
|
+
* Useful when you want to react to any of several events.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const { stream, cleanup } = mergeStreams(wavesurfer, ['play', 'pause'])
|
|
68
|
+
*
|
|
69
|
+
* stream.subscribe(({ event, args }) => {
|
|
70
|
+
* console.log(`Event ${event} fired with`, args)
|
|
71
|
+
* })
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @param emitter - EventEmitter instance
|
|
75
|
+
* @param eventNames - Array of event names to merge
|
|
76
|
+
* @returns Object with merged stream and cleanup function
|
|
77
|
+
*/
|
|
78
|
+
export declare function mergeStreams<T extends Record<string, any[]>, K extends keyof T>(emitter: EventEmitter<T>, eventNames: K[]): {
|
|
79
|
+
stream: Signal<{
|
|
80
|
+
event: K;
|
|
81
|
+
args: T[K];
|
|
82
|
+
} | null>;
|
|
83
|
+
cleanup: () => void;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Helper to map event stream values
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const { stream: timeStream } = toStream(wavesurfer, 'timeupdate')
|
|
91
|
+
* const seconds = mapStream(timeStream, ([time]) => Math.floor(time))
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export declare function mapStream<T, U>(source: Signal<T>, mapper: (value: T) => U): Signal<U>;
|
|
95
|
+
/**
|
|
96
|
+
* Helper to filter event stream values
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const { stream: timeStream } = toStream(wavesurfer, 'timeupdate')
|
|
101
|
+
* const afterTenSeconds = filterStream(timeStream, ([time]) => time > 10)
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export declare function filterStream<T>(source: Signal<T>, predicate: (value: T) => boolean): Signal<T | null>;
|