mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(toggle): Added dragging support to toggle switches
Merge pull request #706 from driftyco/wip-draggable-toggle
This commit is contained in:
48
js/ext/angular/src/directive/ionicToggle.js
vendored
48
js/ext/angular/src/directive/ionicToggle.js
vendored
@@ -5,7 +5,7 @@ angular.module('ionic.ui.toggle', [])
|
||||
|
||||
// The Toggle directive is a toggle switch that can be tapped to change
|
||||
// its value
|
||||
.directive('ionToggle', function() {
|
||||
.directive('ionToggle', ['$ionicGesture', '$timeout', function($ionicGesture, $timeout) {
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
@@ -36,30 +36,38 @@ angular.module('ionic.ui.toggle', [])
|
||||
if(attr.ngTrueValue) input.attr('ng-true-value', attr.ngTrueValue);
|
||||
if(attr.ngFalseValue) input.attr('ng-false-value', attr.ngFalseValue);
|
||||
|
||||
// return function link($scope, $element, $attr, ngModel) {
|
||||
// var el, checkbox, track, handle;
|
||||
return function($scope, $element, $attr) {
|
||||
var el, checkbox, track, handle;
|
||||
|
||||
// el = $element[0].getElementsByTagName('label')[0];
|
||||
// checkbox = el.children[0];
|
||||
// track = el.children[1];
|
||||
// handle = track.children[0];
|
||||
el = $element[0].getElementsByTagName('label')[0];
|
||||
checkbox = el.children[0];
|
||||
track = el.children[1];
|
||||
handle = track.children[0];
|
||||
|
||||
var ngModelController = angular.element(checkbox).controller('ngModel');
|
||||
|
||||
// $scope.toggle = new ionic.views.Toggle({
|
||||
// el: el,
|
||||
// track: track,
|
||||
// checkbox: checkbox,
|
||||
// handle: handle
|
||||
// });
|
||||
$scope.toggle = new ionic.views.Toggle({
|
||||
el: el,
|
||||
track: track,
|
||||
checkbox: checkbox,
|
||||
handle: handle,
|
||||
onChange: function() {
|
||||
if(checkbox.checked) {
|
||||
ngModelController.$setViewValue(true);
|
||||
} else {
|
||||
ngModelController.$setViewValue(false);
|
||||
}
|
||||
$scope.$apply();
|
||||
}
|
||||
});
|
||||
|
||||
// ionic.on('drag', function(e) {
|
||||
// console.log('drag');
|
||||
// $scope.toggle.drag(e);
|
||||
// }, handle);
|
||||
|
||||
// }
|
||||
$scope.$on('$destroy', function() {
|
||||
$scope.toggle.destroy();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
}]);
|
||||
|
||||
})(window.ionic);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
describe('Ionic Toggle', function() {
|
||||
var el, rootScope, compile;
|
||||
|
||||
beforeEach(module('ionic.ui.toggle'));
|
||||
beforeEach(module('ionic'));
|
||||
|
||||
beforeEach(inject(function($compile, $rootScope) {
|
||||
compile = $compile;
|
||||
@@ -9,7 +9,6 @@ describe('Ionic Toggle', function() {
|
||||
el = $compile('<ion-toggle ng-model="data.name"></ion-toggle>')($rootScope);
|
||||
}));
|
||||
|
||||
/*
|
||||
it('Should load', function() {
|
||||
var toggleView = el.isolateScope().toggle;
|
||||
expect(toggleView).not.toEqual(null);
|
||||
@@ -17,35 +16,56 @@ describe('Ionic Toggle', function() {
|
||||
expect(toggleView.handle).not.toEqual(null);
|
||||
});
|
||||
|
||||
it('Should toggle', function() {
|
||||
var toggle = el.isolateScope().toggle;
|
||||
expect(toggle.val()).toBe(false);
|
||||
el.click();
|
||||
expect(toggle.val()).toBe(true);
|
||||
el.click();
|
||||
expect(toggle.val()).toBe(false);
|
||||
it('Should destroy', function() {
|
||||
var toggleView = el.isolateScope().toggle;
|
||||
spyOn(toggleView, 'destroy');
|
||||
el.isolateScope().$destroy();
|
||||
expect(toggleView.destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should disable and enable', function() {
|
||||
|
||||
// Init with not disabled
|
||||
rootScope.data = { isDisabled: false };
|
||||
el = compile('<ion-toggle ng-model="data.name" ng-disabled="data.isDisabled"></ion-toggle>')(rootScope);
|
||||
|
||||
// Grab fields
|
||||
var label = el[0].querySelector('label');
|
||||
var toggle = el.isolateScope().toggle;
|
||||
var input = el[0].querySelector('input');
|
||||
|
||||
// Not disabled, we can toggle
|
||||
expect(toggle.val()).toBe(false);
|
||||
el.click();
|
||||
ionic.trigger('click', {target: label})
|
||||
expect(toggle.val()).toBe(true);
|
||||
|
||||
// Disable it
|
||||
rootScope.data.isDisabled = true;
|
||||
rootScope.$apply();
|
||||
expect(toggle.el.getAttribute('disabled')).toBe('disabled');
|
||||
el.click();
|
||||
expect(input.getAttribute('disabled')).toBe('disabled');
|
||||
|
||||
// We shouldn't be able to toggle it now
|
||||
ionic.trigger('click', {target: label})
|
||||
expect(toggle.val()).toBe(true);
|
||||
|
||||
// Re-enable it
|
||||
rootScope.data.isDisabled = false;
|
||||
rootScope.$apply();
|
||||
el.click();
|
||||
expect(toggle.el.getAttribute('disabled')).not.toBe('disabled');
|
||||
|
||||
// Should be able to toggle it now
|
||||
ionic.trigger('click', {target: label})
|
||||
expect(toggle.val()).toBe(false);
|
||||
expect(input.getAttribute('disabled')).not.toBe('disabled');
|
||||
});
|
||||
|
||||
it('Should toggle', function() {
|
||||
var toggle = el.isolateScope().toggle;
|
||||
var label = el[0].querySelector('label');
|
||||
expect(toggle.val()).toBe(false);
|
||||
ionic.trigger('click', {target: label})
|
||||
expect(toggle.val()).toBe(true);
|
||||
ionic.trigger('click', {target: label})
|
||||
expect(toggle.val()).toBe(false);
|
||||
});
|
||||
*/
|
||||
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<div class="list">
|
||||
<ion-toggle ng-model="myModel" ng-disabled="isDisabled">myModel ({{!!myModel}})</ion-toggle>
|
||||
<ion-toggle ng-model="catModel" ng-disabled="isDisabled" ng-true-value="cats" ng-false-value="dogs">Cats or dogs? ({{catModel}})</ion-toggle>
|
||||
<ion-toggle ng-model="isDisabled">Disable myModel ({{!!isDisabled}})</ion-toggle>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,7 +26,9 @@
|
||||
|
||||
<script>
|
||||
angular.module('toggleTest', ['ionic'])
|
||||
.controller('TestCtrl', function($scope) {});
|
||||
.controller('TestCtrl', function($scope) {
|
||||
$scope.catModel = 'dogs';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,11 +3,41 @@
|
||||
|
||||
ionic.views.Toggle = ionic.views.View.inherit({
|
||||
initialize: function(opts) {
|
||||
var self = this;
|
||||
|
||||
this.el = opts.el;
|
||||
this.checkbox = opts.checkbox;
|
||||
this.track = opts.track;
|
||||
this.handle = opts.handle;
|
||||
this.openPercent = -1;
|
||||
this.onChange = opts.onChange || function() {};
|
||||
|
||||
this.triggerThreshold = opts.triggerThreshold || 20;
|
||||
|
||||
this.dragStartHandler = function(e) {
|
||||
self.dragStart(e);
|
||||
};
|
||||
this.dragHandler = function(e) {
|
||||
self.drag(e);
|
||||
};
|
||||
this.holdHandler = function(e) {
|
||||
self.hold(e);
|
||||
};
|
||||
this.releaseHandler = function(e) {
|
||||
self.release(e);
|
||||
};
|
||||
|
||||
this.dragStartGesture = ionic.onGesture('dragstart', this.dragStartHandler, this.el);
|
||||
this.dragGesture = ionic.onGesture('drag', this.dragHandler, this.el);
|
||||
this.dragHoldGesture = ionic.onGesture('hold', this.holdHandler, this.el);
|
||||
this.dragReleaseGesture = ionic.onGesture('release', this.releaseHandler, this.el);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
ionic.offGesture(this.dragStartGesture, 'dragstart', this.dragStartGesture);
|
||||
ionic.offGesture(this.dragGesture, 'drag', this.dragGesture);
|
||||
ionic.offGesture(this.dragHoldGesture, 'hold', this.holdHandler);
|
||||
ionic.offGesture(this.dragReleaseGesture, 'release', this.releaseHandler);
|
||||
},
|
||||
|
||||
tap: function(e) {
|
||||
@@ -16,19 +46,71 @@
|
||||
}
|
||||
},
|
||||
|
||||
drag: function(e) {
|
||||
var slidePageLeft = this.track.offsetLeft + (this.handle.offsetWidth / 2);
|
||||
var slidePageRight = this.track.offsetLeft + this.track.offsetWidth - (this.handle.offsetWidth / 2);
|
||||
dragStart: function(e) {
|
||||
if(this.checkbox.disabled) return;
|
||||
|
||||
if(e.pageX >= slidePageRight - 4) {
|
||||
this.val(true);
|
||||
} else if(e.pageX <= slidePageLeft) {
|
||||
this.val(false);
|
||||
} else {
|
||||
this.setOpenPercent( Math.round( (1 - ((slidePageRight - e.pageX) / (slidePageRight - slidePageLeft) )) * 100) );
|
||||
}
|
||||
this._dragInfo = {
|
||||
width: this.el.offsetWidth,
|
||||
left: this.el.offsetLeft,
|
||||
right: this.el.offsetLeft + this.el.offsetWidth,
|
||||
triggerX: this.el.offsetWidth / 2,
|
||||
initialState: this.checkbox.checked
|
||||
};
|
||||
|
||||
// Stop any parent dragging
|
||||
e.gesture.srcEvent.preventDefault();
|
||||
|
||||
// Trigger hold styles
|
||||
this.hold(e);
|
||||
},
|
||||
|
||||
drag: function(e) {
|
||||
var self = this;
|
||||
if(!this._dragInfo) { return; }
|
||||
|
||||
// Stop any parent dragging
|
||||
e.gesture.srcEvent.preventDefault();
|
||||
|
||||
ionic.requestAnimationFrame(function(amount) {
|
||||
|
||||
var slidePageLeft = self.track.offsetLeft + (self.handle.offsetWidth / 2);
|
||||
var slidePageRight = self.track.offsetLeft + self.track.offsetWidth - (self.handle.offsetWidth / 2);
|
||||
var dx = e.gesture.deltaX;
|
||||
|
||||
var px = e.gesture.touches[0].pageX - self._dragInfo.left;
|
||||
var mx = self._dragInfo.width - self.triggerThreshold;
|
||||
|
||||
// The initial state was on, so "tend towards" on
|
||||
if(self._dragInfo.initialState) {
|
||||
if(px < self.triggerThreshold) {
|
||||
self.setOpenPercent(0);
|
||||
} else if(px > self._dragInfo.triggerX) {
|
||||
self.setOpenPercent(100);
|
||||
}
|
||||
} else {
|
||||
// The initial state was off, so "tend towards" off
|
||||
if(px < self._dragInfo.triggerX) {
|
||||
self.setOpenPercent(0);
|
||||
} else if(px > mx) {
|
||||
self.setOpenPercent(100);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
endDrag: function(e) {
|
||||
this._dragInfo = null;
|
||||
},
|
||||
|
||||
hold: function(e) {
|
||||
this.el.classList.add('dragging');
|
||||
},
|
||||
release: function(e) {
|
||||
this.el.classList.remove('dragging');
|
||||
this.endDrag(e);
|
||||
},
|
||||
|
||||
|
||||
setOpenPercent: function(openPercent) {
|
||||
// only make a change if the new open percent has changed
|
||||
if(this.openPercent < 0 || (openPercent < (this.openPercent - 3) || openPercent > (this.openPercent + 3) ) ) {
|
||||
@@ -46,10 +128,6 @@
|
||||
}
|
||||
},
|
||||
|
||||
release: function(e) {
|
||||
this.val( this.openPercent >= 50 );
|
||||
},
|
||||
|
||||
val: function(value) {
|
||||
if(value === true || value === false) {
|
||||
if(this.handle.style[ionic.CSS.TRANSFORM] !== "") {
|
||||
@@ -57,6 +135,7 @@
|
||||
}
|
||||
this.checkbox.checked = value;
|
||||
this.openPercent = (value ? 100 : 0);
|
||||
this.onChange && this.onChange();
|
||||
}
|
||||
return this.checkbox.checked;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
.toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: -$toggle-hit-area-expansion;
|
||||
padding: $toggle-hit-area-expansion;
|
||||
|
||||
&.dragging {
|
||||
.handle {
|
||||
background-color: $toggle-handle-dragging-bg-color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* hide the actual input checkbox */
|
||||
@@ -37,8 +45,8 @@
|
||||
.toggle .handle {
|
||||
@include transition($toggle-transition-duration ease-in-out);
|
||||
position: absolute;
|
||||
top: $toggle-border-width;
|
||||
left: $toggle-border-width;
|
||||
top: $toggle-border-width + $toggle-hit-area-expansion;
|
||||
left: $toggle-border-width + $toggle-hit-area-expansion;
|
||||
display: block;
|
||||
width: $toggle-handle-width;
|
||||
height: $toggle-handle-height;
|
||||
|
||||
@@ -419,6 +419,7 @@ $toggle-border-radius: 20px !default;
|
||||
$toggle-handle-width: $toggle-height - ($toggle-border-width * 2) !default;
|
||||
$toggle-handle-height: $toggle-handle-width !default;
|
||||
$toggle-handle-radius: 50% !default;
|
||||
$toggle-handle-dragging-bg-color: darken(#fff, 5%) !default;
|
||||
|
||||
$toggle-off-bg-color: #E5E5E5 !default;
|
||||
$toggle-off-border-color: #E5E5E5 !default;
|
||||
@@ -426,10 +427,13 @@ $toggle-off-border-color: #E5E5E5 !default;
|
||||
$toggle-on-bg-color: #4A87EE !default;
|
||||
$toggle-on-border-color: $toggle-on-bg-color !default;
|
||||
|
||||
|
||||
$toggle-handle-off-bg-color: $light !default;
|
||||
$toggle-handle-on-bg-color: $toggle-handle-off-bg-color !default;
|
||||
|
||||
$toggle-transition-duration: .1s !default;
|
||||
$toggle-transition-duration: .2s !default;
|
||||
|
||||
$toggle-hit-area-expansion: 5px;
|
||||
|
||||
|
||||
// Checkbox
|
||||
|
||||
30
test/unit/views/toggleView.unit.js
Normal file
30
test/unit/views/toggleView.unit.js
Normal file
@@ -0,0 +1,30 @@
|
||||
describe('Toggle view', function() {
|
||||
var element, toggle;
|
||||
|
||||
beforeEach(function() {
|
||||
element = $('<div class="item item-toggle disable-pointer-events">' +
|
||||
'<div>Cats</div>' +
|
||||
'<label class="toggle enable-pointer-events">' +
|
||||
'<input type="checkbox">' +
|
||||
'<div class="track disable-pointer-events">' +
|
||||
'<div class="handle"></div>' +
|
||||
'</div>' +
|
||||
'</label>' +
|
||||
'</div>');
|
||||
|
||||
el = element[0].getElementsByTagName('label')[0];
|
||||
checkbox = el.children[0];
|
||||
track = el.children[1];
|
||||
handle = track.children[0];
|
||||
toggle = new ionic.views.Toggle({
|
||||
el: el,
|
||||
checkbox: checkbox,
|
||||
track: track,
|
||||
handle: handle
|
||||
});
|
||||
});
|
||||
|
||||
it('Should init', function() {
|
||||
expect(toggle.el).not.toBe(undefined);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user