wavesurfer.js 7.0.0-alpha.8 → 7.0.0-beta.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.
Files changed (47) hide show
  1. package/README.md +90 -18
  2. package/dist/base-plugin.d.ts +6 -4
  3. package/dist/base-plugin.js +9 -3
  4. package/dist/decoder.d.ts +8 -7
  5. package/dist/decoder.js +43 -38
  6. package/dist/event-emitter.d.ts +16 -10
  7. package/dist/event-emitter.js +38 -16
  8. package/dist/fetcher.d.ts +4 -3
  9. package/dist/fetcher.js +5 -15
  10. package/dist/player.d.ts +31 -11
  11. package/dist/player.js +58 -31
  12. package/dist/plugins/envelope.d.ts +71 -0
  13. package/dist/plugins/envelope.js +347 -0
  14. package/dist/plugins/envelope.min.js +1 -0
  15. package/dist/plugins/minimap.d.ts +39 -0
  16. package/dist/plugins/minimap.js +117 -0
  17. package/dist/plugins/minimap.min.js +1 -0
  18. package/dist/plugins/multitrack.d.ts +117 -0
  19. package/dist/plugins/multitrack.js +507 -0
  20. package/dist/plugins/multitrack.min.js +1 -0
  21. package/dist/plugins/record.d.ts +26 -0
  22. package/dist/plugins/record.js +126 -0
  23. package/dist/plugins/record.min.js +1 -0
  24. package/dist/plugins/regions.d.ts +83 -43
  25. package/dist/plugins/regions.js +362 -190
  26. package/dist/plugins/regions.min.js +1 -0
  27. package/dist/plugins/spectrogram.d.ts +69 -0
  28. package/dist/plugins/spectrogram.js +340 -0
  29. package/dist/plugins/spectrogram.min.js +1 -0
  30. package/dist/plugins/timeline.d.ts +26 -9
  31. package/dist/plugins/timeline.js +65 -22
  32. package/dist/plugins/timeline.min.js +1 -0
  33. package/dist/renderer.d.ts +29 -13
  34. package/dist/renderer.js +280 -161
  35. package/dist/timer.d.ts +1 -1
  36. package/dist/wavesurfer.d.ts +151 -0
  37. package/dist/wavesurfer.js +241 -0
  38. package/dist/wavesurfer.min.js +1 -1
  39. package/package.json +54 -27
  40. package/dist/index.d.ts +0 -117
  41. package/dist/index.js +0 -227
  42. package/dist/player-webaudio.d.ts +0 -8
  43. package/dist/player-webaudio.js +0 -32
  44. package/dist/react/useWavesurfer.d.ts +0 -5
  45. package/dist/react/useWavesurfer.js +0 -20
  46. package/dist/wavesurfer.Regions.min.js +0 -1
  47. package/dist/wavesurfer.Timeline.min.js +0 -1
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Multitrack isn't a plugin, but rather a helper class for creating a multitrack audio player.
3
+ * Individual tracks are synced and played together. They can be dragged to set their start position.
4
+ */
5
+ import { type WaveSurferOptions } from '../wavesurfer.js';
6
+ import { type EnvelopePluginOptions } from './envelope.js';
7
+ import EventEmitter from '../event-emitter.js';
8
+ export type TrackId = string | number;
9
+ export type TrackOptions = {
10
+ id: TrackId;
11
+ url?: string;
12
+ peaks?: WaveSurferOptions['peaks'];
13
+ draggable?: boolean;
14
+ startPosition: number;
15
+ startCue?: number;
16
+ endCue?: number;
17
+ fadeInEnd?: number;
18
+ fadeOutStart?: number;
19
+ volume?: number;
20
+ markers?: Array<{
21
+ time: number;
22
+ label?: string;
23
+ color?: string;
24
+ }>;
25
+ intro?: {
26
+ endTime: number;
27
+ label?: string;
28
+ color?: string;
29
+ };
30
+ options?: WaveSurferOptions;
31
+ };
32
+ export type MultitrackOptions = {
33
+ container: HTMLElement;
34
+ minPxPerSec?: number;
35
+ cursorColor?: string;
36
+ cursorWidth?: number;
37
+ trackBackground?: string;
38
+ trackBorderColor?: string;
39
+ rightButtonDrag?: boolean;
40
+ envelopeOptions?: EnvelopePluginOptions;
41
+ };
42
+ export type MultitrackEvents = {
43
+ canplay: [];
44
+ 'start-position-change': [{
45
+ id: TrackId;
46
+ startPosition: number;
47
+ }];
48
+ 'start-cue-change': [{
49
+ id: TrackId;
50
+ startCue: number;
51
+ }];
52
+ 'end-cue-change': [{
53
+ id: TrackId;
54
+ endCue: number;
55
+ }];
56
+ 'fade-in-change': [{
57
+ id: TrackId;
58
+ fadeInEnd: number;
59
+ }];
60
+ 'fade-out-change': [{
61
+ id: TrackId;
62
+ fadeOutStart: number;
63
+ }];
64
+ 'volume-change': [{
65
+ id: TrackId;
66
+ volume: number;
67
+ }];
68
+ 'intro-end-change': [{
69
+ id: TrackId;
70
+ endTime: number;
71
+ }];
72
+ drop: [{
73
+ id: TrackId;
74
+ }];
75
+ };
76
+ export type MultitrackTracks = Array<TrackOptions>;
77
+ declare class MultiTrack extends EventEmitter<MultitrackEvents> {
78
+ private tracks;
79
+ private options;
80
+ private audios;
81
+ private wavesurfers;
82
+ private durations;
83
+ private currentTime;
84
+ private maxDuration;
85
+ private rendering;
86
+ private isDragging;
87
+ private frameRequest;
88
+ private timer;
89
+ private subscriptions;
90
+ private timeline;
91
+ static create(tracks: MultitrackTracks, options: MultitrackOptions): MultiTrack;
92
+ constructor(tracks: MultitrackTracks, options: MultitrackOptions);
93
+ private initDurations;
94
+ private initAudio;
95
+ private initAllAudios;
96
+ private initWavesurfer;
97
+ private initAllWavesurfers;
98
+ private initTimeline;
99
+ private updatePosition;
100
+ private setIsDragging;
101
+ private onDrag;
102
+ private findCurrentTracks;
103
+ private startSync;
104
+ play(): void;
105
+ pause(): void;
106
+ isPlaying(): boolean;
107
+ getCurrentTime(): number;
108
+ /** Position percentage from 0 to 1 */
109
+ seekTo(position: number): void;
110
+ /** Set time in seconds */
111
+ setTime(time: number): void;
112
+ zoom(pxPerSec: number): void;
113
+ addTrack(track: TrackOptions): void;
114
+ destroy(): void;
115
+ setSinkId(sinkId: string): Promise<void[]>;
116
+ }
117
+ export default MultiTrack;
@@ -0,0 +1,507 @@
1
+ /**
2
+ * Multitrack isn't a plugin, but rather a helper class for creating a multitrack audio player.
3
+ * Individual tracks are synced and played together. They can be dragged to set their start position.
4
+ */
5
+ import WaveSurfer from '../wavesurfer.js';
6
+ import RegionsPlugin from './regions.js';
7
+ import TimelinePlugin from './timeline.js';
8
+ import EnvelopePlugin from './envelope.js';
9
+ import EventEmitter from '../event-emitter.js';
10
+ class MultiTrack extends EventEmitter {
11
+ static create(tracks, options) {
12
+ return new MultiTrack(tracks, options);
13
+ }
14
+ constructor(tracks, options) {
15
+ super();
16
+ this.audios = [];
17
+ this.wavesurfers = [];
18
+ this.durations = [];
19
+ this.currentTime = 0;
20
+ this.maxDuration = 0;
21
+ this.isDragging = false;
22
+ this.frameRequest = null;
23
+ this.timer = null;
24
+ this.subscriptions = [];
25
+ this.timeline = null;
26
+ this.tracks = tracks.map((track) => ({
27
+ ...track,
28
+ startPosition: track.startPosition || 0,
29
+ peaks: track.peaks || (track.url ? undefined : [new Float32Array()]),
30
+ }));
31
+ this.options = options;
32
+ this.rendering = initRendering(this.tracks, this.options);
33
+ this.rendering.addDropHandler((trackId) => {
34
+ this.emit('drop', { id: trackId });
35
+ });
36
+ this.initAllAudios().then((durations) => {
37
+ this.initDurations(durations);
38
+ this.initAllWavesurfers();
39
+ this.rendering.containers.forEach((container, index) => {
40
+ const drag = initDragging(container, (delta) => this.onDrag(index, delta), options.rightButtonDrag);
41
+ this.wavesurfers[index].once('destroy', () => drag?.destroy());
42
+ });
43
+ this.rendering.addClickHandler((position) => {
44
+ if (this.isDragging)
45
+ return;
46
+ this.seekTo(position);
47
+ });
48
+ this.emit('canplay');
49
+ });
50
+ }
51
+ initDurations(durations) {
52
+ this.durations = durations;
53
+ this.maxDuration = this.tracks.reduce((max, track, index) => {
54
+ return Math.max(max, track.startPosition + durations[index]);
55
+ }, 0);
56
+ this.rendering.setMainWidth(durations, this.maxDuration);
57
+ }
58
+ initAudio(track) {
59
+ const audio = new Audio(track.url);
60
+ return new Promise((resolve) => {
61
+ if (!audio.src)
62
+ return resolve(audio);
63
+ audio.addEventListener('loadedmetadata', () => resolve(audio), { once: true });
64
+ });
65
+ }
66
+ async initAllAudios() {
67
+ this.audios = await Promise.all(this.tracks.map((track) => this.initAudio(track)));
68
+ return this.audios.map((a) => (a.src ? a.duration : 0));
69
+ }
70
+ initWavesurfer(track, index) {
71
+ const container = this.rendering.containers[index];
72
+ // Create a wavesurfer instance
73
+ const ws = WaveSurfer.create({
74
+ ...track.options,
75
+ container,
76
+ minPxPerSec: 0,
77
+ media: this.audios[index],
78
+ peaks: track.peaks,
79
+ cursorColor: 'transparent',
80
+ cursorWidth: 0,
81
+ interact: false,
82
+ hideScrollbar: true,
83
+ });
84
+ // Regions and markers
85
+ const wsRegions = RegionsPlugin.create();
86
+ ws.registerPlugin(wsRegions);
87
+ this.subscriptions.push(ws.once('decode', () => {
88
+ // Start and end cues
89
+ if (track.startCue != null || track.endCue != null) {
90
+ const { startCue = 0, endCue = this.durations[index] } = track;
91
+ const startCueRegion = wsRegions.addRegion({
92
+ start: 0,
93
+ end: startCue,
94
+ color: 'rgba(0, 0, 0, 0.7)',
95
+ drag: false,
96
+ });
97
+ const endCueRegion = wsRegions.addRegion({
98
+ start: endCue,
99
+ end: endCue + this.durations[index],
100
+ color: 'rgba(0, 0, 0, 0.7)',
101
+ drag: false,
102
+ });
103
+ // Allow resizing only from one side
104
+ startCueRegion.element.firstElementChild?.remove();
105
+ endCueRegion.element.lastChild?.remove();
106
+ // Prevent clicks when dragging
107
+ // Update the start and end cues on resize
108
+ this.subscriptions.push(startCueRegion.on('update-end', () => {
109
+ track.startCue = startCueRegion.end;
110
+ this.emit('start-cue-change', { id: track.id, startCue: track.startCue });
111
+ }), endCueRegion.on('update-end', () => {
112
+ track.endCue = endCueRegion.start;
113
+ this.emit('end-cue-change', { id: track.id, endCue: track.endCue });
114
+ }));
115
+ }
116
+ // Intro
117
+ if (track.intro) {
118
+ const introRegion = wsRegions.addRegion({
119
+ start: 0,
120
+ end: track.intro.endTime,
121
+ content: track.intro.label,
122
+ color: this.options.trackBackground,
123
+ drag: false,
124
+ });
125
+ introRegion.element.querySelector('[data-resize="left"]')?.remove();
126
+ introRegion.element.parentElement.style.mixBlendMode = 'plus-lighter';
127
+ if (track.intro.color) {
128
+ ;
129
+ introRegion.element.querySelector('[data-resize="right"]').style.borderColor =
130
+ track.intro.color;
131
+ }
132
+ this.subscriptions.push(introRegion.on('update-end', () => {
133
+ this.emit('intro-end-change', { id: track.id, endTime: introRegion.end });
134
+ }));
135
+ }
136
+ // Render markers
137
+ if (track.markers) {
138
+ track.markers.forEach((marker) => {
139
+ wsRegions.addRegion({
140
+ start: marker.time,
141
+ content: marker.label,
142
+ color: marker.color,
143
+ resize: false,
144
+ });
145
+ });
146
+ }
147
+ }));
148
+ // Envelope
149
+ const envelope = ws.registerPlugin(EnvelopePlugin.create({
150
+ ...this.options.envelopeOptions,
151
+ fadeInStart: track.startCue,
152
+ fadeInEnd: track.fadeInEnd,
153
+ fadeOutStart: track.fadeOutStart,
154
+ fadeOutEnd: track.endCue,
155
+ volume: track.volume,
156
+ }));
157
+ this.subscriptions.push(envelope.on('volume-change', (volume) => {
158
+ this.setIsDragging();
159
+ this.emit('volume-change', { id: track.id, volume });
160
+ }), envelope.on('fade-in-change', (time) => {
161
+ this.setIsDragging();
162
+ this.emit('fade-in-change', { id: track.id, fadeInEnd: time });
163
+ }), envelope.on('fade-out-change', (time) => {
164
+ this.setIsDragging();
165
+ this.emit('fade-out-change', { id: track.id, fadeOutStart: time });
166
+ }), this.on('start-cue-change', ({ id, startCue }) => {
167
+ if (id === track.id) {
168
+ envelope.setStartTime(startCue);
169
+ }
170
+ }), this.on('end-cue-change', ({ id, endCue }) => {
171
+ if (id === track.id) {
172
+ envelope.setEndTime(endCue);
173
+ }
174
+ }));
175
+ return ws;
176
+ }
177
+ initAllWavesurfers() {
178
+ const wavesurfers = this.tracks.map((track, index) => {
179
+ return this.initWavesurfer(track, index);
180
+ });
181
+ this.wavesurfers = wavesurfers;
182
+ this.initTimeline();
183
+ }
184
+ initTimeline() {
185
+ if (this.timeline)
186
+ this.timeline.destroy();
187
+ this.timeline = this.wavesurfers[0].registerPlugin(TimelinePlugin.create({
188
+ duration: this.maxDuration,
189
+ container: this.rendering.containers[0].parentElement,
190
+ }));
191
+ }
192
+ updatePosition(time, autoCenter = false) {
193
+ const precisionSeconds = 0.3;
194
+ const isPaused = !this.isPlaying();
195
+ if (time !== this.currentTime) {
196
+ this.currentTime = time;
197
+ this.rendering.updateCursor(time / this.maxDuration, autoCenter);
198
+ }
199
+ // Update the current time of each audio
200
+ this.tracks.forEach((track, index) => {
201
+ const audio = this.audios[index];
202
+ const duration = this.durations[index];
203
+ const newTime = time - track.startPosition;
204
+ if (Math.abs(audio.currentTime - newTime) > precisionSeconds) {
205
+ audio.currentTime = newTime;
206
+ }
207
+ // If the position is out of the track bounds, pause it
208
+ if (isPaused || newTime < 0 || newTime > duration) {
209
+ !audio.paused && audio.pause();
210
+ }
211
+ else if (!isPaused) {
212
+ // If the position is in the track bounds, play it
213
+ audio.paused && audio.play();
214
+ }
215
+ // Unmute if cue is reached
216
+ const newVolume = newTime >= (track.startCue || 0) && newTime < (track.endCue || Infinity) ? 1 : 0;
217
+ if (newVolume !== audio.volume)
218
+ audio.volume = newVolume;
219
+ });
220
+ }
221
+ setIsDragging() {
222
+ // Prevent click events when dragging
223
+ this.isDragging = true;
224
+ if (this.timer)
225
+ clearTimeout(this.timer);
226
+ this.timer = setTimeout(() => (this.isDragging = false), 300);
227
+ }
228
+ onDrag(index, delta) {
229
+ this.setIsDragging();
230
+ const track = this.tracks[index];
231
+ if (!track.draggable)
232
+ return;
233
+ const newStartPosition = track.startPosition + delta * this.maxDuration;
234
+ const mainIndex = this.tracks.findIndex((item) => item.url && !item.draggable);
235
+ const mainTrack = this.tracks[mainIndex];
236
+ const minStart = (mainTrack ? mainTrack.startPosition : 0) - this.durations[index];
237
+ const maxStart = mainTrack ? mainTrack.startPosition + this.durations[mainIndex] : this.maxDuration;
238
+ if (newStartPosition >= minStart && newStartPosition <= maxStart) {
239
+ track.startPosition = newStartPosition;
240
+ this.initDurations(this.durations);
241
+ this.rendering.setContainerOffsets();
242
+ this.updatePosition(this.currentTime);
243
+ this.emit('start-position-change', { id: track.id, startPosition: newStartPosition });
244
+ }
245
+ }
246
+ findCurrentTracks() {
247
+ // Find the audios at the current time
248
+ const indexes = [];
249
+ this.tracks.forEach((track, index) => {
250
+ if (track.url &&
251
+ this.currentTime >= track.startPosition &&
252
+ this.currentTime < track.startPosition + this.durations[index]) {
253
+ indexes.push(index);
254
+ }
255
+ });
256
+ if (indexes.length === 0) {
257
+ const minStartTime = Math.min(...this.tracks.filter((t) => t.url).map((track) => track.startPosition));
258
+ indexes.push(this.tracks.findIndex((track) => track.startPosition === minStartTime));
259
+ }
260
+ return indexes;
261
+ }
262
+ startSync() {
263
+ const onFrame = () => {
264
+ const position = this.audios.reduce((pos, audio, index) => {
265
+ if (!audio.paused) {
266
+ pos = Math.max(pos, audio.currentTime + this.tracks[index].startPosition);
267
+ }
268
+ return pos;
269
+ }, this.currentTime);
270
+ if (position > this.currentTime) {
271
+ this.updatePosition(position, true);
272
+ }
273
+ this.frameRequest = requestAnimationFrame(onFrame);
274
+ };
275
+ onFrame();
276
+ }
277
+ play() {
278
+ this.startSync();
279
+ const indexes = this.findCurrentTracks();
280
+ indexes.forEach((index) => {
281
+ this.audios[index]?.play();
282
+ });
283
+ }
284
+ pause() {
285
+ this.audios.forEach((audio) => audio.pause());
286
+ }
287
+ isPlaying() {
288
+ return this.audios.some((audio) => !audio.paused);
289
+ }
290
+ getCurrentTime() {
291
+ return this.currentTime;
292
+ }
293
+ /** Position percentage from 0 to 1 */
294
+ seekTo(position) {
295
+ const wasPlaying = this.isPlaying();
296
+ this.updatePosition(position * this.maxDuration);
297
+ if (wasPlaying)
298
+ this.play();
299
+ }
300
+ /** Set time in seconds */
301
+ setTime(time) {
302
+ const wasPlaying = this.isPlaying();
303
+ this.updatePosition(time);
304
+ if (wasPlaying)
305
+ this.play();
306
+ }
307
+ zoom(pxPerSec) {
308
+ this.options.minPxPerSec = pxPerSec;
309
+ this.wavesurfers.forEach((ws, index) => this.tracks[index].url && ws.zoom(pxPerSec));
310
+ this.rendering.setMainWidth(this.durations, this.maxDuration);
311
+ this.rendering.setContainerOffsets();
312
+ }
313
+ addTrack(track) {
314
+ const index = this.tracks.findIndex((t) => t.id === track.id);
315
+ if (index !== -1) {
316
+ this.tracks[index] = track;
317
+ this.initAudio(track).then((audio) => {
318
+ this.audios[index] = audio;
319
+ this.durations[index] = audio.duration;
320
+ this.initDurations(this.durations);
321
+ const container = this.rendering.containers[index];
322
+ container.innerHTML = '';
323
+ this.wavesurfers[index].destroy();
324
+ this.wavesurfers[index] = this.initWavesurfer(track, index);
325
+ const drag = initDragging(container, (delta) => this.onDrag(index, delta), this.options.rightButtonDrag);
326
+ this.wavesurfers[index].once('destroy', () => drag?.destroy());
327
+ this.initTimeline();
328
+ this.emit('canplay');
329
+ });
330
+ }
331
+ }
332
+ destroy() {
333
+ if (this.frameRequest)
334
+ cancelAnimationFrame(this.frameRequest);
335
+ this.rendering.destroy();
336
+ this.audios.forEach((audio) => {
337
+ audio.pause();
338
+ audio.src = '';
339
+ });
340
+ this.wavesurfers.forEach((ws) => {
341
+ ws.destroy();
342
+ });
343
+ }
344
+ // See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId
345
+ setSinkId(sinkId) {
346
+ return Promise.all(this.wavesurfers.map((ws) => ws.setSinkId(sinkId)));
347
+ }
348
+ }
349
+ function initRendering(tracks, options) {
350
+ let pxPerSec = 0;
351
+ let durations = [];
352
+ let mainWidth = 0;
353
+ // Create a common container for all tracks
354
+ const scroll = document.createElement('div');
355
+ scroll.setAttribute('style', 'width: 100%; overflow-x: scroll; overflow-y: hidden; user-select: none;');
356
+ const wrapper = document.createElement('div');
357
+ wrapper.style.position = 'relative';
358
+ scroll.appendChild(wrapper);
359
+ options.container.appendChild(scroll);
360
+ // Create a common cursor
361
+ const cursor = document.createElement('div');
362
+ cursor.setAttribute('style', 'height: 100%; position: absolute; z-index: 10; top: 0; left: 0');
363
+ cursor.style.backgroundColor = options.cursorColor || '#000';
364
+ cursor.style.width = `${options.cursorWidth ?? 1}px`;
365
+ wrapper.appendChild(cursor);
366
+ const { clientWidth } = wrapper;
367
+ // Create containers for each track
368
+ const containers = tracks.map((track, index) => {
369
+ const container = document.createElement('div');
370
+ container.style.position = 'relative';
371
+ if (options.trackBorderColor && index > 0) {
372
+ const borderDiv = document.createElement('div');
373
+ borderDiv.setAttribute('style', `width: 100%; height: 2px; background-color: ${options.trackBorderColor}`);
374
+ wrapper.appendChild(borderDiv);
375
+ }
376
+ if (options.trackBackground && track.url) {
377
+ container.style.background = options.trackBackground;
378
+ }
379
+ // No audio on this track, so make it droppable
380
+ if (!track.url) {
381
+ const dropArea = document.createElement('div');
382
+ dropArea.setAttribute('style', `position: absolute; z-index: 10; left: 10px; top: 10px; right: 10px; bottom: 10px; border: 2px dashed ${options.trackBorderColor};`);
383
+ dropArea.addEventListener('dragover', (e) => {
384
+ e.preventDefault();
385
+ dropArea.style.background = options.trackBackground || '';
386
+ });
387
+ dropArea.addEventListener('dragleave', (e) => {
388
+ e.preventDefault();
389
+ dropArea.style.background = '';
390
+ });
391
+ dropArea.addEventListener('drop', (e) => {
392
+ e.preventDefault();
393
+ dropArea.style.background = '';
394
+ });
395
+ container.appendChild(dropArea);
396
+ }
397
+ wrapper.appendChild(container);
398
+ return container;
399
+ });
400
+ // Set the positions of each container
401
+ const setContainerOffsets = () => {
402
+ containers.forEach((container, i) => {
403
+ const offset = tracks[i].startPosition * pxPerSec;
404
+ if (durations[i]) {
405
+ container.style.width = `${durations[i] * pxPerSec}px`;
406
+ }
407
+ container.style.transform = `translateX(${offset}px)`;
408
+ });
409
+ };
410
+ return {
411
+ containers,
412
+ // Set the start offset
413
+ setContainerOffsets,
414
+ // Set the container width
415
+ setMainWidth: (trackDurations, maxDuration) => {
416
+ durations = trackDurations;
417
+ pxPerSec = Math.max(options.minPxPerSec || 0, clientWidth / maxDuration);
418
+ mainWidth = pxPerSec * maxDuration;
419
+ wrapper.style.width = `${mainWidth}px`;
420
+ setContainerOffsets();
421
+ },
422
+ // Update cursor position
423
+ updateCursor: (position, autoCenter) => {
424
+ cursor.style.left = `${Math.min(100, position * 100)}%`;
425
+ // Update scroll
426
+ const { clientWidth, scrollLeft } = scroll;
427
+ const center = clientWidth / 2;
428
+ const minScroll = autoCenter ? center : clientWidth;
429
+ const pos = position * mainWidth;
430
+ if (pos > scrollLeft + minScroll || pos < scrollLeft) {
431
+ scroll.scrollLeft = pos - center;
432
+ }
433
+ },
434
+ // Click to seek
435
+ addClickHandler: (onClick) => {
436
+ wrapper.addEventListener('click', (e) => {
437
+ const rect = wrapper.getBoundingClientRect();
438
+ const x = e.clientX - rect.left;
439
+ const position = x / wrapper.offsetWidth;
440
+ onClick(position);
441
+ });
442
+ },
443
+ // Destroy the container
444
+ destroy: () => {
445
+ scroll.remove();
446
+ },
447
+ // Do something on drop
448
+ addDropHandler: (onDrop) => {
449
+ tracks.forEach((track, index) => {
450
+ if (!track.url) {
451
+ const droppable = containers[index].querySelector('div');
452
+ droppable?.addEventListener('drop', (e) => {
453
+ e.preventDefault();
454
+ onDrop(track.id);
455
+ });
456
+ }
457
+ });
458
+ },
459
+ };
460
+ }
461
+ function initDragging(container, onDrag, rightButtonDrag = false) {
462
+ const wrapper = container.parentElement;
463
+ if (!wrapper)
464
+ return;
465
+ // Dragging tracks to set position
466
+ let dragStart = null;
467
+ container.addEventListener('contextmenu', (e) => {
468
+ rightButtonDrag && e.preventDefault();
469
+ });
470
+ // Drag start
471
+ container.addEventListener('mousedown', (e) => {
472
+ if (rightButtonDrag && e.button !== 2)
473
+ return;
474
+ const rect = wrapper.getBoundingClientRect();
475
+ dragStart = e.clientX - rect.left;
476
+ container.style.cursor = 'grabbing';
477
+ });
478
+ // Drag end
479
+ const onMouseUp = (e) => {
480
+ if (dragStart != null) {
481
+ e.stopPropagation();
482
+ dragStart = null;
483
+ container.style.cursor = '';
484
+ }
485
+ };
486
+ // Drag move
487
+ const onMouseMove = (e) => {
488
+ if (dragStart == null)
489
+ return;
490
+ const rect = wrapper.getBoundingClientRect();
491
+ const x = e.clientX - rect.left;
492
+ const diff = x - dragStart;
493
+ if (diff > 1 || diff < -1) {
494
+ dragStart = x;
495
+ onDrag(diff / wrapper.offsetWidth);
496
+ }
497
+ };
498
+ document.body.addEventListener('mouseup', onMouseUp);
499
+ document.body.addEventListener('mousemove', onMouseMove);
500
+ return {
501
+ destroy: () => {
502
+ document.body.removeEventListener('mouseup', onMouseUp);
503
+ document.body.removeEventListener('mousemove', onMouseMove);
504
+ },
505
+ };
506
+ }
507
+ export default MultiTrack;