Skip to content

Commit cc15a5b

Browse files
committed
feat(toggle): Added dragging support to toggle switches
Merge pull request #706 from driftyco/wip-draggable-toggle
2 parents cb5510c + 739292d commit cc15a5b

File tree

7 files changed

+205
-53
lines changed

7 files changed

+205
-53
lines changed

js/ext/angular/src/directive/ionicToggle.js

+28-20
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ angular.module('ionic.ui.toggle', [])
55

66
// The Toggle directive is a toggle switch that can be tapped to change
77
// its value
8-
.directive('ionToggle', function() {
8+
.directive('ionToggle', ['$ionicGesture', '$timeout', function($ionicGesture, $timeout) {
99

1010
return {
1111
restrict: 'E',
@@ -36,30 +36,38 @@ angular.module('ionic.ui.toggle', [])
3636
if(attr.ngTrueValue) input.attr('ng-true-value', attr.ngTrueValue);
3737
if(attr.ngFalseValue) input.attr('ng-false-value', attr.ngFalseValue);
3838

39-
// return function link($scope, $element, $attr, ngModel) {
40-
// var el, checkbox, track, handle;
39+
return function($scope, $element, $attr) {
40+
var el, checkbox, track, handle;
4141

42-
// el = $element[0].getElementsByTagName('label')[0];
43-
// checkbox = el.children[0];
44-
// track = el.children[1];
45-
// handle = track.children[0];
42+
el = $element[0].getElementsByTagName('label')[0];
43+
checkbox = el.children[0];
44+
track = el.children[1];
45+
handle = track.children[0];
46+
47+
var ngModelController = angular.element(checkbox).controller('ngModel');
4648

47-
// $scope.toggle = new ionic.views.Toggle({
48-
// el: el,
49-
// track: track,
50-
// checkbox: checkbox,
51-
// handle: handle
52-
// });
49+
$scope.toggle = new ionic.views.Toggle({
50+
el: el,
51+
track: track,
52+
checkbox: checkbox,
53+
handle: handle,
54+
onChange: function() {
55+
if(checkbox.checked) {
56+
ngModelController.$setViewValue(true);
57+
} else {
58+
ngModelController.$setViewValue(false);
59+
}
60+
$scope.$apply();
61+
}
62+
});
5363

54-
// ionic.on('drag', function(e) {
55-
// console.log('drag');
56-
// $scope.toggle.drag(e);
57-
// }, handle);
58-
59-
// }
64+
$scope.$on('$destroy', function() {
65+
$scope.toggle.destroy();
66+
});
67+
};
6068
}
6169

6270
};
63-
});
71+
}]);
6472

6573
})(window.ionic);
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,71 @@
11
describe('Ionic Toggle', function() {
22
var el, rootScope, compile;
33

4-
beforeEach(module('ionic.ui.toggle'));
4+
beforeEach(module('ionic'));
55

66
beforeEach(inject(function($compile, $rootScope) {
77
compile = $compile;
88
rootScope = $rootScope;
99
el = $compile('<ion-toggle ng-model="data.name"></ion-toggle>')($rootScope);
1010
}));
1111

12-
/*
1312
it('Should load', function() {
1413
var toggleView = el.isolateScope().toggle;
1514
expect(toggleView).not.toEqual(null);
1615
expect(toggleView.checkbox).not.toEqual(null);
1716
expect(toggleView.handle).not.toEqual(null);
1817
});
1918

20-
it('Should toggle', function() {
21-
var toggle = el.isolateScope().toggle;
22-
expect(toggle.val()).toBe(false);
23-
el.click();
24-
expect(toggle.val()).toBe(true);
25-
el.click();
26-
expect(toggle.val()).toBe(false);
19+
it('Should destroy', function() {
20+
var toggleView = el.isolateScope().toggle;
21+
spyOn(toggleView, 'destroy');
22+
el.isolateScope().$destroy();
23+
expect(toggleView.destroy).toHaveBeenCalled();
2724
});
2825

2926
it('Should disable and enable', function() {
3027

28+
// Init with not disabled
3129
rootScope.data = { isDisabled: false };
3230
el = compile('<ion-toggle ng-model="data.name" ng-disabled="data.isDisabled"></ion-toggle>')(rootScope);
31+
32+
// Grab fields
33+
var label = el[0].querySelector('label');
3334
var toggle = el.isolateScope().toggle;
35+
var input = el[0].querySelector('input');
36+
37+
// Not disabled, we can toggle
3438
expect(toggle.val()).toBe(false);
35-
el.click();
39+
ionic.trigger('click', {target: label})
3640
expect(toggle.val()).toBe(true);
3741

42+
// Disable it
3843
rootScope.data.isDisabled = true;
3944
rootScope.$apply();
40-
expect(toggle.el.getAttribute('disabled')).toBe('disabled');
41-
el.click();
45+
expect(input.getAttribute('disabled')).toBe('disabled');
46+
47+
// We shouldn't be able to toggle it now
48+
ionic.trigger('click', {target: label})
4249
expect(toggle.val()).toBe(true);
4350

51+
// Re-enable it
4452
rootScope.data.isDisabled = false;
4553
rootScope.$apply();
46-
el.click();
47-
expect(toggle.el.getAttribute('disabled')).not.toBe('disabled');
54+
55+
// Should be able to toggle it now
56+
ionic.trigger('click', {target: label})
57+
expect(toggle.val()).toBe(false);
58+
expect(input.getAttribute('disabled')).not.toBe('disabled');
59+
});
60+
61+
it('Should toggle', function() {
62+
var toggle = el.isolateScope().toggle;
63+
var label = el[0].querySelector('label');
64+
expect(toggle.val()).toBe(false);
65+
ionic.trigger('click', {target: label})
66+
expect(toggle.val()).toBe(true);
67+
ionic.trigger('click', {target: label})
68+
expect(toggle.val()).toBe(false);
4869
});
49-
*/
5070

5171
});

