From 3d3eb7117017fbbad73a0b4ac8b44990f96202b0 Mon Sep 17 00:00:00 2001 From: Thomas Burleson Date: Fri, 26 Sep 2014 19:22:29 -0500 Subject: [PATCH 1/2] fix(slider): discrete sliders now support live dragging between discrete values and snap-to animate to closest discrete value. --- src/components/slider/slider.js | 81 +++++++++++++++++++++++++--- src/components/slider/slider.spec.js | 26 ++++----- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/src/components/slider/slider.js b/src/components/slider/slider.js index 3bd7990384..3f72090004 100644 --- a/src/components/slider/slider.js +++ b/src/components/slider/slider.js @@ -53,7 +53,6 @@ function SliderDirective() { '$element', '$attrs', '$$rAF', - '$timeout', '$window', '$materialEffects', '$aria', @@ -99,7 +98,7 @@ function SliderDirective() { * We use a controller for all the logic so that we can expose a few * things to unit tests */ -function SliderController(scope, element, attr, $$rAF, $timeout, $window, $materialEffects, $aria) { +function SliderController(scope, element, attr, $$rAF, $window, $materialEffects, $aria) { this.init = function init(ngModelCtrl) { var thumb = angular.element(element[0].querySelector('.slider-thumb')); @@ -136,6 +135,7 @@ function SliderController(scope, element, attr, $$rAF, $timeout, $window, $mater hammertime.on('hammer.input', onInput); hammertime.on('panstart', onPanStart); hammertime.on('pan', onPan); + hammertime.on('panend', onPanEnd); // On resize, recalculate the slider's dimensions and re-render var updateAll = $$rAF.debounce(function() { @@ -282,18 +282,25 @@ function SliderController(scope, element, attr, $$rAF, $timeout, $window, $mater * Slide listeners */ var isSliding = false; + var isDiscrete = angular.isDefined(attr.discrete); + function onInput(ev) { if (!isSliding && ev.eventType === Hammer.INPUT_START && !element[0].hasAttribute('disabled')) { isSliding = true; + element.addClass('active'); element[0].focus(); refreshSliderDimensions(); - doSlide(ev.center.x); + + onPan(ev); } else if (isSliding && ev.eventType === Hammer.INPUT_END) { + + if ( isDiscrete ) onPanEnd(ev); isSliding = false; + element.removeClass('panning active'); } } @@ -303,8 +310,33 @@ function SliderController(scope, element, attr, $$rAF, $timeout, $window, $mater } function onPan(ev) { if (!isSliding) return; - doSlide(ev.center.x); + + // While panning discrete, update only the + // visual positioning but not the model value. + + if ( isDiscrete ) doPan( ev.center.x ); + else doSlide( ev.center.x ); + ev.preventDefault(); + ev.srcEvent.stopPropagation(); + } + + function onPanEnd(ev) { + if ( isDiscrete ) { + // Convert exact to closest discrete value. + // Slide animate the thumb... and then update the model value. + + var exactVal = percentToValue( positionToPercent( ev.center.x )); + var closestVal = minMaxValidator( stepValidator(exactVal) ); + + setSliderPercent( valueToPercent(closestVal)); + $$rAF(function(){ + setModelValue( closestVal ); + }); + + ev.preventDefault(); + ev.srcEvent.stopPropagation(); + } } /** @@ -314,9 +346,44 @@ function SliderController(scope, element, attr, $$rAF, $timeout, $window, $mater this._onPanStart = onPanStart; this._onPan = onPan; - function doSlide(x) { - var percent = (x - sliderDimensions.left) / (sliderDimensions.width); - scope.$evalAsync(function() { setModelValue(min + percent * (max - min)); }); + /** + * Slide the UI by changing the model value + * @param x + */ + function doSlide( x ) { + scope.$evalAsync( function() { + setModelValue( percentToValue( positionToPercent(x) )); + }); + } + + /** + * Slide the UI without changing the model (while dragging/panning) + * @param x + */ + function doPan( x ) { + setSliderPercent( positionToPercent(x) ); + } + + /** + * Convert horizontal position on slider to percentage value of offset from beginning... + * @param x + * @returns {number} + */ + function positionToPercent( x ) { + return (x - sliderDimensions.left) / (sliderDimensions.width); + } + + /** + * Convert percentage offset on slide to equivalent model value + * @param percent + * @returns {*} + */ + function percentToValue( percent ) { + return (min + percent * (max - min)); + } + + function valueToPercent( val ) { + return (val - min)/(max - min); } }; diff --git a/src/components/slider/slider.spec.js b/src/components/slider/slider.spec.js index 6cf1f245aa..c3de0b1763 100644 --- a/src/components/slider/slider.spec.js +++ b/src/components/slider/slider.spec.js @@ -1,6 +1,17 @@ describe('material-slider', function() { + function simulateEventAt( centerX, eventType ) { + return { + eventType: eventType, + center: { x: centerX }, + preventDefault: angular.noop, + srcEvent : { + stopPropagation : angular.noop + } + }; + } + beforeEach(module('material.components.slider','material.decorators')); it('should set model on press', inject(function($compile, $rootScope, $timeout) { @@ -14,25 +25,16 @@ describe('material-slider', function() { right: 0 }); - sliderCtrl._onInput({ - eventType: Hammer.INPUT_START, - center: { x: 30 } - }); + sliderCtrl._onInput( simulateEventAt( 30, Hammer.INPUT_START )); $timeout.flush(); expect($rootScope.value).toBe(30); //When going past max, it should clamp to max - sliderCtrl._onPan({ - center: { x: 500 }, - preventDefault: angular.noop - }); + sliderCtrl._onPan( simulateEventAt( 500 )); $timeout.flush(); expect($rootScope.value).toBe(100); - sliderCtrl._onPan({ - center: { x: 50 }, - preventDefault: angular.noop - }); + sliderCtrl._onPan( simulateEventAt( 50 )); $timeout.flush(); expect($rootScope.value).toBe(50); })); From 16a31f3b3ab707533a62ee8066755d3879754900 Mon Sep 17 00:00:00 2001 From: Thomas Burleson Date: Tue, 30 Sep 2014 08:33:32 -0500 Subject: [PATCH 2/2] rename doPan() to adjustThumbPosition() for clarity --- src/components/slider/slider.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/slider/slider.js b/src/components/slider/slider.js index 3f72090004..53e49f9ee1 100644 --- a/src/components/slider/slider.js +++ b/src/components/slider/slider.js @@ -314,7 +314,7 @@ function SliderController(scope, element, attr, $$rAF, $window, $materialEffects // While panning discrete, update only the // visual positioning but not the model value. - if ( isDiscrete ) doPan( ev.center.x ); + if ( isDiscrete ) adjustThumbPosition( ev.center.x ); else doSlide( ev.center.x ); ev.preventDefault(); @@ -360,7 +360,7 @@ function SliderController(scope, element, attr, $$rAF, $window, $materialEffects * Slide the UI without changing the model (while dragging/panning) * @param x */ - function doPan( x ) { + function adjustThumbPosition( x ) { setSliderPercent( positionToPercent(x) ); }