wavesurfer.js 7.0.0-alpha.9 → 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 (50) 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 +85 -12
  19. package/dist/plugins/multitrack.js +331 -84
  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 -192
  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 +25 -9
  31. package/dist/plugins/timeline.js +65 -24
  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/plugins/xmultitrack.d.ts +0 -44
  45. package/dist/plugins/xmultitrack.js +0 -260
  46. package/dist/react/useWavesurfer.d.ts +0 -5
  47. package/dist/react/useWavesurfer.js +0 -20
  48. package/dist/wavesurfer.Multitrack.min.js +0 -1
  49. package/dist/wavesurfer.Regions.min.js +0 -1
  50. package/dist/wavesurfer.Timeline.min.js +0 -1
@@ -1,24 +1,80 @@
1
- import { type WaveSurferOptions } from '../index.js';
2
- type MultitrackTracks = Array<{
3
- id: string | number;
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;
4
11
  url?: string;
5
12
  peaks?: WaveSurferOptions['peaks'];
6
13
  draggable?: boolean;
7
14
  startPosition: number;
8
15
  startCue?: number;
9
16
  endCue?: number;
17
+ fadeInEnd?: number;
18
+ fadeOutStart?: number;
19
+ volume?: number;
10
20
  markers?: Array<{
11
- id: string | number;
12
21
  time: number;
13
22
  label?: string;
14
23
  color?: string;
15
24
  }>;
16
- }>;
17
- type MultitrackOptions = {
25
+ intro?: {
26
+ endTime: number;
27
+ label?: string;
28
+ color?: string;
29
+ };
30
+ options?: WaveSurferOptions;
31
+ };
32
+ export type MultitrackOptions = {
18
33
  container: HTMLElement;
19
- minPxPerSec: number;
20
- } & WaveSurferOptions;
21
- declare class MultiTrack {
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> {
22
78
  private tracks;
23
79
  private options;
24
80
  private audios;
@@ -28,17 +84,34 @@ declare class MultiTrack {
28
84
  private maxDuration;
29
85
  private rendering;
30
86
  private isDragging;
87
+ private frameRequest;
88
+ private timer;
89
+ private subscriptions;
90
+ private timeline;
31
91
  static create(tracks: MultitrackTracks, options: MultitrackOptions): MultiTrack;
32
92
  constructor(tracks: MultitrackTracks, options: MultitrackOptions);
33
- private initAudios;
34
- private initWavesurfers;
93
+ private initDurations;
94
+ private initAudio;
95
+ private initAllAudios;
96
+ private initWavesurfer;
97
+ private initAllWavesurfers;
98
+ private initTimeline;
35
99
  private updatePosition;
36
- private onSeek;
100
+ private setIsDragging;
37
101
  private onDrag;
38
102
  private findCurrentTracks;
103
+ private startSync;
39
104
  play(): void;
40
105
  pause(): void;
41
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;
42
114
  destroy(): void;
115
+ setSinkId(sinkId: string): Promise<void[]>;
43
116
  }
44
117
  export default MultiTrack;
@@ -1,86 +1,201 @@
1
- import WaveSurfer from '../index.js';
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';
2
6
  import RegionsPlugin from './regions.js';
3
- class MultiTrack {
7
+ import TimelinePlugin from './timeline.js';
8
+ import EnvelopePlugin from './envelope.js';
9
+ import EventEmitter from '../event-emitter.js';
10
+ class MultiTrack extends EventEmitter {
4
11
  static create(tracks, options) {
5
12
  return new MultiTrack(tracks, options);
6
13
  }
7
14
  constructor(tracks, options) {
15
+ super();
8
16
  this.audios = [];
9
17
  this.wavesurfers = [];
10
18
  this.durations = [];
11
19
  this.currentTime = 0;
12
20
  this.maxDuration = 0;
13
21
  this.isDragging = false;
14
- this.tracks = tracks.map((track) => (Object.assign(Object.assign({}, track), { startPosition: track.startPosition || 0 })));
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
+ }));
15
31
  this.options = options;
16
32
  this.rendering = initRendering(this.tracks, this.options);
17
- this.initAudios().then((durations) => {
18
- this.durations = durations;
19
- const maxDuration = this.tracks.reduce((max, track, index) => {
20
- return Math.max(max, track.startPosition + durations[index]);
21
- }, 0);
22
- this.rendering.setMainWidth(maxDuration * this.options.minPxPerSec);
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
+ });
23
43
  this.rendering.addClickHandler((position) => {
24
- this.onSeek(position * maxDuration);
44
+ if (this.isDragging)
45
+ return;
46
+ this.seekTo(position);
25
47
  });
26
- this.maxDuration = maxDuration;
48
+ this.emit('canplay');
27
49
  });