js/ext/angular/test/toggle.html

+4-1
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,17 @@ <h1 class="title">Toggle</h1>
1818

1919
<div class="list">
2020
<ion-toggle ng-model="myModel" ng-disabled="isDisabled">myModel ({{!!myModel}})</ion-toggle>
21+
<ion-toggle ng-model="catModel" ng-disabled="isDisabled" ng-true-value="cats" ng-false-value="dogs">Cats or dogs? ({{catModel}})</ion-toggle>
2122
<ion-toggle ng-model="isDisabled">Disable myModel ({{!!isDisabled}})</ion-toggle>
2223
</div>
2324
</div>
2425
</ion-content>
2526

2627
<script>
2728
angular.module('toggleTest', ['ionic'])
28-
.controller('TestCtrl', function($scope) {});
29+
.controller('TestCtrl', function($scope) {
30+
$scope.catModel = 'dogs';
31+
});
2932
</script>
3033
</body>
3134
</html>

js/views/toggleView.js

+93-14
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,41 @@
33

44
ionic.views.Toggle = ionic.views.View.inherit({
55
initialize: function(opts) {
6+
var self = this;
7+
68
this.el = opts.el;
79
this.checkbox = opts.checkbox;
810
this.track = opts.track;
911
this.handle = opts.handle;
1012
this.openPercent = -1;
13+
this.onChange = opts.onChange || function() {};
14+
15+
this.triggerThreshold = opts.triggerThreshold || 20;
16+
17+
this.dragStartHandler = function(e) {
18+
self.dragStart(e);
19+
};
20+
this.dragHandler = function(e) {
21+
self.drag(e);
22+
};
23+
this.holdHandler = function(e) {
24+
self.hold(e);
25+
};
26+
this.releaseHandler = function(e) {
27+
self.release(e);
28+
};
29+
30+
this.dragStartGesture = ionic.onGesture('dragstart', this.dragStartHandler, this.el);
31+
this.dragGesture = ionic.onGesture('drag', this.dragHandler, this.el);
32+
this.dragHoldGesture = ionic.onGesture('hold', this.holdHandler, this.el);
33+
this.dragReleaseGesture = ionic.onGesture('release', this.releaseHandler, this.el);
34+
},
35+
36+
destroy: function() {
37+
ionic.offGesture(this.dragStartGesture, 'dragstart', this.dragStartGesture);
38+
ionic.offGesture(this.dragGesture, 'drag', this.dragGesture);
39+
ionic.offGesture(this.dragHoldGesture, 'hold', this.holdHandler);
40+
ionic.offGesture(this.dragReleaseGesture, 'release', this.releaseHandler);
1141
},
1242

1343
tap: function(e) {
@@ -16,19 +46,71 @@
1646
}
1747
},
1848

49+
dragStart: function(e) {
50+
if(this.checkbox.disabled) return;
51+
52+
this._dragInfo = {
53+
width: this.el.offsetWidth,
54+
left: this.el.offsetLeft,
55+
right: this.el.offsetLeft + this.el.offsetWidth,
56+
triggerX: this.el.offsetWidth / 2,
57+
initialState: this.checkbox.checked
58+
};
59+
60+
// Stop any parent dragging
61+
e.gesture.srcEvent.preventDefault();
62+
63+
// Trigger hold styles
64+
this.hold(e);
65+
},
66+
1967
drag: function(e) {
20-
var slidePageLeft = this.track.offsetLeft + (this.handle.offsetWidth / 2);
21-
var slidePageRight = this.track.offsetLeft + this.track.offsetWidth - (this.handle.offsetWidth / 2);
22-
23-
if(e.pageX >= slidePageRight - 4) {
24-
this.val(true);
25-
} else if(e.pageX <= slidePageLeft) {
26-
this.val(false);
27-
} else {
28-
this.setOpenPercent( Math.round( (1 - ((slidePageRight - e.pageX) / (slidePageRight - slidePageLeft) )) * 100) );
29-
}
68+
var self = this;
69+
if(!this._dragInfo) { return; }
70+
71+
// Stop any parent dragging
72+
e.gesture.srcEvent.preventDefault();
73+
74+
ionic.requestAnimationFrame(function(amount) {
75+
76+
var slidePageLeft = self.track.offsetLeft + (self.handle.offsetWidth / 2);
77+
var slidePageRight = self.track.offsetLeft + self.track.offsetWidth - (self.handle.offsetWidth / 2);
78+
var dx = e.gesture.deltaX;
79+
80+
var px = e.gesture.touches[0].pageX - self._dragInfo.left;
81+
var mx = self._dragInfo.width - self.triggerThreshold;
82+
83+
// The initial state was on, so "tend towards" on
84+
if(self._dragInfo.initialState) {
85+
if(px < self.triggerThreshold) {
86+
self.setOpenPercent(0);
87+
} else if(px > self._dragInfo.triggerX) {
88+
self.setOpenPercent(100);
89+
}
90+
} else {
91+
// The initial state was off, so "tend towards" off
92+
if(px < self._dragInfo.triggerX) {
93+
self.setOpenPercent(0);
94+
} else if(px > mx) {
95+
self.setOpenPercent(100);
96+
}
97+
}
98+
});
99+
},
100+
101+
endDrag: function(e) {
102+
this._dragInfo = null;
103+
},
104+
105+
hold: function(e) {
106+
this.el.classList.add('dragging');
107+
},
108+
release: function(e) {
109+
this.el.classList.remove('dragging');
110+
this.endDrag(e);
30111
},
31112

113+
32114
setOpenPercent: function(openPercent) {
33115
// only make a change if the new open percent has changed
34116
if(this.openPercent < 0 || (openPercent < (this.openPercent - 3) || openPercent > (this.openPercent + 3) ) ) {
@@ -46,17 +128,14 @@
46128
}
47129
},
48130

49-
release: function(e) {
50-
this.val( this.openPercent >= 50 );
51-
},
52-
53131
val: function(value) {
54132
if(value === true || value === false) {
55133
if(this.handle.style[ionic.CSS.TRANSFORM] !== "") {
56134
this.handle.style[ionic.CSS.TRANSFORM] = "";
57135
}
58136
this.checkbox.checked = value;
59137
this.openPercent = (value ? 100 : 0);
138+
this.onChange && this.onChange();
60139
}
61140
return this.checkbox.checked;
62141
}

scss/_toggle.scss

+10-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
.toggle {
99
position: relative;
1010
display: inline-block;
11+
margin: -$toggle-hit-area-expansion;
12+
padding: $toggle-hit-area-expansion;
13+
14+
&.dragging {
15+
.handle {
16+
background-color: $toggle-handle-dragging-bg-color !important;
17+
}
18+
}
1119
}
1220

1321
/* hide the actual input checkbox */
@@ -37,8 +45,8 @@
3745
.toggle .handle {
3846
@include transition($toggle-transition-duration ease-in-out);
3947
position: absolute;
40-
top: $toggle-border-width;
41-
left: $toggle-border-width;
48+
top: $toggle-border-width + $toggle-hit-area-expansion;
49+
left: $toggle-border-width + $toggle-hit-area-expansion;
4250
display: block;
4351
width: $toggle-handle-width;
4452
height: $toggle-handle-height;

scss/_variables.scss

+5-1
Original file line numberDiff line numberDiff line change
@@ -419,17 +419,21 @@ $toggle-border-radius: 20px !default;
419419
$toggle-handle-width: $toggle-height - ($toggle-border-width * 2) !default;
420420
$toggle-handle-height: $toggle-handle-width !default;
421421
$toggle-handle-radius: 50% !default;
422+
$toggle-handle-dragging-bg-color: darken(#fff, 5%) !default;
422423

423424
$toggle-off-bg-color: #E5E5E5 !default;
424425
$toggle-off-border-color: #E5E5E5 !default;
425426

426427
$toggle-on-bg-color: #4A87EE !default;
427428
$toggle-on-border-color: $toggle-on-bg-color !default;
428429

430+
429431
$toggle-handle-off-bg-color: $light !default;
430432
$toggle-handle-on-bg-color: $toggle-handle-off-bg-color !default;
431433

432-
$toggle-transition-duration: .1s !default;
434+
$toggle-transition-duration: .2s !default;
435+
436+
$toggle-hit-area-expansion: 5px;
433437

434438

435439
// Checkbox

test/unit/views/toggleView.unit.js

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
describe('Toggle view', function() {
2+
var element, toggle;
3+
4+
beforeEach(function() {
5+
element = $('<div class="item item-toggle disable-pointer-events">' +
6+
'<div>Cats</div>' +
7+
'<label class="toggle enable-pointer-events">' +
8+
'<input type="checkbox">' +
9+
'<div class="track disable-pointer-events">' +
10+
'<div class="handle"></div>' +
11+
'</div>' +
12+
'</label>' +
13+
'</div>');
14+
15+
el = element[0].getElementsByTagName('label')[0];
16+
checkbox = el.children[0];
17+
track = el.children[1];
18+
handle = track.children[0];
19+
toggle = new ionic.views.Toggle({
20+
el: el,
21+
checkbox: checkbox,
22+
track: track,
23+
handle: handle
24+
});
25+
});
26+
27+
it('Should init', function() {
28+
expect(toggle.el).not.toBe(undefined);
29+
});
30+
});

0 commit comments

Comments
 (0)