@checksub_team/peaks_timeline 2.3.0-alpha.0 → 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.
@@ -8,8 +8,9 @@
8
8
 
9
9
  define([
10
10
  '../utils',
11
+ './invoker',
11
12
  'konva'
12
- ], function(Utils, Konva) {
13
+ ], function(Utils, Invoker, Konva) {
13
14
  'use strict';
14
15
 
15
16
  var LEFT_PADDING = 4;
@@ -29,52 +30,62 @@ define([
29
30
  function Axis(peaks, view, options) {
30
31
  this._view = view;
31
32
 
33
+ this._invoker = new Invoker();
34
+
32
35
  var self = this;
33
36
 
34
37
  peaks.on('playhead.moved', this._onPlayheadMoved.bind(this));
35
38
  peaks.on('playhead.hidden', this._onPlayheadHidden.bind(this));
36
39
 
37
- self._axisGridlineColor = options.axisGridlineColor;
38
- self._axisLabelColor = options.axisLabelColor;
40
+ this._axisGridlineColor = options.axisGridlineColor;
41
+ this._axisLabelColor = options.axisLabelColor;
39
42
 
40
- self._backLayer = new Konva.Layer({
43
+ this._backLayer = new Konva.Layer({
41
44
  listening: false
42
45
  });
43
46
 
44
- self._frontLayer = new Konva.Layer({
47
+ this._frontLayer = new Konva.Layer({
45
48
  listening: false
46
49
  });
47
50
 
48
- self._axisShape = new Konva.Shape({
51
+ this._axisShape = new Konva.Shape({
49
52
  sceneFunc: function(context) {
50
53
  self.drawAxis(context, view);
51
54
  }
52
55
  });
53
- self._backLayer.add(self._axisShape);
56
+ this._backLayer.add(this._axisShape);
54
57
 
55
- self._timesShape = new Konva.Shape({
58
+ this._timesShape = new Konva.Shape({
56
59
  sceneFunc: function(context) {
57
60
  self.drawTimes(context, view);
58
61
  }
59
62
  });
60
- self._frontLayer.add(self._timesShape);
63
+ this._frontLayer.add(this._timesShape);
64
+
65
+ // Throttled draws to prevent excessive redraws.
66
+ this._throttledBackDraw = this._invoker.throttleTrailing(
67
+ this._backLayer.batchDraw.bind(this._backLayer)
68
+ );
69
+ this._throttledFrontDraw = this._invoker.throttleTrailing(
70
+ this._frontLayer.batchDraw.bind(this._frontLayer)
71
+ );
61
72
  }
62
73
 
63
74
  Axis.prototype._onPlayheadMoved = function(playheadX, playheadWidth) {
64
75
  this._maskStart = playheadX + this._view.getFrameOffset();
65
76
  this._maskEnd = playheadX + this._view.getFrameOffset() + playheadWidth;
66
- this._frontLayer.batchDraw();
77
+ this._throttledFrontDraw();
67
78
  };
68
79
 
69
80
  Axis.prototype._onPlayheadHidden = function() {
70
81
  this._maskStart = null;
71
82
  this._maskEnd = null;
72
- this._frontLayer.batchDraw();
83
+ this._throttledFrontDraw();
73
84
  };
74
85
 
75
86
  Axis.prototype.batchDraw = function() {
76
- this._backLayer.batchDraw();
77
- this._frontLayer.batchDraw();
87
+ this._throttledBackDraw();
88
+ this._throttledFrontDraw();
78
89
  };
79
90
 
80
91
  Axis.prototype.addBackToStage = function(stage) {
@@ -347,43 +347,11 @@ define([
347
347
  this._automaticallyCreatedLineId = automaticallyCreatedLineGroup.getId();
348
348
  this._moveSourcesToPositionIfPossible(sources, newLinePosition);
349
349
 
350
- // After creating and moving to the new line, nudge the vertical scroll so
351
- // the pointer is closer to the center of that line.
352
- this._nudgeFrameOffsetYToLineCenter(newLinePosition, this._automaticLineCreationMouseY);
353
-
354
350
  // Notify that sources have been moved to a new line (for ghost preview updates)
355
351
  this._peaks.emit('sources.delayedLineChange', sources);
356
352
  }.bind(this), this._peaks.options.automaticLineCreationDelay);
357
353
  };
358
354
 
359
- LineGroups.prototype._nudgeFrameOffsetYToLineCenter = function(linePosition, mouseY) {
360
- if (!this._peaks.options.enableVerticalScrolling) {
361
- return;
362
- }
363
-
364
- if (Utils.isNullOrUndefined(mouseY)) {
365
- return;
366
- }
367
-
368
- const lineGroup = this._lineGroupsByPosition[linePosition];
369
-
370
- if (!lineGroup) {
371
- return;
372
- }
373
-
374
- const targetCenterY = lineGroup.y() + (lineGroup.lineHeight() / 2);
375
- const deltaY = targetCenterY - mouseY;
376
-
377
- if (deltaY === 0) {
378
- return;
379
- }
380
-
381
- const maxOffsetY = Math.max(0, this._view.getFullHeight() - this._view.getHeight());
382
- const nextOffsetY = Utils.clamp(this._view.getFrameOffsetY() + deltaY, 0, maxOffsetY);
383
-
384
- this._view.updateTimeline(null, nextOffsetY);
385
- };
386
-
387
355
  LineGroups.prototype.manageVerticalPosition = function(sources, startTime, endTime, mouseX, mouseY) {
388
356
  if (Utils.isNullOrUndefined(mouseX)) {
389
357
  return;
@@ -9,10 +9,12 @@
9
9
  define([
10
10
  'konva',
11
11
  './svgs',
12
+ './invoker',
12
13
  '../utils'
13
14
  ], function(
14
15
  Konva,
15
16
  SVGs,
17
+ Invoker,
16
18
  Utils) {
17
19
  'use strict';
18
20
 
@@ -31,6 +33,8 @@ define([
31
33
  this._view = view;
32
34
  this._container = container;
33
35
 
36
+ this._invoker = new Invoker();
37
+
34
38
  this._width = this._peaks.options.lineIndicatorWidth;
35
39
  this._height = this._view.getHeight();
36
40
 
@@ -58,6 +62,11 @@ define([
58
62
  this._layer = new Konva.Layer();
59
63
  this._stage.add(this._layer);
60
64
 
65
+ // Throttled draws to coalesce multiple updates per frame.
66
+ this._throttledBatchDraw = this._invoker.throttleTrailing(
67
+ this._layer.batchDraw.bind(this._layer)
68
+ );
69
+
61
70
  this._indicators = {};
62
71
 
63
72
  this._separatingLine = new Konva.Line({
@@ -69,7 +78,7 @@ define([
69
78
 
70
79
  this._layer.add(this._separatingLine);
71
80
 
72
- this._layer.batchDraw();
81
+ this.batchDraw();
73
82
 
74
83
  this._isDragging = false;
75
84
  this._dragLineId = null;
@@ -423,7 +432,7 @@ define([
423
432
  };
424
433
 
425
434
  LineIndicator.prototype.batchDraw = function() {
426
- this._layer.batchDraw();
435
+ this._throttledBatchDraw();
427
436
  };
428
437
 
429
438
  LineIndicator.prototype._onWindowMove = function(event) {
@@ -8,10 +8,11 @@
8
8
 
9
9
  define([
10
10
  './source-group',
11
+ './invoker',
11
12
  '../models/source',
12
13
  '../utils',
13
14
  'konva'
14
- ], function(SourceGroup, Source, Utils, Konva) {
15
+ ], function(SourceGroup, Invoker, Source, Utils, Konva) {
15
16
  'use strict';
16
17
 
17
18
  var TIME_X_OFFSET = 20;
@@ -35,6 +36,8 @@ define([
35
36
  this._playheadLayer = playheadLayer;
36
37
  this._stage = stage;
37
38
 
39
+ this._invoker = new Invoker();
40
+
38
41
  this._selectedElements = {};
39
42
  this._currentLine = null;
40
43
 
@@ -42,6 +45,11 @@ define([
42
45
  listening: this._mode !== 'default'
43
46
  });
44
47
 
48
+ // Throttled draws to coalesce multiple updates per frame.
49
+ this._throttledBatchDraw = this._invoker.throttleTrailing(
50
+ this._layer.batchDraw.bind(this._layer)
51
+ );
52
+
45
53
  this._prepareDefaultMode();
46
54
 
47
55
  this._onMouseClickInDefaultMode = this._onMouseClickInDefaultMode.bind(this);
@@ -59,6 +67,10 @@ define([
59
67
  this._peaks.on('handler.sources.destroy', this._onSourcesDestroy.bind(this));
60
68
  }
61
69
 
70
+ ModeLayer.prototype.batchDraw = function() {
71
+ this._throttledBatchDraw();
72
+ };
73
+
62
74
  ModeLayer.prototype._onSourcesDestroy = function(sources) {
63
75
  const selectedElementsToDeselect = {};
64
76
 
@@ -262,7 +274,7 @@ define([
262
274
  ModeLayer.prototype._onMouseEnterInCutMode = function() {
263
275
  this._cutGroup.visible(true);
264
276
 
265
- this._layer.batchDraw();
277
+ this.batchDraw();
266
278
  };
267
279
 
268
280
  ModeLayer.prototype._updateCursorTime = function(position) {
@@ -363,13 +375,13 @@ define([
363
375
  this._updateCursorTime(cuttingPosition);
364
376
  this._updateCuttingLine(cuttingPosition);
365
377
 
366
- this._layer.batchDraw();
378
+ this.batchDraw();
367
379
  };
368
380
 
369
381
  ModeLayer.prototype._onMouseLeaveInCutMode = function() {
370
382
  this._cutGroup.visible(false);
371
383
 
372
- this._layer.batchDraw();
384
+ this.batchDraw();
373
385
  };
374
386
 
375
387
  ModeLayer.prototype._onMouseClickInCutMode = function() {
@@ -414,18 +426,49 @@ define([
414
426
 
415
427
  this._cuttingLine.visible(false);
416
428
 
429
+ // Avoid keeping a reference to a SourceGroup that will be destroyed
430
+ // as part of the cut update.
431
+ this._view.setHoveredElement(null);
432
+
417
433
  this._peaks.cutSource(hoveredElement.getSource(), this._view.pixelsToTime(cuttingPixel));
418
434
 
419
- this._view.setHoveredElement(null);
435
+ // Cutting replaces Konva nodes under the cursor; Konva won't always emit
436
+ // enter/leave for the new nodes until the mouse moves. Re-sync hover now
437
+ // so subsequent leave events behave correctly.
438
+ this._syncHoveredElementFromPointer();
420
439
 
421
440
  this._updateCursorTime(cuttingPosition);
422
441
  this._updateCuttingLine(cuttingPosition);
423
442
 
424
- this._layer.batchDraw();
443
+ this.batchDraw();
425
444
  }
426
445
  }
427
446
  };
428
447
 
448
+ ModeLayer.prototype._syncHoveredElementFromPointer = function() {
449
+ var pointerPos = this._stage.getPointerPosition();
450
+
451
+ if (!pointerPos) {
452
+ return;
453
+ }
454
+
455
+ var node = this._stage.getIntersection(pointerPos);
456
+
457
+ while (node) {
458
+ if (node.attrs && node.attrs.sourceId) {
459
+ var sourceGroup = this._view.getSourceGroupById(node.attrs.sourceId);
460
+
461
+ if (sourceGroup && !sourceGroup.isHovered()) {
462
+ sourceGroup.startHover();
463
+ }
464
+
465
+ return;
466
+ }
467
+
468
+ node = node.getParent ? node.getParent() : null;
469
+ }
470
+ };
471
+
429
472
  ModeLayer.prototype.setMode = function(mode) {
430
473
  if (this._mode === mode) {
431
474
  return;
@@ -498,7 +541,7 @@ define([
498
541
  }
499
542
 
500
543
  this._mode = mode;
501
- this._layer.batchDraw();
544
+ this.batchDraw();
502
545
  this._view.batchDrawSourcesLayer();
503
546
  };
504
547
 
@@ -8,8 +8,9 @@
8
8
 
9
9
  define([
10
10
  '../utils',
11
+ './invoker',
11
12
  'konva'
12
- ], function(Utils, Konva) {
13
+ ], function(Utils, Invoker, Konva) {
13
14
  'use strict';
14
15
 
15
16
  var HANDLE_RADIUS = 10;
@@ -38,6 +39,11 @@ define([
38
39
 
39
40
  this._playheadLayer = new Konva.Layer();
40
41
 
42
+ this._invoker = new Invoker();
43
+ this._throttledBatchDraw = this._invoker.throttleTrailing(
44
+ this._playheadLayer.batchDraw.bind(this._playheadLayer)
45
+ );
46
+
41
47
  this._activeSegments = {};
42
48
  this._activeSources = {};
43
49
 
@@ -236,10 +242,14 @@ define([
236
242
 
237
243
  this._time = time;
238
244
  if (pixelHasChanged || timeHasChanged) {
239
- this._playheadLayer.batchDraw();
245
+ this.batchDraw();
240
246
  }
241
247
  };
242
248
 
249
+ PlayheadLayer.prototype.batchDraw = function() {
250
+ this._throttledBatchDraw();
251
+ };
252
+
243
253
  /**
244
254
  * Update cached pixel values and position of the playhead group.
245
255
  * @private
@@ -375,7 +385,7 @@ define([
375
385
  }
376
386
 
377
387
  if (updated) {
378
- this._playheadLayer.batchDraw();
388
+ this.batchDraw();
379
389
  }
380
390
  };
381
391
 
@@ -66,6 +66,9 @@ define([
66
66
  this._isHandleDragging = false;
67
67
  this._destroyed = false;
68
68
 
69
+ // Optional Konva element used as a drag "ghost" preview.
70
+ this._dragGhost = null;
71
+
69
72
  // Performance: track if draw is needed
70
73
  this._drawScheduled = false;
71
74
 
@@ -151,11 +154,54 @@ define([
151
154
  SourceGroup.prototype._onHoverEnd = function() {
152
155
  this._hovered = false;
153
156
  this._manualHover = false;
157
+ this._disableManualHoverTracking();
154
158
  this._view.setHoveredElement(null);
155
159
  this._hideButtons();
156
160
  this._scheduleBatchDraw();
157
161
  };
158
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;
203
+ };
204
+
159
205
  SourceGroup.prototype._onDragStart = function(element) {
160
206
  this._isDragged = true;
161
207
  this._layer.onSourcesGroupDragStart(element);
@@ -231,7 +277,7 @@ define([
231
277
  this._layer.batchDraw();
232
278
  }
233
279
  }.bind(this)
234
- );
280
+ , null, false);
235
281
 
236
282
  return {
237
283
  x: draggedElement.absolutePosition().x,
@@ -285,6 +331,13 @@ define([
285
331
  // update unwrap
286
332
  this.updatePreviews();
287
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
+ }
288
341
  };
289
342
 
290
343
  SourceGroup.prototype.setWrapping = function(wrap, forceCreate, notify) {
@@ -753,6 +806,7 @@ define([
753
806
 
754
807
  SourceGroup.prototype.startHover = function() {
755
808
  this._manualHover = true;
809
+ this._enableManualHoverTracking();
756
810
  this._group.fire('mouseenter', { evt: new MouseEvent('mouseenter') }, true);
757
811
  };
758
812
 
@@ -762,6 +816,14 @@ define([
762
816
 
763
817
  SourceGroup.prototype.setDragging = function(isDragging) {
764
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
+ }
765
827
  };
766
828
 
767
829
  SourceGroup.prototype.startDrag = function() {
@@ -1271,6 +1333,90 @@ define([
1271
1333
  return this._height;
1272
1334
  };
1273
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
+
1274
1420
  SourceGroup.prototype.getHeights = function() {
1275
1421
  return {
1276
1422
  unwrapped: this._unwrappedHeight,
@@ -1782,6 +1928,10 @@ define([
1782
1928
  };
1783
1929
 
1784
1930
  SourceGroup.prototype.destroy = function() {
1931
+ this.destroyDragGhost();
1932
+
1933
+ this._disableManualHoverTracking();
1934
+
1785
1935
  // Cancel any pending idle callbacks to prevent memory leaks
1786
1936
  if (this._pendingIdleCallbacks) {
1787
1937
  this._pendingIdleCallbacks.forEach(function(id) {