28
- this.initWavesurfers();
29
- this.rendering.containers.forEach((container, index) => {
30
- const drag = initDragging(container, (delta) => this.onDrag(index, delta));
31
- this.wavesurfers[index].once('destroy', () => {
32
- drag === null || drag === void 0 ? void 0 : drag.destroy();
33
- });
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 });
34
64
  });
35
65
  }
36
- initAudios() {
37
- const emptyTrackUrl = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA';
38
- this.audios = this.tracks.map((track) => new Audio(track.url || emptyTrackUrl));
39
- return Promise.all(this.audios.map((audio) => {
40
- return new Promise((resolve) => {
41
- audio.addEventListener('canplay', () => {
42
- resolve(audio.duration);
43
- }, { once: true });
44
- });
45
- }));
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));
46
69
  }
47
- initWavesurfers() {
48
- const wavesurfers = this.tracks.map((track, index) => {
49
- // Create a wavesurfer instance
50
- const ws = WaveSurfer.create(Object.assign(Object.assign({}, this.options), { container: this.rendering.containers[index], media: this.audios[index], peaks: track.peaks, cursorColor: 'transparent', fillParent: false, interactive: false }));
51
- ws.on('timeupdate', ({ currentTime }) => {
52
- if (ws.isPlaying() && currentTime + track.startPosition > this.currentTime) {
53
- this.updatePosition(track.startPosition + currentTime);
54
- }
55
- });
56
- const wsRegions = ws.registerPlugin(RegionsPlugin, {
57
- draggable: false,
58
- resizable: false,
59
- dragSelection: false,
60
- });
61
- ws.once('decode', () => {
62
- // Render start and end cues
63
- if (track.startCue) {
64
- wsRegions.add(track.startCue, track.startCue, 'start');
65
- }
66
- if (track.endCue) {
67
- wsRegions.add(track.endCue, track.endCue, 'end');
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;
68
131
  }
69
- // Render markers
70
- ;
71
- (track.markers || []).forEach((marker) => {
72
- wsRegions.add(marker.time, marker.time, marker.label, marker.color);
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
+ });
73
145
  });
74
- });
75
- return ws;
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);
76
180
  });
77
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
+ }));
78
191
  }
79
- updatePosition(time) {
192
+ updatePosition(time, autoCenter = false) {
80
193
  const precisionSeconds = 0.3;
81
- this.currentTime = time;
82
- this.rendering.updateCursor(time / this.maxDuration);
83
194
  const isPaused = !this.isPlaying();
195
+ if (time !== this.currentTime) {
196
+ this.currentTime = time;
197
+ this.rendering.updateCursor(time / this.maxDuration, autoCenter);
198
+ }
84
199
  // Update the current time of each audio
85
200
  this.tracks.forEach((track, index) => {
86
201
  const audio = this.audios[index];
@@ -103,23 +218,30 @@ class MultiTrack {
103
218
  audio.volume = newVolume;
104
219
  });
105
220
  }
106
- onSeek(time) {
107
- if (this.isDragging)
108
- return;
109
- const wasPlaying = this.isPlaying();
110
- this.updatePosition(time);
111
- if (wasPlaying)
112
- this.play();
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);
113
227
  }
114
228
  onDrag(index, delta) {
229
+ this.setIsDragging();
115
230
  const track = this.tracks[index];
116
231
  if (!track.draggable)
117
232
  return;
118
- this.isDragging = true;
119
- setTimeout(() => (this.isDragging = false), 300);
120
- const newTime = track.startPosition + delta * this.maxDuration;
121
- track.startPosition = newTime;
122
- this.rendering.setContainerOffsets();
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
+ }
123
245
  }
124
246
  findCurrentTracks() {
125
247
  // Find the audios at the current time
@@ -137,11 +259,26 @@ class MultiTrack {
137
259
  }
138
260
  return indexes;
139
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
+ }
140
277
  play() {
278
+ this.startSync();
141
279
  const indexes = this.findCurrentTracks();
142
280
  indexes.forEach((index) => {
143
- var _a;
144
- (_a = this.audios[index]) === null || _a === void 0 ? void 0 : _a.play();
281
+ this.audios[index]?.play();
145
282
  });
146
283
  }
