forked from ovidiuch/dragdealer
-
Notifications
You must be signed in to change notification settings - Fork 1
/
dragdealer.js
995 lines (947 loc) · 35.6 KB
/
dragdealer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
/**
* Dragdealer.js 0.9.8
* http://github.com/skidding/dragdealer
*
* (c) 2010+ Ovidiu Cherecheș
* http://skidding.mit-license.org
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(factory);
} else if (typeof module === 'object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like enviroments that support module.exports,
// like Node.
module.exports.Dragdealer = factory();
} else {
// Browser globals
root.Dragdealer = factory();
}
}(this, function () {
var Dragdealer = function(wrapper, options) {
/**
* Drag-based component that works around two basic DOM elements.
*
* - The wrapper: The top-level element with the .dragdealer class. We
* create a Dragdealer instance with the wrapper as the
* first constructor parameter (it can either receive the ID
* of the wrapper, or the element itself.) The wrapper
* establishes the dragging bounds.
*
* - The handle: A child of the wrapper element, div with a required
* .handle class (may be overridden in options). This will be
* the dragged element, constrained by the wrapper's bounds.
*
*
* The handle can be both smaller or bigger than the wrapper.
*
* - When the handle is smaller, Dragdealer will act as a regular slider,
* enabling the handle to be dragged from one side of the wrapper to
* another.
*
* - When the handle is bigger, Dragdealer will act a mask for a draggable
* surface, where the handle is the draggable surface contrained by the
* smaller bounds of the wrapper. The drag action in this case is used
* to reveal and "discover" partial content at a time.
*
*
* Simple usage:
*
* // JavaScript
* new Dragdealer('simple-slider');
*
* <!-- HTML -->
* <div id="simple-slider" class="dragdealer">
* <div class="handle">drag me</div>
* </div>
*
*
* The second parameter of the Dragdealer constructor is an object used for
* specifying any of the supported options. All of them are optional.
*
* - bool disabled=false: Init Dragdealer in a disabled state. The handle
* will have a .disabled class.
*
* - bool horizontal=true: Enable horizontal dragging.
*
* - bool vertical=false: Enable vertical dragging.
*
* - number x=0: Initial horizontal (left) position. Accepts a float number
* value between 0 and 1. Read below about positioning in
* Dragdealer.
*
* - number y=0: Initial vertical (top) position. Accepts a float number
* value between 0 and 1. Read below about positoning in
* Dragdealer.
*
* - number steps=0: Limit the positioning of the handle within the bounds
* of the wrapper, by defining a virtual grid made out of
* a number of equally-spaced steps. This restricts
* placing the handle anywhere in-between these steps.
* E.g. setting 3 steps to a regular slider will only
* allow you to move it to the left, to the right or
* exactly in the middle.
*
* - bool snap=false: When a number of steps is set, snap the position of
* the handle to its closest step instantly, even when
* dragging.
*
* - number speed=0.1: Speed can be set between 0 and 1, with 1 being the
* fastest. It represents how fast the handle will slide
* to position after you mouse up.
*
* - bool slide=true: Slide handle after releasing it, depending on the
* movement speed before the mouse/touch release. The
* formula for calculating how much will the handle
* slide after releasing it is defined by simply
* extending the movement of the handle in the current
* direction, with the last movement unit times four (a
* movement unit is considered the distance crossed
* since the last animation loop, which is currently
* 25ms.) So if you were to drag the handle 50px in the
* blink of an eye, it will slide another 200px in the
* same direction. Steps interfere with this formula, as
* the closest step is calculated before the sliding
* distance.
*
* - bool loose=false: Loosen-up wrapper boundaries when dragging. This
* allows the handle to be *slightly* dragged outside
* the bounds of the wrapper, but slides it back to the
* margins of the wrapper upon release. The formula for
* calculating how much the handle exceeds the wrapper
* bounds is made out of the actual drag distance
* divided by 4. E.g. Pulling a slider outside its
* frame by 100px will only position it 25px outside
* the frame.
*
* - number top=0: Top padding between the wrapper and the handle.
*
* - number bottom=0: Bottom padding between the wrapper and the handle.
*
* - number left=0: Left padding between the wrapper and the handle.
*
* - number right=0: Right padding between the wrapper and the handle.
*
* - number inertia=4: Controls the speed or time it takes to dampen or
* slow after throwing the object. Preferred range is
* between 0 (doesn't throw prop) and 10 (makes the
* prop fly till hitting edge).
*
* - fn callback(x, y): Called when releasing handle, with the projected
* x, y position of the handle. Projected value means
* the value the slider will have after finishing a
* sliding animation, caused by either a step
* restriction or drag motion (see steps and slide
* options.) This implies that the actual position of
* the handle at the time this callback is called
* might not yet reflect the x, y values received.
*
* - fn clickCallback(x,y): Only fires when a click is instantiated. Does
* not fire if dragging occurs. Will not fire until
* tap or click is released.
*
* - fn dragStopCallback(x,y): Same as callback(x,y) but only called after
* a drag motion, not after setting the step
* manually. This neve fires unless you actually
* begin dragging, else clickCallback is called.
*
* - fn dragStartCallback(x,y): Same as dragStopCallback(x,y) but called at
* the beginning of a drag motion and with the
* sliders initial x, y values. Only calls when
* when user actually drags and not on click.
*
* - fn animationCallback(x, y): Called every animation loop, as long as
* the handle is being dragged or in the
* process of a sliding animation. The x, y
* positional values received by this
* callback reflect the exact position of the
* handle DOM element, which includes
* exceeding values (even negative values)
* when the loose option is set true.
*
* - string handleClass='handle': Custom class of handle element.
*
* - bool css3=true: Use css3 transform in modern browsers instead of
* absolute positioning.
*
* - fn customRequestAnimationFrame: Provide custom requestAnimationFrame
* function (used in tests).
* - fn customCancelAnimationFrame: Provide custom cancelAnimationFrame
* function (used in tests).
*
* Dragdealer also has a few methods to interact with, post-initialization.
*
* - disable: Disable dragging of a Dragdealer instance. Just as with the
* disabled option, the handle will receive a .disabled class
*
* - enable: Enable dragging of a Dragdealer instance. The .disabled class
* of the handle will be removed.
*
* - reflow: Recalculate the wrapper bounds of a Dragdealer instance, used
* when the wrapper is responsive and its parent container
* changed its size, or after changing the size of the wrapper
* directly.
*
* - getValue: Get the value of a Dragdealer instance programatically. The
* value is returned as an [x, y] tuple and is the equivalent
* of the (projected) value returned by the regular callback,
* not animationCallback.
*
* - getStep: Same as getValue, but the value returned is in step
* increments (see steps option)
*
* - setValue(x, y, snap=false): Set the value of a Dragdealer instance
* programatically. The 3rd parameter allows
* to snap the handle directly to the desired
* value, without any sliding transition.
*
* - setStep(x, y, snap=false): Same as setValue, but the value is received
* in step increments (see steps option)
*
*
* Positioning in Dragdealer:
*
* Besides the top, bottom, left and right paddings, which represent a
* number of pixels, Dragdealer uses a [0, 1]-based positioning. Both
* horizontal and vertical positions are represented by ratios between 0
* and 1. This allows the Dragdealer wrapper to have a responsive size and
* not revolve around a specific number of pixels. This is how the x, y
* options are set, what the callback args contain and what values the
* setValue method expects. Once picked up, the ratios can be scaled and
* mapped to match any real-life system of coordinates or dimensions.
*/
this.options = this.applyDefaults(options || {});
this.bindMethods();
this.wrapper = this.getWrapperElement(wrapper);
if (!this.wrapper) {
return;
}
this.handle = this.getHandleElement(this.wrapper, this.options.handleClass);
if (!this.handle) {
return;
}
this.init();
this.bindEventListeners();
};
Dragdealer.prototype = {
defaults: {
disabled: false,
horizontal: true,
vertical: false,
slide: true,
steps: 0,
inertia: 4,
snap: false,
loose: false,
speed: 0.1,
xPrecision: 0,
yPrecision: 0,
handleClass: 'handle',
css3: true,
activeClass: 'active',
tapping: true
},
init: function() {
if (this.options.css3) {
triggerWebkitHardwareAcceleration(this.handle);
}
this.value = {
prev: [-1, -1],
current: [this.options.x || 0, this.options.y || 0],
target: [this.options.x || 0, this.options.y || 0]
};
this.offset = {
wrapper: [0, 0],
mouse: [0, 0],
prev: [-999999, -999999],
current: [0, 0],
target: [0, 0]
};
this.dragStartPosition = {x: 0, y: 0};
this.change = [0, 0];
this.stepRatios = this.calculateStepRatios();
this.activity = false;
this.dragging = false;
this.tapping = false;
this.hasStartedDrag = false;
this.dragNeverCaptured = false;
this.reflow();
if (this.options.disabled) {
this.disable();
}
},
applyDefaults: function(options) {
for (var k in this.defaults) {
if (!options.hasOwnProperty(k)) {
options[k] = this.defaults[k];
}
}
return options;
},
getWrapperElement: function(wrapper) {
if (typeof(wrapper) == 'string') {
return document.getElementById(wrapper);
} else {
return wrapper;
}
},
getHandleElement: function(wrapper, handleClass) {
var childElements,
handleClassMatcher,
i;
if (wrapper.getElementsByClassName) {
childElements = wrapper.getElementsByClassName(handleClass);
if (childElements.length > 0) {
return childElements[0];
}
} else {
handleClassMatcher = new RegExp('(^|\\s)' + handleClass + '(\\s|$)');
childElements = wrapper.getElementsByTagName('*');
for (i = 0; i < childElements.length; i++) {
if (handleClassMatcher.test(childElements[i].className)) {
return childElements[i];
}
}
}
},
calculateStepRatios: function() {
var stepRatios = [];
if (this.options.steps >= 1) {
for (var i = 0; i <= this.options.steps - 1; i++) {
if (this.options.steps > 1) {
stepRatios[i] = i / (this.options.steps - 1);
} else {
// A single step will always have a 0 value
stepRatios[i] = 0;
}
}
}
return stepRatios;
},
setWrapperOffset: function() {
this.offset.wrapper = Position.get(this.wrapper);
},
calculateBounds: function() {
// Apply top/bottom/left and right padding options to wrapper extremities
// when calculating its bounds
var bounds = {
top: this.options.top || 0,
bottom: -(this.options.bottom || 0) + this.wrapper.offsetHeight,
left: this.options.left || 0,
right: -(this.options.right || 0) + this.wrapper.offsetWidth
};
// The available width and height represents the horizontal and vertical
// space the handle has for moving. It is determined by the width and
// height of the wrapper, minus the width and height of the handle
bounds.availWidth = (bounds.right - bounds.left) - this.handle.offsetWidth;
bounds.availHeight = (bounds.bottom - bounds.top) - this.handle.offsetHeight;
return bounds;
},
calculateValuePrecision: function() {
// The sliding transition works by dividing itself until it reaches a min
// value step; because Dragdealer works with [0-1] values, we need this
// "min value step" to represent a pixel when applied to the real handle
// position within the DOM. The xPrecision/yPrecision options can be
// specified to increase the granularity when we're controlling larger
// objects from one of the callbacks
var xPrecision = this.options.xPrecision || Math.abs(this.bounds.availWidth),
yPrecision = this.options.yPrecision || Math.abs(this.bounds.availHeight);
return [
xPrecision ? 1 / xPrecision : 0,
yPrecision ? 1 / yPrecision : 0
];
},
bindMethods: function() {
if (typeof(this.options.customRequestAnimationFrame) === 'function') {
this.requestAnimationFrame = bind(this.options.customRequestAnimationFrame, window);
} else {
this.requestAnimationFrame = bind(requestAnimationFrame, window);
}
if (typeof(this.options.customCancelAnimationFrame) === 'function') {
this.cancelAnimationFrame = bind(this.options.customCancelAnimationFrame, window);
} else {
this.cancelAnimationFrame = bind(cancelAnimationFrame, window);
}
this.animateWithRequestAnimationFrame = bind(this.animateWithRequestAnimationFrame, this);
this.animate = bind(this.animate, this);
this.onHandleMouseDown = bind(this.onHandleMouseDown, this);
this.onHandleTouchStart = bind(this.onHandleTouchStart, this);
this.onDocumentMouseMove = bind(this.onDocumentMouseMove, this);
this.onWrapperTouchMove = bind(this.onWrapperTouchMove, this);
this.onWrapperMouseDown = bind(this.onWrapperMouseDown, this);
this.onWrapperTouchStart = bind(this.onWrapperTouchStart, this);
this.onDocumentMouseUp = bind(this.onDocumentMouseUp, this);
this.onDocumentTouchEnd = bind(this.onDocumentTouchEnd, this);
this.onHandleClick = bind(this.onHandleClick, this);
this.onWindowResize = bind(this.onWindowResize, this);
},
bindEventListeners: function() {
// Start dragging
addEventListener(this.handle, 'mousedown', this.onHandleMouseDown);
addEventListener(this.handle, 'touchstart', this.onHandleTouchStart);
// While dragging
addEventListener(document, 'mousemove', this.onDocumentMouseMove);
addEventListener(this.wrapper, 'touchmove', this.onWrapperTouchMove);
// Start tapping
addEventListener(this.wrapper, 'mousedown', this.onWrapperMouseDown);
addEventListener(this.wrapper, 'touchstart', this.onWrapperTouchStart);
// Stop dragging/tapping
addEventListener(document, 'mouseup', this.onDocumentMouseUp);
addEventListener(document, 'touchend', this.onDocumentTouchEnd);
addEventListener(this.handle, 'click', this.onHandleClick);
addEventListener(window, 'resize', this.onWindowResize);
this.animate(false, true);
this.interval = this.requestAnimationFrame(this.animateWithRequestAnimationFrame);
},
unbindEventListeners: function() {
removeEventListener(this.handle, 'mousedown', this.onHandleMouseDown);
removeEventListener(this.handle, 'touchstart', this.onHandleTouchStart);
removeEventListener(document, 'mousemove', this.onDocumentMouseMove);
removeEventListener(this.wrapper, 'touchmove', this.onWrapperTouchMove);
removeEventListener(this.wrapper, 'mousedown', this.onWrapperMouseDown);
removeEventListener(this.wrapper, 'touchstart', this.onWrapperTouchStart);
removeEventListener(document, 'mouseup', this.onDocumentMouseUp);
removeEventListener(document, 'touchend', this.onDocumentTouchEnd);
removeEventListener(this.handle, 'click', this.onHandleClick);
removeEventListener(window, 'resize', this.onWindowResize);
this.cancelAnimationFrame(this.interval);
},
onHandleMouseDown: function(e) {
Cursor.refresh(e);
preventEventDefaults(e);
stopEventPropagation(e);
this.activity = false;
this.startDrag();
},
onHandleTouchStart: function(e) {
Cursor.refresh(e);
// Unlike in the `mousedown` event handler, we don't prevent defaults here,
// because this would disable the dragging altogether. Instead, we prevent
// it in the `touchmove` handler. Read more about touch events
// https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Touch_events#Handling_clicks
stopEventPropagation(e);
this.activity = false;
this.startDrag();
},
onDocumentMouseMove: function(e) {
if ((e.clientX - this.dragStartPosition.x) === 0 &&
(e.clientY - this.dragStartPosition.y) === 0) {
// This is required on some Windows8 machines that get mouse move events without actual mouse movement
return;
}
Cursor.refresh(e);
if (this.dragging) {
this.activity = true;
preventEventDefaults(e);
}
},
onWrapperTouchMove: function(e) {
Cursor.refresh(e);
// Dragging on a disabled axis (horizontal or vertical) shouldn't prevent
// defaults on touch devices. !this.activity denotes this is the first move
// inside a drag action; you can drag in any direction after this point if
// the dragging wasn't stopped
if (!this.activity && this.draggingOnDisabledAxis()) {
if (this.dragging) {
this.stopDrag();
}
return;
}
// Read comment in `onHandleTouchStart` above, to understand why we're
// preventing defaults here and not there
preventEventDefaults(e);
this.activity = true;
},
onWrapperMouseDown: function(e) {
Cursor.refresh(e);
preventEventDefaults(e);
this.startTap();
},
onWrapperTouchStart: function(e) {
Cursor.refresh(e);
preventEventDefaults(e);
this.startTap();
},
onDocumentMouseUp: function(e) {
this.stopDrag();
this.stopTap();
},
onDocumentTouchEnd: function(e) {
this.stopDrag();
this.stopTap();
},
onHandleClick: function(e) {
// We keep track if any dragging activity has been made between the
// mouse/touch down and up events; based on this we allow or cancel a click
// event from inside the handle. i.e. Click events shouldn't be triggered
// when dragging, but should be allowed when clicking still
if (this.activity) {
preventEventDefaults(e);
stopEventPropagation(e);
}
},
onWindowResize: function(e) {
this.reflow();
},
enable: function() {
this.disabled = false;
this.handle.className = this.handle.className.replace(/\s?disabled/g, '');
},
disable: function() {
this.disabled = true;
this.handle.className += ' disabled';
},
reflow: function() {
this.setWrapperOffset();
this.bounds = this.calculateBounds();
this.valuePrecision = this.calculateValuePrecision();
this.updateOffsetFromValue();
},
getStep: function() {
return [
this.getStepNumber(this.value.target[0]),
this.getStepNumber(this.value.target[1])
];
},
getStepWidth: function () {
return Math.abs(this.bounds.availWidth / this.options.steps);
},
getValue: function() {
return this.value.target;
},
setStep: function(x, y, snap) {
this.setValue(
this.options.steps && x > 1 ? (x - 1) / (this.options.steps - 1) : 0,
this.options.steps && y > 1 ? (y - 1) / (this.options.steps - 1) : 0,
snap
);
},
setValue: function(x, y, snap) {
this.setTargetValue([x, y || 0]);
if (snap) {
this.groupCopy(this.value.current, this.value.target);
// Since the current value will be equal to the target one instantly, the
// animate function won't get to run so we need to update the positions
// and call the callbacks manually
this.updateOffsetFromValue();
this.callAnimationCallback();
}
},
startTap: function() {
if (this.disabled || !this.options.tapping) {
return;
}
this.tapping = true;
this.setWrapperOffset();
this.setTargetValueByOffset([
Cursor.x - this.offset.wrapper[0] - (this.handle.offsetWidth / 2),
Cursor.y - this.offset.wrapper[1] - (this.handle.offsetHeight / 2)
]);
},
stopTap: function() {
if (this.disabled || !this.tapping) {
return;
}
this.tapping = false;
this.setTargetValue(this.value.current);
},
startDrag: function() {
if (this.disabled) {
return;
}
this.dragging = true;
this.setWrapperOffset();
this.dragStartPosition = {x: Cursor.x, y: Cursor.y};
this.offset.mouse = [
Cursor.x - Position.get(this.handle)[0],
Cursor.y - Position.get(this.handle)[1]
];
if (!this.wrapper.className.match(this.options.activeClass)) {
this.wrapper.className += ' ' + this.options.activeClass;
}
},
stopDrag: function() {
if (this.disabled || !this.dragging) {
return;
}
this.dragging = false;
var deltaX = this.bounds.availWidth === 0 ? 0 :
((Cursor.x - this.dragStartPosition.x) / this.bounds.availWidth),
deltaY = this.bounds.availHeight === 0 ? 0 :
((Cursor.y - this.dragStartPosition.y) / this.bounds.availHeight),
delta = [deltaX, deltaY];
var target = this.groupClone(this.value.current);
if (this.options.slide) {
var ratioChange = this.change;
target[0] += ratioChange[0] * this.options.inertia;
target[1] += ratioChange[1] * this.options.inertia;
}
this.setTargetValue(target);
this.wrapper.className = this.wrapper.className.replace(' ' + this.options.activeClass, '');
if (this.hasStartedDrag) {
this.callDragStopCallback(delta);
}
if (this.dragNeverCaptured) {
this.callClickCallback();
}
this.hasStartedDrag = false;
this.hasClicked = false;
},
callAnimationCallback: function() {
var value = this.value.current;
if (this.options.snap && this.options.steps > 1) {
value = this.getClosestSteps(value);
}
if (!this.groupCompare(value, this.value.prev)) {
if (typeof(this.options.animationCallback) == 'function') {
this.options.animationCallback.call(this, value[0], value[1]);
}
this.groupCopy(this.value.prev, value);
}
},
callClickCallback: function() {
if (typeof(this.options.clickCallback) == 'function') {
this.options.clickCallback.call(this, this.value.target[0], this.value.target[1]);
}
},
callTargetCallback: function() {
if (typeof(this.options.callback) == 'function') {
this.options.callback.call(this, this.value.target[0], this.value.target[1]);
}
},
callDragStartCallback: function() {
if (typeof(this.options.dragStartCallback) == 'function') {
this.options.dragStartCallback.call(this, this.value.target[0], this.value.target[1]);
}
},
callDragStopCallback: function(delta) {
if (typeof(this.options.dragStopCallback) == 'function') {
this.options.dragStopCallback.call(this, this.value.target[0], this.value.target[1], delta);
}
},
animateWithRequestAnimationFrame: function (time) {
if (time) {
// using requestAnimationFrame
this.timeOffset = this.timeStamp ? time - this.timeStamp : 0;
this.timeStamp = time;
} else {
// using setTimeout(callback, 25) polyfill
this.timeOffset = 25;
}
this.animate();
this.interval = this.requestAnimationFrame(this.animateWithRequestAnimationFrame);
},
animate: function(direct, first) {
if (direct && !this.dragging) {
return;
}
if ((Cursor.x !== this.dragStartPosition.x ||
Cursor.y !== this.dragStartPosition.y)) {
if (!this.hasStartedDrag && this.dragging) {
this.callDragStartCallback();
this.hasStartedDrag = true;
this.dragNeverCaptured = false;
}
} else {
this.dragNeverCaptured = true;
}
if (this.dragging) {
var prevTarget = this.groupClone(this.value.target);
var offset = [
Cursor.x - this.offset.wrapper[0] - this.offset.mouse[0],
Cursor.y - this.offset.wrapper[1] - this.offset.mouse[1]
];
this.setTargetValueByOffset(offset, this.options.loose);
this.change = [
this.value.target[0] - prevTarget[0],
this.value.target[1] - prevTarget[1]
];
}
if (this.dragging || first) {
this.groupCopy(this.value.current, this.value.target);
}
if (this.dragging || this.glide() || first) {
this.updateOffsetFromValue();
this.callAnimationCallback();
}
},
glide: function() {
var diff = [
this.value.target[0] - this.value.current[0],
this.value.target[1] - this.value.current[1]
];
if (!diff[0] && !diff[1]) {
return false;
}
if (Math.abs(diff[0]) > this.valuePrecision[0] ||
Math.abs(diff[1]) > this.valuePrecision[1]) {
this.value.current[0] += diff[0] * Math.min(this.options.speed * this.timeOffset / 25, 1);
this.value.current[1] += diff[1] * Math.min(this.options.speed * this.timeOffset / 25, 1);
} else {
this.groupCopy(this.value.current, this.value.target);
}
return true;
},
updateOffsetFromValue: function() {
if (!this.options.snap) {
this.offset.current = this.getOffsetsByRatios(this.value.current);
} else {
this.offset.current = this.getOffsetsByRatios(
this.getClosestSteps(this.value.current)
);
}
if (!this.groupCompare(this.offset.current, this.offset.prev)) {
this.renderHandlePosition();
this.groupCopy(this.offset.prev, this.offset.current);
}
},
renderHandlePosition: function() {
var transform = '';
if (this.options.css3 && StylePrefix.transform) {
if (this.options.horizontal) {
transform += 'translateX(' + this.offset.current[0] + 'px)';
}
if (this.options.vertical) {
transform += ' translateY(' + this.offset.current[1] + 'px)';
}
this.handle.style[StylePrefix.transform] = transform;
return;
}
if (this.options.horizontal) {
this.handle.style.left = this.offset.current[0] + 'px';
}
if (this.options.vertical) {
this.handle.style.top = this.offset.current[1] + 'px';
}
},
setTargetValue: function(value, loose) {
var target = loose ? this.getLooseValue(value) : this.getProperValue(value);
this.groupCopy(this.value.target, target);
this.offset.target = this.getOffsetsByRatios(target);
this.callTargetCallback();
},
setTargetValueByOffset: function(offset, loose) {
var value = this.getRatiosByOffsets(offset);
var target = loose ? this.getLooseValue(value) : this.getProperValue(value);
this.groupCopy(this.value.target, target);
this.offset.target = this.getOffsetsByRatios(target);
},
getLooseValue: function(value) {
var proper = this.getProperValue(value);
return [
proper[0] + ((value[0] - proper[0]) / 4),
proper[1] + ((value[1] - proper[1]) / 4)
];
},
getProperValue: function(value) {
var proper = this.groupClone(value);
proper[0] = Math.max(proper[0], 0);
proper[1] = Math.max(proper[1], 0);
proper[0] = Math.min(proper[0], 1);
proper[1] = Math.min(proper[1], 1);
if ((!this.dragging && !this.tapping) || this.options.snap) {
if (this.options.steps > 1) {
proper = this.getClosestSteps(proper);
}
}
return proper;
},
getRatiosByOffsets: function(group) {
return [
this.getRatioByOffset(group[0], this.bounds.availWidth, this.bounds.left),
this.getRatioByOffset(group[1], this.bounds.availHeight, this.bounds.top)
];
},
getRatioByOffset: function(offset, range, padding) {
return range ? (offset - padding) / range : 0;
},
getOffsetsByRatios: function(group) {
return [
this.getOffsetByRatio(group[0], this.bounds.availWidth, this.bounds.left),
this.getOffsetByRatio(group[1], this.bounds.availHeight, this.bounds.top)
];
},
getOffsetByRatio: function(ratio, range, padding) {
return Math.round(ratio * range) + padding;
},
getStepNumber: function(value) {
// Translate a [0-1] value into a number from 1 to N steps (set using the
// "steps" option)
return this.getClosestStep(value) * (this.options.steps - 1) + 1;
},
getClosestSteps: function(group) {
return [
this.getClosestStep(group[0]),
this.getClosestStep(group[1])
];
},
getClosestStep: function(value) {
var k = 0;
var min = 1;
for (var i = 0; i <= this.options.steps - 1; i++) {
if (Math.abs(this.stepRatios[i] - value) < min) {
min = Math.abs(this.stepRatios[i] - value);
k = i;
}
}
return this.stepRatios[k];
},
groupCompare: function(a, b) {
return a[0] == b[0] && a[1] == b[1];
},
groupCopy: function(a, b) {
a[0] = b[0];
a[1] = b[1];
},
groupClone: function(a) {
return [a[0], a[1]];
},
draggingOnDisabledAxis: function() {
return (!this.options.horizontal && Cursor.xDiff > Cursor.yDiff) ||
(!this.options.vertical && Cursor.yDiff > Cursor.xDiff);
}
};
var bind = function(fn, context) {
/**
* CoffeeScript-like function to bind the scope of a method to an instance,
* the context of that method, regardless from where it is called
*/
return function() {
return fn.apply(context, arguments);
};
};
// Cross-browser vanilla JS event handling
var addEventListener = function(element, type, callback) {
if (element.addEventListener) {
element.addEventListener(type, callback, false);
} else if (element.attachEvent) {
element.attachEvent('on' + type, callback);
}
};
var removeEventListener = function(element, type, callback) {
if (element.removeEventListener) {
element.removeEventListener(type, callback, false);
} else if (element.detachEvent) {
element.detachEvent('on' + type, callback);
}
};
var preventEventDefaults = function(e) {
if (!e) {
e = window.event;
}
if (e.preventDefault) {
e.preventDefault();
}
e.returnValue = false;
};
var stopEventPropagation = function(e) {
if (!e) {
e = window.event;
}
if (e.stopPropagation) {
e.stopPropagation();
}
e.cancelBubble = true;
};
var Cursor = {
/**
* Abstraction for making the combined mouse or touch position available at
* any time.
*
* It picks up the "move" events as an independent component and simply makes
* the latest x and y mouse/touch position of the user available at any time,
* which is requested with Cursor.x and Cursor.y respectively.
*
* It can receive both mouse and touch events consecutively, extracting the
* relevant meta data from each type of event.
*
* Cursor.refresh(e) is called to update the global x and y values, with a
* genuine MouseEvent or a TouchEvent from an event listener, e.g.
* mousedown/up or touchstart/end
*/
x: 0,
y: 0,
xDiff: 0,
yDiff: 0,
refresh: function(e) {
if (!e) {
e = window.event;
}
if (e.type == 'mousemove') {
this.set(e);
} else if (e.touches) {
this.set(e.touches[0]);
}
},
set: function(e) {
var lastX = this.x,
lastY = this.y;
if (e.clientX || e.clientY) {
this.x = e.clientX;
this.y = e.clientY;
} else if (e.pageX || e.pageY) {
this.x = e.pageX - document.body.scrollLeft - document.documentElement.scrollLeft;
this.y = e.pageY - document.body.scrollTop - document.documentElement.scrollTop;
}
this.xDiff = Math.abs(this.x - lastX);
this.yDiff = Math.abs(this.y - lastY);
}
};
var Position = {
/**
* Helper for extracting position of a DOM element, relative to the viewport
*
* The get(obj) method accepts a DOM element as the only parameter, and
* returns the position under a (x, y) tuple, as an array with two elements.
*/
get: function(obj) {
// Dragdealer relies on getBoundingClientRect to calculate element offsets,
// but we want to be sure we don't throw any unhandled exceptions and break
// other code from the page if running from in very old browser that doesn't
// support this method
var rect = {left: 0, top: 0};
if (obj.getBoundingClientRect !== undefined) {
rect = obj.getBoundingClientRect();
}
return [rect.left, rect.top];
}
};
var StylePrefix = {
transform: getPrefixedStylePropName('transform'),
perspective: getPrefixedStylePropName('perspective'),
backfaceVisibility: getPrefixedStylePropName('backfaceVisibility')
};
function getPrefixedStylePropName(propName) {
var domPrefixes = 'Webkit Moz ms O'.split(' '),
elStyle = document.documentElement.style;
if (elStyle[propName] !== undefined) return propName; // Is supported unprefixed
propName = propName.charAt(0).toUpperCase() + propName.substr(1);
for (var i = 0; i < domPrefixes.length; i++) {
if (elStyle[domPrefixes[i] + propName] !== undefined) {
return domPrefixes[i] + propName; // Is supported with prefix
}
}
};
function triggerWebkitHardwareAcceleration(element) {
if (StylePrefix.backfaceVisibility && StylePrefix.perspective) {
element.style[StylePrefix.perspective] = '1000px';
element.style[StylePrefix.backfaceVisibility] = 'hidden';
}
};
var vendors = ['webkit', 'moz'];
var requestAnimationFrame = window.requestAnimationFrame;
var cancelAnimationFrame = window.cancelAnimationFrame;
for (var x = 0; x < vendors.length && !requestAnimationFrame; ++x) {
requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!requestAnimationFrame) {
requestAnimationFrame = function (callback) {
return setTimeout(callback, 25);
};
cancelAnimationFrame = clearTimeout;
}
return Dragdealer;
}));