@checksub_team/peaks_timeline 2.2.1 → 2.3.0-alpha.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.
@@ -10,12 +10,14 @@ define([
10
10
  './waveform-builder',
11
11
  './waveform-shape',
12
12
  './loader',
13
+ './invoker',
13
14
  '../utils',
14
15
  'konva'
15
16
  ], function(
16
17
  WaveformBuilder,
17
18
  WaveformShape,
18
19
  Loader,
20
+ Invoker,
19
21
  Utils,
20
22
  Konva) {
21
23
  'use strict';
@@ -24,7 +26,10 @@ define([
24
26
  var SPACING_BETWEEN_PREVIEWS = 1.5;
25
27
  var CORNER_RADIUS = 8;
26
28
  var INDICATOR_RADIUS = 4; // px
27
- var PREVIEW_CREATE_CHUNK = 8; // number of preview tiles to add per idle slice
29
+ var PREVIEW_CREATE_CHUNK = 12; // number of preview tiles to add per idle slice (increased for better throughput)
30
+
31
+ // Shared invoker for all source groups to coordinate updates
32
+ var sharedInvoker = new Invoker();
28
33
 
29
34
  /**
30
35
  * Creates a source group for the given source.
@@ -47,7 +52,6 @@ define([
47
52
 
48
53
  var self = this;
49
54
 
50
- this._x = this._view.timeToPixels(source.startTime);
51
55
  this._width = this._view.timeToPixels(source.endTime - source.startTime);
52
56
  var heights = SourceGroup.getHeights(source, peaks);
53
57
 
@@ -59,16 +63,25 @@ define([
59
63
  this._selected = this._source.selected;
60
64
  this._hovered = false;
61
65
  this._isDragged = false;
66
+ this._isHandleDragging = false;
62
67
  this._destroyed = false;
63
68
 
69
+ // Optional Konva element used as a drag "ghost" preview.
70
+ this._dragGhost = null;
71
+
72
+ // Performance: track if draw is needed
73
+ this._drawScheduled = false;
74
+
64
75
  this._previewList = [];
65
- // internal queue state for async preview creation
66
- this._previewBuildQueue = new Set();
76
+ // internal queue state for async preview creation
77
+ this._previewBuildQueue = new Set();
78
+ // Track pending idle callbacks for cleanup
79
+ this._pendingIdleCallbacks = new Set();
67
80
 
68
81
  this._markersGroup = this._createMarkers();
69
82
 
70
83
  this._group = new Konva.Group({
71
- x: this._x,
84
+ x: this._view.timeToPixels(source.startTime),
72
85
  sourceId: this._source.id,
73
86
  draggable: this._source.draggable,
74
87
  dragBoundFunc: function() {
@@ -135,15 +148,58 @@ define([
135
148
  if (!this._source.loading) {
136
149
  this._showButtons();
137
150
  }
138
- this._batchDraw();
151
+ this._scheduleBatchDraw();
139
152
  };
140
153
 
141
154
  SourceGroup.prototype._onHoverEnd = function() {
142
155
  this._hovered = false;
143
156
  this._manualHover = false;
157
+ this._disableManualHoverTracking();
144
158
  this._view.setHoveredElement(null);
145
159
  this._hideButtons();
146
- this._batchDraw();
160
+ this._scheduleBatchDraw();
161
+ };
162
+
163
+ SourceGroup.prototype._enableManualHoverTracking = function() {
164
+ if (this._manualHoverTrackingEnabled) {
165
+ return;
166
+ }
167
+
168
+ if (!this._group || this._destroyed) {
169
+ return;
170
+ }
171
+
172
+ var stage = this._group.getStage && this._group.getStage();
173
+
174
+ if (!stage) {
175
+ return;
176
+ }
177
+
178
+ this._manualHoverTrackingEnabled = true;
179
+ this._manualHoverNamespace = '.manualHover.' + this._source.id;
180
+
181
+ this._manualHoverMoveHandler = function() {
182
+ this._manageManualHoverStop();
183
+ }.bind(this);
184
+
185
+ stage.on('mousemove' + this._manualHoverNamespace, this._manualHoverMoveHandler);
186
+ stage.on('touchmove' + this._manualHoverNamespace, this._manualHoverMoveHandler);
187
+ };
188
+
189
+ SourceGroup.prototype._disableManualHoverTracking = function() {
190
+ if (!this._manualHoverTrackingEnabled) {
191
+ return;
192
+ }
193
+
194
+ var stage = this._group && this._group.getStage && this._group.getStage();
195
+
196
+ if (stage && this._manualHoverNamespace) {
197
+ stage.off(this._manualHoverNamespace);
198
+ }
199
+
200
+ this._manualHoverTrackingEnabled = false;
201
+ this._manualHoverMoveHandler = null;
202
+ this._manualHoverNamespace = null;
147
203
  };
148
204
 
149
205
  SourceGroup.prototype._onDragStart = function(element) {
@@ -182,7 +238,7 @@ define([
182
238
  };
183
239
 
184
240
  SourceGroup.prototype.isActive = function() {
185
- return this._isDragged;
241
+ return this._isDragged || this._isHandleDragging;
186
242
  };
187
243
 
188
244
  SourceGroup.prototype.addToContent = function(newChild) {
@@ -202,14 +258,17 @@ define([
202
258
  this._rightHandle.x(this._width - handleWidth);
203
259
  };
204
260
 
205
- SourceGroup.prototype._onSourceGroupHandleDrag = function(draggedElement, dragPos, leftHandle) {
206
- const diff = this._view.pixelsToTime(dragPos.x - this._mouseDownX);
207
- const timeOffsetDiff = this._view.getTimeOffset() - this._initialTimeOffset;
208
-
261
+ SourceGroup.prototype._onSourceGroupHandleDrag = function(draggedElement, leftHandle) {
209
262
  const { start, end } = this._initialTimes;
210
263
 
211
264
  this._view.updateWithAutoScroll(
212
265
  function() {
266
+ var pointer = this._view.getPointerPosition();
267
+ var pointerX = pointer ? pointer.x : this._mouseDownX;
268
+
269
+ const diff = this._view.pixelsToTime(pointerX - this._mouseDownX);
270
+ const timeOffsetDiff = this._view.getTimeOffset() - this._initialTimeOffset;
271
+
213
272
  if (this._layer.manageSourceMovements(
214
273
  [this._source],
215
274
  leftHandle ? start + diff + timeOffsetDiff : null,
@@ -218,7 +277,7 @@ define([
218
277
  this._layer.batchDraw();
219
278
  }
220
279
  }.bind(this)
221
- );
280
+ , null, false);
222
281
 
223
282
  return {
224
283
  x: draggedElement.absolutePosition().x,
@@ -232,9 +291,13 @@ define([
232
291
  const frameOffset = this._view.timeToPixels(this._view.getTimeOffset());
233
292
  const newTimeToPixelsScale = this._view.getTimeToPixelsScale();
234
293
 
235
- this._group.x(startPixel - frameOffset);
236
-
237
- this._x = startPixel;
294
+ // When sources are being moved (dragged as blocks), their Konva positions are
295
+ // controlled by the drag handler so they follow the cursor.
296
+ // Auto-scroll triggers frequent `updateSources()` calls; avoid overwriting
297
+ // the drag position here to prevent jitter.
298
+ if (!this._isDragged) {
299
+ this._group.x(startPixel - frameOffset);
300
+ }
238
301
 
239
302
  const newWidth = endPixel - startPixel;
240
303
 
@@ -268,6 +331,13 @@ define([
268
331
  // update unwrap
269
332
  this.updatePreviews();
270
333
  }
334
+
335
+ // Keep the drag ghost in sync automatically while dragging.
336
+ // This lets SourcesLayer avoid manually updating ghosts.
337
+ if (this._isDragged) {
338
+ this.createDragGhost();
339
+ this.updateDragGhost();
340
+ }
271
341
  };
272
342
 
273
343
  SourceGroup.prototype.setWrapping = function(wrap, forceCreate, notify) {
@@ -307,14 +377,20 @@ define([
307
377
  start: this._source.startTime,
308
378
  end: this._source.endTime
309
379
  };
310
- this._isDragged = true;
380
+ this._isHandleDragging = true;
311
381
 
312
382
  this._hideButtons();
313
383
  };
314
384
 
315
385
  SourceGroup.prototype._onHandleDragEnd = function() {
316
- this._isDragged = false;
386
+ this._isHandleDragging = false;
317
387
  this._showButtons();
388
+
389
+ // When resizing via handles, drag events no longer bubble to the parent
390
+ // group, so we won't hit the move-drag end path that calls prepareDragEnd.
391
+ // Normalize handle geometry here so handles snap to the updated width.
392
+ this.update();
393
+ this.prepareDragEnd();
318
394
  };
319
395
 
320
396
  SourceGroup.prototype._addHandles = function(forceCreate) {
@@ -328,14 +404,23 @@ define([
328
404
  height: this._unwrappedHeight,
329
405
  visible: true,
330
406
  draggable: this._source.resizable,
331
- dragBoundFunc: function(pos) {
332
- return self._onSourceGroupHandleDrag(this, pos, true);
407
+ dragBoundFunc: function() {
408
+ return self._onSourceGroupHandleDrag(this, true);
333
409
  }
334
410
  });
335
411
 
336
- this._leftHandle.on('dragstart', this._onHandleDragStart.bind(this));
412
+ // Prevent handle drag events from bubbling to the parent group.
413
+ // Otherwise the parent SourceGroup dragstart/dragend handlers run and
414
+ // the sources-layer creates move-drag ghosts during resize.
415
+ this._leftHandle.on('dragstart', function(event) {
416
+ event.cancelBubble = true;
417
+ self._onHandleDragStart(event);
418
+ });
337
419
 
338
- this._leftHandle.on('dragend', this._onHandleDragEnd.bind(this));
420
+ this._leftHandle.on('dragend', function(event) {
421
+ event.cancelBubble = true;
422
+ self._onHandleDragEnd(event);
423
+ });
339
424
 
340
425
  if (this._source.resizable) {
341
426
  this._leftHandle.on('mouseover', function() {
@@ -355,14 +440,23 @@ define([
355
440
  height: this._unwrappedHeight,
356
441
  visible: true,
357
442
  draggable: this._source.resizable,
358
- dragBoundFunc: function(pos) {
359
- return self._onSourceGroupHandleDrag(this, pos, false);
443
+ dragBoundFunc: function() {
444
+ return self._onSourceGroupHandleDrag(this, false);
360
445
  }
361
446
  });
362
447
 
363
- this._rightHandle.on('dragstart', this._onHandleDragStart.bind(this));
448
+ // Prevent handle drag events from bubbling to the parent group.
449
+ // Otherwise the parent SourceGroup dragstart/dragend handlers run and
450
+ // the sources-layer creates move-drag ghosts during resize.
451
+ this._rightHandle.on('dragstart', function(event) {
452
+ event.cancelBubble = true;
453
+ self._onHandleDragStart(event);
454
+ });
364
455
 
365
- this._rightHandle.on('dragend', this._onHandleDragEnd.bind(this));
456
+ this._rightHandle.on('dragend', function(event) {
457
+ event.cancelBubble = true;
458
+ self._onHandleDragEnd(event);
459
+ });
366
460
 
367
461
  if (this._source.resizable) {
368
462
  this._rightHandle.on('mouseover', function() {
@@ -425,17 +519,20 @@ define([
425
519
  )
426
520
  )
427
521
  );
522
+
523
+ var actualX = this._group.x() + this._view.getFrameOffset();
428
524
  var x = Math.max(
429
525
  0,
430
- this._view.getFrameOffset() - this._x - 2 * radius
526
+ this._view.getFrameOffset() - actualX - 2 * radius
431
527
  );
432
528
  var width = Math.min(
433
529
  this._width - x,
434
530
  this._view.getWidth() + 4 * radius - Math.max(
435
531
  0,
436
- this._x - this._view.getFrameOffset()
532
+ actualX - this._view.getFrameOffset()
437
533
  )
438
534
  );
535
+
439
536
  var xWidth = x + width;
440
537
 
441
538
  if (width > 0) {
@@ -678,10 +775,6 @@ define([
678
775
  return this._width;
679
776
  };
680
777
 
681
- SourceGroup.prototype.getX = function() {
682
- return this._x;
683
- };
684
-
685
778
  SourceGroup.prototype.getAbsoluteY = function() {
686
779
  return this._group.absolutePosition().y;
687
780
  };
@@ -700,12 +793,20 @@ define([
700
793
  return this._group.y(value);
701
794
  };
702
795
 
796
+ SourceGroup.prototype.absolutePosition = function(value) {
797
+ if (value) {
798
+ return this._group.absolutePosition(value);
799
+ }
800
+ return this._group.absolutePosition();
801
+ };
802
+
703
803
  SourceGroup.prototype.getSource = function() {
704
804
  return this._source;
705
805
  };
706
806
 
707
807
  SourceGroup.prototype.startHover = function() {
708
808
  this._manualHover = true;
809
+ this._enableManualHoverTracking();
709
810
  this._group.fire('mouseenter', { evt: new MouseEvent('mouseenter') }, true);
710
811
  };
711
812
 
@@ -713,6 +814,18 @@ define([
713
814
  this._group.fire('mouseleave', { evt: new MouseEvent('mouseleave') }, true);
714
815
  };
715
816
 
817
+ SourceGroup.prototype.setDragging = function(isDragging) {
818
+ this._isDragged = isDragging;
819
+
820
+ // Ghost lifecycle is tied to dragging state.
821
+ if (isDragging) {
822
+ this.createDragGhost();
823
+ }
824
+ else {
825
+ this.destroyDragGhost();
826
+ }
827
+ };
828
+
716
829
  SourceGroup.prototype.startDrag = function() {
717
830
  return this._group.startDrag();
718
831
  };
@@ -725,12 +838,12 @@ define([
725
838
  this._group.moveTo(group);
726
839
  };
727
840
 
728
- SourceGroup.prototype.isDescendantOf = function(group) {
729
- return group.isAncestorOf(this._group);
841
+ SourceGroup.prototype.moveToTop = function() {
842
+ this._group.moveToTop();
730
843
  };
731
844
 
732
- SourceGroup.prototype.hideButKeepFocus = function() {
733
- this._group.moveTo(this._view.getTempGroup());
845
+ SourceGroup.prototype.isDescendantOf = function(group) {
846
+ return group.isAncestorOf(this._group);
734
847
  };
735
848
 
736
849
  SourceGroup.prototype.getParent = function() {
@@ -878,19 +991,38 @@ define([
878
991
  };
879
992
 
880
993
  SourceGroup.prototype._createAudioPreview = function(preview, redraw) {
994
+ var url = preview.url;
995
+
996
+ // Resample the waveform data for the current zoom level if needed
997
+ var scaledData = this._layer.getLoadedData(url + '-scaled');
998
+ var currentScale = this._view.getTimeToPixelsScale();
999
+
1000
+ if (scaledData && scaledData.scale !== currentScale) {
1001
+ var originalData = this._layer.getLoadedData(url);
1002
+
1003
+ if (originalData) {
1004
+ this._layer.setLoadedData(url + '-scaled', {
1005
+ data: originalData.resample({
1006
+ scale: originalData.sample_rate / currentScale
1007
+ }),
1008
+ scale: currentScale
1009
+ });
1010
+ }
1011
+ }
1012
+
881
1013
  var waveform = new WaveformShape({
882
1014
  layer: this._layer,
883
1015
  view: this._view,
884
1016
  source: this._source,
885
1017
  height: preview.group.height(),
886
- url: preview.url
1018
+ url: url
887
1019
  });
888
1020
 
889
1021
  preview.group.add(waveform);
890
1022
  this._addToUnwrap(preview.group);
891
1023
 
892
1024
  if (redraw) {
893
- this._layer.rescale(true);
1025
+ this._scheduleBatchDraw();
894
1026
  }
895
1027
 
896
1028
  this._previewList.push(preview);
@@ -991,6 +1123,26 @@ define([
991
1123
  });
992
1124
  };
993
1125
 
1126
+ /**
1127
+ * Schedules a batch draw using RAF to coalesce multiple draw requests.
1128
+ * This is more efficient than calling _batchDraw directly.
1129
+ */
1130
+ SourceGroup.prototype._scheduleBatchDraw = function() {
1131
+ if (this._destroyed || this._drawScheduled) {
1132
+ return;
1133
+ }
1134
+
1135
+ this._drawScheduled = true;
1136
+ var self = this;
1137
+
1138
+ sharedInvoker.scheduleFrame(function() {
1139
+ self._drawScheduled = false;
1140
+ if (!self._destroyed) {
1141
+ self._batchDraw();
1142
+ }
1143
+ });
1144
+ };
1145
+
994
1146
  SourceGroup.prototype._batchDraw = function() {
995
1147
  var layer = this._group && this._group.getLayer && this._group.getLayer();
996
1148
 
@@ -999,31 +1151,16 @@ define([
999
1151
  }
1000
1152
  };
1001
1153
 
1002
- // Utility to schedule work during idle time or next frame
1154
+ // Utility to schedule work during idle time, with tracking for cleanup
1003
1155
  SourceGroup.prototype._scheduleIdle = function(fn) {
1004
- if (typeof window !== 'undefined' && window.requestIdleCallback) {
1005
- return window.requestIdleCallback(fn, { timeout: 50 });
1006
- }
1007
-
1008
- if (typeof window !== 'undefined' && window.requestAnimationFrame) {
1009
- return window.requestAnimationFrame(function() {
1010
- fn({
1011
- timeRemaining: function() {
1012
- return 0;
1013
- },
1014
- didTimeout: true
1015
- });
1016
- });
1017
- }
1156
+ var self = this;
1157
+ var id = Utils.scheduleIdle(function(deadline) {
1158
+ self._pendingIdleCallbacks.delete(id);
1159
+ fn(deadline);
1160
+ }, { timeout: 50 });
1018
1161
 
1019
- return setTimeout(function() {
1020
- fn({
1021
- timeRemaining: function() {
1022
- return 0;
1023
- },
1024
- didTimeout: true
1025
- });
1026
- }, 0);
1162
+ this._pendingIdleCallbacks.add(id);
1163
+ return id;
1027
1164
  };
1028
1165
 
1029
1166
  SourceGroup.prototype._ensureImagePreviewCount = function(preview, targetCount, interImageSpacing) {
@@ -1038,7 +1175,7 @@ define([
1038
1175
  }
1039
1176
 
1040
1177
  if (currentCount >= targetCount || this._previewBuildQueue.has(preview)) {
1041
- this._batchDraw();
1178
+ this._scheduleBatchDraw();
1042
1179
  return;
1043
1180
  }
1044
1181
 
@@ -1066,7 +1203,7 @@ define([
1066
1203
  added += 1;
1067
1204
  }
1068
1205
 
1069
- self._batchDraw();
1206
+ self._scheduleBatchDraw();
1070
1207
 
1071
1208
  if (nextIndex < targetCount) {
1072
1209
  self._scheduleIdle(buildChunk);
@@ -1111,7 +1248,7 @@ define([
1111
1248
  this._ensureImagePreviewCount(preview, targetCount, interImageSpacing);
1112
1249
 
1113
1250
  if (redraw) {
1114
- this._batchDraw();
1251
+ this._scheduleBatchDraw();
1115
1252
  }
1116
1253
 
1117
1254
  this._previewList.push(preview);
@@ -1196,6 +1333,90 @@ define([
1196
1333
  return this._height;
1197
1334
  };
1198
1335
 
1336
+ /**
1337
+ * Creates the drag ghost preview element if it does not exist.
1338
+ * The ghost shows where the source will be placed when released.
1339
+ */
1340
+ SourceGroup.prototype.createDragGhost = function() {
1341
+ if (this._dragGhost) {
1342
+ return this._dragGhost;
1343
+ }
1344
+
1345
+ var frameOffset = this._view.getFrameOffset();
1346
+ var x = this._view.timeToPixels(this._source.startTime) - frameOffset;
1347
+ var width = this._view.timeToPixels(this._source.endTime - this._source.startTime);
1348
+ var height = this.getCurrentHeight();
1349
+ var y = this.getAbsoluteY();
1350
+
1351
+ this._dragGhost = new Konva.Rect({
1352
+ x: x,
1353
+ y: y,
1354
+ width: width,
1355
+ height: height,
1356
+ fill: this._source.backgroundColor,
1357
+ opacity: 0.4,
1358
+ stroke: this._source.selectedBorderColor,
1359
+ strokeWidth: 2,
1360
+ dash: [8, 4],
1361
+ cornerRadius: 8,
1362
+ listening: false
1363
+ });
1364
+
1365
+ // Add to the main sources layer (not the group) so it stays behind sources.
1366
+ this._layer.add(this._dragGhost);
1367
+ this._dragGhost.moveToBottom();
1368
+
1369
+ // Ensure initial Y snaps to the current line position.
1370
+ this.updateDragGhost();
1371
+
1372
+ return this._dragGhost;
1373
+ };
1374
+
1375
+ /**
1376
+ * Updates the drag ghost preview position and size.
1377
+ *
1378
+ * @param {Object} lineGroupsById Map of lineId -> Konva.Group
1379
+ */
1380
+ SourceGroup.prototype.updateDragGhost = function(lineGroupsById) {
1381
+ if (!this._dragGhost) {
1382
+ return;
1383
+ }
1384
+
1385
+ // Allow callers to omit the lookup; resolve via the owning layer.
1386
+ if (!lineGroupsById
1387
+ && this._layer
1388
+ && typeof this._layer.getLineGroups === 'function') {
1389
+ var lineGroups = this._layer.getLineGroups();
1390
+
1391
+ if (lineGroups && typeof lineGroups.getLineGroupsById === 'function') {
1392
+ lineGroupsById = lineGroups.getLineGroupsById();
1393
+ }
1394
+ }
1395
+
1396
+ var frameOffset = this._view.getFrameOffset();
1397
+ var x = this._view.timeToPixels(this._source.startTime) - frameOffset;
1398
+ var width = this._view.timeToPixels(this._source.endTime - this._source.startTime);
1399
+
1400
+ this._dragGhost.x(x);
1401
+ this._dragGhost.width(width);
1402
+ this._dragGhost.height(this.getCurrentHeight());
1403
+
1404
+ if (lineGroupsById) {
1405
+ var lineGroup = lineGroupsById[this._source.lineId];
1406
+
1407
+ if (lineGroup) {
1408
+ this._dragGhost.y(lineGroup.y());
1409
+ }
1410
+ }
1411
+ };
1412
+
1413
+ SourceGroup.prototype.destroyDragGhost = function() {
1414
+ if (this._dragGhost) {
1415
+ this._dragGhost.destroy();
1416
+ this._dragGhost = null;
1417
+ }
1418
+ };
1419
+
1199
1420
  SourceGroup.prototype.getHeights = function() {
1200
1421
  return {
1201
1422
  unwrapped: this._unwrappedHeight,
@@ -1684,7 +1905,7 @@ define([
1684
1905
  self._source.volume = Math.max(self._source.volumeRange[0], Math.min(volume, self._source.volumeRange[1]));
1685
1906
  self._peaks.emit('source.volumeChanged', self._source);
1686
1907
 
1687
- self._batchDraw();
1908
+ self._scheduleBatchDraw();
1688
1909
  });
1689
1910
 
1690
1911
  volumeSliderGroup.on('dragend', function() {
@@ -1707,6 +1928,23 @@ define([
1707
1928
  };
1708
1929
 
1709
1930
  SourceGroup.prototype.destroy = function() {
1931
+ this.destroyDragGhost();
1932
+
1933
+ this._disableManualHoverTracking();
1934
+
1935
+ // Cancel any pending idle callbacks to prevent memory leaks
1936
+ if (this._pendingIdleCallbacks) {
1937
+ this._pendingIdleCallbacks.forEach(function(id) {
1938
+ Utils.cancelIdle(id);
1939
+ });
1940
+ this._pendingIdleCallbacks.clear();
1941
+ }
1942
+
1943
+ // Clear preview build queue
1944
+ if (this._previewBuildQueue) {
1945
+ this._previewBuildQueue.clear();
1946
+ }
1947
+
1710
1948
  if (this._buttonsAnimation) {
1711
1949
  this._buttonsAnimation.destroy();
1712
1950
  this._buttonsAnimation = null;