147
284
  pause() {
@@ -150,7 +287,51 @@ class MultiTrack {
150
287
  isPlaying() {
151
288
  return this.audios.some((audio) => !audio.paused);
152
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
+ }
153
332
  destroy() {
333
+ if (this.frameRequest)
334
+ cancelAnimationFrame(this.frameRequest);
154
335
  this.rendering.destroy();
155
336
  this.audios.forEach((audio) => {
156
337
  audio.pause();
@@ -160,10 +341,17 @@ class MultiTrack {
160
341
  ws.destroy();
161
342
  });
162
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
+ }
163
348
  }
164
349
  function initRendering(tracks, options) {
350
+ let pxPerSec = 0;
351
+ let durations = [];
352
+ let mainWidth = 0;
165
353
  // Create a common container for all tracks
166
- let scroll = document.createElement('div');
354
+ const scroll = document.createElement('div');
167
355
  scroll.setAttribute('style', 'width: 100%; overflow-x: scroll; overflow-y: hidden; user-select: none;');
168
356
  const wrapper = document.createElement('div');
169
357
  wrapper.style.position = 'relative';
@@ -171,37 +359,77 @@ function initRendering(tracks, options) {
171
359
  options.container.appendChild(scroll);
172
360
  // Create a common cursor
173
361
  const cursor = document.createElement('div');
174
- cursor.setAttribute('style', 'width: 1px; height: 100%; position: absolute; z-index: 10; top: 0; left: 0');
175
- cursor.style.backgroundColor = options.cursorColor || options.progressColor || '#000';
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`;
176
365
  wrapper.appendChild(cursor);
366
+ const { clientWidth } = wrapper;
177
367
  // Create containers for each track
178
- const containers = tracks.map(() => {
368
+ const containers = tracks.map((track, index) => {
179
369
  const container = document.createElement('div');
180
- container.style.width = 'fit-content';
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
+ }
181
397
  wrapper.appendChild(container);
182
398
  return container;
183
399
  });
184
400
  // Set the positions of each container
185
401
  const setContainerOffsets = () => {
186
402
  containers.forEach((container, i) => {
187
- const offset = (tracks[i].startPosition * options.minPxPerSec) / devicePixelRatio;
403
+ const offset = tracks[i].startPosition * pxPerSec;
404
+ if (durations[i]) {
405
+ container.style.width = `${durations[i] * pxPerSec}px`;
406
+ }
188
407
  container.style.transform = `translateX(${offset}px)`;
189
- if (tracks[i].draggable)
190
- container.style.cursor = 'ew-resize';
191
408
  });
192
409
  };
193
- setContainerOffsets();
194
410
  return {
195
411
  containers,
196
412
  // Set the start offset
197
413
  setContainerOffsets,
198
414
  // Set the container width
199
- setMainWidth: (width) => {
200
- wrapper.style.width = `${width / devicePixelRatio}px`;
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();
201
421
  },
202
422
  // Update cursor position
203
- updateCursor: (position) => {
423
+ updateCursor: (position, autoCenter) => {
204
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
+ }
205
433
  },
206
434
  // Click to seek
207
435
  addClickHandler: (onClick) => {
@@ -216,24 +444,43 @@ function initRendering(tracks, options) {
216
444
  destroy: () => {
217
445
  scroll.remove();
218
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
+ },
219
459
  };
220
460
  }
221
- function initDragging(container, onDrag) {
461
+ function initDragging(container, onDrag, rightButtonDrag = false) {
222
462
  const wrapper = container.parentElement;
223
463
  if (!wrapper)
224
464
  return;
225
465
  // Dragging tracks to set position
226
466
  let dragStart = null;
467
+ container.addEventListener('contextmenu', (e) => {
468
+ rightButtonDrag && e.preventDefault();
469
+ });
227
470
  // Drag start
228
471
  container.addEventListener('mousedown', (e) => {
472
+ if (rightButtonDrag && e.button !== 2)
473
+ return;
229
474
  const rect = wrapper.getBoundingClientRect();
230
475
  dragStart = e.clientX - rect.left;
476
+ container.style.cursor = 'grabbing';
231
477
  });
232
478
  // Drag end
233
479
  const onMouseUp = (e) => {
234
480
  if (dragStart != null) {
235
481
  e.stopPropagation();
236
482
  dragStart = null;
483
+ container.style.cursor = '';
237
484
  }
238
485
  };
239
486
  // Drag move