diff --git a/js/ext/angular/src/directive/ionicToggle.js b/js/ext/angular/src/directive/ionicToggle.js index 6c217027dad..ca410e4c141 100644 --- a/js/ext/angular/src/directive/ionicToggle.js +++ b/js/ext/angular/src/directive/ionicToggle.js @@ -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); diff --git a/js/ext/angular/test/directive/ionicToggle.unit.js b/js/ext/angular/test/directive/ionicToggle.unit.js index e36e1e330d3..d05a3d5b096 100644 --- a/js/ext/angular/test/directive/ionicToggle.unit.js +++ b/js/ext/angular/test/directive/ionicToggle.unit.js @@ -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('')($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('')(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); }); - */ }); diff --git a/js/ext/angular/test/toggle.html b/js/ext/angular/test/toggle.html index 058808adf02..d17479c96c6 100644 --- a/js/ext/angular/test/toggle.html +++ b/js/ext/angular/test/toggle.html @@ -18,6 +18,7 @@

Toggle

myModel ({{!!myModel}}) + Cats or dogs? ({{catModel}}) Disable myModel ({{!!isDisabled}})
@@ -25,7 +26,9 @@

Toggle

diff --git a/js/views/toggleView.js b/js/views/toggleView.js index cb238959116..468c598d18f 100644 --- a/js/views/toggleView.js +++ b/js/views/toggleView.js @@ -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 @@ } }, + dragStart: function(e) { + if(this.checkbox.disabled) return; + + 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 slidePageLeft = this.track.offsetLeft + (this.handle.offsetWidth / 2); - var slidePageRight = this.track.offsetLeft + this.track.offsetWidth - (this.handle.offsetWidth / 2); - - 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) ); - } + 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; } diff --git a/scss/_toggle.scss b/scss/_toggle.scss index 6076f39de95..7f8753ec3e8 100644 --- a/scss/_toggle.scss +++ b/scss/_toggle.scss @@ -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; diff --git a/scss/_variables.scss b/scss/_variables.scss index fa1ffbfd07f..13eba0e8c14 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -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 diff --git a/test/unit/views/toggleView.unit.js b/test/unit/views/toggleView.unit.js new file mode 100644 index 00000000000..65893086e03 --- /dev/null +++ b/test/unit/views/toggleView.unit.js @@ -0,0 +1,30 @@ +describe('Toggle view', function() { + var element, toggle; + + beforeEach(function() { + element = $('
' + + '
Cats
' + + '' + + '
'); + + 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); + }); +});