├── .gitignore ├── README.md ├── angular-ranger.css ├── angular-ranger.js ├── angular-ranger.min.css ├── angular-ranger.min.js ├── bower.json ├── gulpfile.js ├── index.html ├── index.js ├── package.json └── src ├── angular-ranger.html ├── angular-ranger.js ├── angular-ranger.scss └── lib └── PointerDraw.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-ranger 2 | ===================== 3 | 4 | A mobile friendly, *super-fast*, range slider. No jQuery necessary... 5 | 6 | [**Check out a demo**](http://justmaier.github.io/angular-ranger/#demo) 7 | 8 | ## Installation 9 | 10 | #### Bower 11 | `bower install angular-ranger` 12 | 13 | #### Nuget 14 | `install-package AngularJs.Ranger` 15 | 16 | #### Manually 17 | ```html 18 | 19 | 20 | ``` 21 | 22 | ## Usage 23 | 24 | 0. Install `angular-ranger` using one of the methods above. 25 | 1. Add `angular-ranger` as a module dependency to your app 26 | 2. Drop a ranger into your html 27 | 28 | #### Javascript 29 | ```javascript 30 | angular.module('app', ['angular-ranger']) 31 | .run(function($rootScope){ 32 | $rootScope.value = { 33 | min: 5, 34 | max: 18, 35 | value: 12 36 | }; 37 | }); 38 | ``` 39 | 40 | #### Html 41 | ```html 42 | 43 | 44 | 45 | 46 | 47 | ``` 48 | 49 | ## Notes 50 | 51 | - Angular-Ranger uses Sass to make the design highly customizable. Check the sass file for all of the available options. 52 | - Angular-Ranger includes a simple gulpfile so that you can make your own adjustments and build/re-minify using `gulp build`. Keep in mind you will need to install the dev dependencies first using `npm install`. 53 | - This has been tested on Windows, Windows Phone, and iOS. Let me know if you run into any bugs. 54 | - Pull requests are always welcome 55 | -------------------------------------------------------------------------------- /angular-ranger.css: -------------------------------------------------------------------------------- 1 | .ranger { 2 | -webkit-box-sizing: border-box; 3 | box-sizing: border-box; 4 | background: #ddd; 5 | height: 50px; 6 | padding: 0 30px; 7 | border-radius: 6px; } 8 | .ranger-scale { 9 | width: 100%; 10 | height: 100%; 11 | position: relative; 12 | padding: 21.5px 0; } 13 | .ranger-scale:after { 14 | content: ""; 15 | position: absolute; 16 | width: 100%; 17 | height: 7px; 18 | border: 1px solid #444; 19 | border-radius: 3px; } 20 | .ranger-marker { 21 | position: absolute; 22 | width: 30px; 23 | height: 30px; 24 | border-radius: 15px; 25 | top: 50%; 26 | -webkit-transform: translate(-50%, -50%); 27 | -ms-transform: translate(-50%, -50%); 28 | transform: translate(-50%, -50%); 29 | background: #444; 30 | z-index: 2; } 31 | .ranger-marker:last-child { 32 | left: 100%; 33 | -webkit-transform: translate(-50%, -50%); 34 | -ms-transform: translate(-50%, -50%); 35 | transform: translate(-50%, -50%); } 36 | .ranger-marker:after { 37 | display: none; 38 | content: attr(marker-value); 39 | position: absolute; 40 | left: 50%; 41 | margin-left: -15px; 42 | top: -39px; 43 | background: #444; 44 | color: #fff; 45 | width: 30px; 46 | height: 30px; 47 | line-height: 30px; 48 | text-align: center; 49 | border-radius: 50% 50% 50% 0; 50 | -webkit-transform: rotate(-45deg); 51 | -ms-transform: rotate(-45deg); 52 | transform: rotate(-45deg); } 53 | .ranger-marker:focus { 54 | outline: none; } 55 | .ranger-marker:focus:after, .ranger-marker:focus:before { 56 | display: block; } 57 | .ranger-fill { 58 | position: absolute; 59 | top: 21.5px; 60 | bottom: 21.5px; 61 | background: #444; } 62 | -------------------------------------------------------------------------------- /angular-ranger.js: -------------------------------------------------------------------------------- 1 | // this javascript function abstracts mouse, pointer, and touch events 2 | // 3 | // invoke with: 4 | // target - the HTML element object which is the target of the drawing 5 | // startDraw - a function called with four parameters (target, pointerId, x, y) when the drawing begins. x and y are guaranteed to be within target's rectange 6 | // extendDraw - a function called with four parameters (target, pointerId, x, y) when the drawing is extended. x and y are guaranteed to be within target's rectange 7 | // endDraw - a function called with two parameters (target, pointerId) when the drawing is ended 8 | // logMessage - a function called with one parameter (string message) that can be logged as the caller desires. multiple line strings separated by \n may be sent 9 | // 10 | // all parameters expect target are optional 11 | // 12 | // target element cannot move within the document during drawing 13 | // 14 | function PointerDraw(target, startDraw, extendDraw, endDraw, logMessage) { 15 | 16 | // an object to keep track of the last x/y positions of the mouse/pointer/touch point 17 | // used to reject redundant moves and as a flag to determine if we're in the "down" state 18 | var lastXYById = {}; 19 | 20 | // an audit function to see if we're keeping lastXYById clean 21 | if (logMessage) { 22 | window.setInterval(function () { 23 | var logthis = false; 24 | var msg = "Current pointerId array contains:"; 25 | 26 | for (var key in lastXYById) { 27 | logthis = true; 28 | msg += " " + key; 29 | } 30 | 31 | if (logthis) { 32 | logMessage(msg); 33 | } 34 | }, 1000); 35 | } 36 | 37 | // Opera doesn't have Object.keys so we use this wrapper 38 | function NumberOfKeys(theObject) { 39 | if (Object.keys) 40 | return Object.keys(theObject).length; 41 | 42 | var n = 0; 43 | for (var key in theObject) 44 | ++n; 45 | 46 | return n; 47 | } 48 | 49 | // IE10's implementation in the Windows Developer Preview requires doing all of this 50 | // Not all of these methods remain in the Windows Consumer Preview, hence the tests for method existence. 51 | function PreventDefaultManipulationAndMouseEvent(evtObj) { 52 | if (evtObj.preventDefault) 53 | evtObj.preventDefault(); 54 | 55 | if (evtObj.preventManipulation) 56 | evtObj.preventManipulation(); 57 | 58 | if (evtObj.preventMouseEvent) 59 | evtObj.preventMouseEvent(); 60 | } 61 | 62 | // we send target-relative coordinates to the draw functions 63 | // this calculates the delta needed to convert pageX/Y to offsetX/Y because offsetX/Y don't exist in the TouchEvent object or in Firefox's MouseEvent object 64 | function ComputeDocumentToElementDelta(theElement) { 65 | var elementLeft = 0; 66 | var elementTop = 0; 67 | 68 | for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) { 69 | // the following is a major hack for versions of IE less than 8 to avoid an apparent problem on the IEBlog with double-counting the offsets 70 | // this may not be a general solution to IE7's problem with offsetLeft/offsetParent 71 | if (navigator.userAgent.match(/\bMSIE\b/) && (!document.documentMode || document.documentMode < 8) && offsetElement.currentStyle.position == "relative" && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position == "relative" && offsetElement.offsetLeft == offsetElement.offsetParent.offsetLeft) { 72 | // add only the top 73 | elementTop += offsetElement.offsetTop; 74 | } 75 | else { 76 | elementLeft += offsetElement.offsetLeft; 77 | elementTop += offsetElement.offsetTop; 78 | } 79 | } 80 | 81 | return { x: elementLeft, y: elementTop }; 82 | } 83 | 84 | // function needed because IE versions before 9 did not define pageX/Y in the MouseEvent object 85 | function EnsurePageXY(eventObj) { 86 | if (typeof eventObj.pageX == 'undefined') { 87 | // initialize assuming our source element is our target 88 | eventObj.pageX = eventObj.offsetX + documentToTargetDelta.x; 89 | eventObj.pageY = eventObj.offsetY + documentToTargetDelta.y; 90 | 91 | if (eventObj.srcElement.offsetParent == target && document.documentMode && document.documentMode == 8 && eventObj.type == "mousedown") { 92 | // source element is a child piece of VML, we're in IE8, and we've not called setCapture yet - add the origin of the source element 93 | eventObj.pageX += eventObj.srcElement.offsetLeft; 94 | eventObj.pageY += eventObj.srcElement.offsetTop; 95 | } 96 | else if (eventObj.srcElement != target && !document.documentMode || document.documentMode < 8) { 97 | // source element isn't the target (most likely it's a child piece of VML) and we're in a version of IE before IE8 - 98 | // the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents 99 | // to get the document-relative coordinates (the same as pageX/Y) 100 | var sx = -2, sy = -2; // adjust for old IE's 2-pixel border 101 | for (var scrollElement = eventObj.srcElement; scrollElement != null; scrollElement = scrollElement.parentNode) { 102 | sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0; 103 | sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0; 104 | } 105 | 106 | eventObj.pageX = eventObj.clientX + sx; 107 | eventObj.pageY = eventObj.clientY + sy; 108 | } 109 | } 110 | } 111 | 112 | // cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart) 113 | var documentToTargetDelta = ComputeDocumentToElementDelta(target); 114 | 115 | // functions to convert document-relative coordinates to target-relative and constrain them to be within the target 116 | function targetRelativeX(px) { return Math.max(0, Math.min(px - documentToTargetDelta.x, target.offsetWidth)); }; 117 | function targetRelativeY(py) { return Math.max(0, Math.min(py - documentToTargetDelta.y, target.offsetHeight)); }; 118 | 119 | // common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events 120 | function DoEvent(theEvtObj) { 121 | 122 | // optimize rejecting mouse moves when mouse is up 123 | if (theEvtObj.type == "mousemove" && NumberOfKeys(lastXYById) == 0) 124 | return; 125 | 126 | PreventDefaultManipulationAndMouseEvent(theEvtObj); 127 | 128 | var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj]; 129 | for (var i = 0; i < pointerList.length; ++i) { 130 | var pointerObj = pointerList[i]; 131 | var pointerId = (typeof pointerObj.identifier != 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId != 'undefined') ? pointerObj.pointerId : 1; 132 | 133 | // use the pageX/Y coordinates to compute target-relative coordinates when we have them (in ie < 9, we need to do a little work to put them there) 134 | EnsurePageXY(pointerObj); 135 | var pageX = pointerObj.pageX; 136 | var pageY = pointerObj.pageY; 137 | 138 | if (theEvtObj.type.match(/(start|down)$/i)) { 139 | // clause for processing MSPointerDown, touchstart, and mousedown 140 | 141 | // refresh the document-to-target delta on start in case the target has moved relative to document 142 | documentToTargetDelta = ComputeDocumentToElementDelta(target); 143 | 144 | // protect against failing to get an up or end on this pointerId 145 | if (lastXYById[pointerId]) { 146 | if (endDraw) 147 | endDraw(target, pointerId); 148 | delete lastXYById[pointerId]; 149 | if (logMessage) 150 | logMessage("Ended draw on pointer " + pointerId + " in " + theEvtObj.type); 151 | } 152 | 153 | if (startDraw) 154 | startDraw(target, pointerId, targetRelativeX(pageX), targetRelativeY(pageY)); 155 | 156 | // init last page positions for this pointer 157 | lastXYById[pointerId] = { x: pageX, y: pageY }; 158 | 159 | // in the Microsoft pointer model, set the capture for this pointer 160 | // in the mouse model, set the capture or add a document-level event handlers if this is our first down point 161 | // nothing is required for the iOS touch model because capture is implied on touchstart 162 | if (target.msSetPointerCapture) 163 | target.msSetPointerCapture(pointerId); 164 | else if (theEvtObj.type == "mousedown" && NumberOfKeys(lastXYById) == 1) { 165 | if (useSetReleaseCapture) 166 | target.setCapture(true); 167 | else { 168 | document.addEventListener("mousemove", DoEvent, false); 169 | document.addEventListener("mouseup", DoEvent, false); 170 | } 171 | } 172 | } 173 | else if (theEvtObj.type.match(/move$/i)) { 174 | // clause handles mousemove, MSPointerMove, and touchmove 175 | 176 | if (lastXYById[pointerId] && !(lastXYById[pointerId].x == pageX && lastXYById[pointerId].y == pageY)) { 177 | // only extend if the pointer is down and it's not the same as the last point 178 | 179 | if (extendDraw) 180 | extendDraw(target, pointerId, targetRelativeX(pageX), targetRelativeY(pageY)); 181 | 182 | // update last page positions for this pointer 183 | lastXYById[pointerId].x = pageX; 184 | lastXYById[pointerId].y = pageY; 185 | } 186 | } 187 | else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) { 188 | // clause handles up/end/cancel 189 | 190 | if (endDraw) 191 | endDraw(target, pointerId); 192 | 193 | // delete last page positions for this pointer 194 | delete lastXYById[pointerId]; 195 | 196 | // in the Microsoft pointer model, release the capture for this pointer 197 | // in the mouse model, release the capture or remove document-level event handlers if there are no down points 198 | // nothing is required for the iOS touch model because capture is implied on touchstart 199 | if (target.msReleasePointerCapture) 200 | target.msReleasePointerCapture(pointerId); 201 | else if (theEvtObj.type == "mouseup" && NumberOfKeys(lastXYById) == 0) { 202 | if (useSetReleaseCapture) 203 | target.releaseCapture(); 204 | else { 205 | document.removeEventListener("mousemove", DoEvent, false); 206 | document.removeEventListener("mouseup", DoEvent, false); 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | var useSetReleaseCapture = false; 214 | 215 | if (window.navigator.msPointerEnabled) { 216 | // Microsoft pointer model 217 | target.addEventListener("MSPointerDown", DoEvent, false); 218 | target.addEventListener("MSPointerMove", DoEvent, false); 219 | target.addEventListener("MSPointerUp", DoEvent, false); 220 | target.addEventListener("MSPointerCancel", DoEvent, false); 221 | 222 | // css way to prevent panning in our target area 223 | if (typeof target.style.msContentZooming != 'undefined') 224 | target.style.msContentZooming = "none"; 225 | 226 | // new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target 227 | // without this, you cannot touch draw on the element because IE will intercept the touch events 228 | if (typeof target.style.msTouchAction != 'undefined') 229 | target.style.msTouchAction = "none"; 230 | 231 | if (logMessage) 232 | logMessage("Using Microsoft pointer model"); 233 | } 234 | else if (target.addEventListener) { 235 | // iOS touch model 236 | target.addEventListener("touchstart", DoEvent, false); 237 | target.addEventListener("touchmove", DoEvent, false); 238 | target.addEventListener("touchend", DoEvent, false); 239 | target.addEventListener("touchcancel", DoEvent, false); 240 | 241 | // mouse model 242 | target.addEventListener("mousedown", DoEvent, false); 243 | 244 | // mouse model with capture 245 | // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target 246 | if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) { 247 | useSetReleaseCapture = true; 248 | 249 | target.addEventListener("mousemove", DoEvent, false); 250 | target.addEventListener("mouseup", DoEvent, false); 251 | 252 | if (logMessage) 253 | logMessage("Using mouse model with capture"); 254 | } 255 | } 256 | else if (target.attachEvent && target.setCapture) { 257 | // legacy IE mode - mouse with capture 258 | useSetReleaseCapture = true; 259 | target.attachEvent("onmousedown", function () { DoEvent(window.event); window.event.returnValue = false; return false; }); 260 | target.attachEvent("onmousemove", function () { DoEvent(window.event); window.event.returnValue = false; return false; }); 261 | target.attachEvent("onmouseup", function () { DoEvent(window.event); window.event.returnValue = false; return false; }); 262 | 263 | if (logMessage) 264 | logMessage("Using legacy IE mode - mouse model with capture"); 265 | } 266 | else { 267 | if (logMessage) 268 | logMessage("Unexpected combination of supported features"); 269 | } 270 | 271 | } 272 | 'use strict'; 273 | 274 | angular.module('angular-ranger',[]) 275 | .directive('angularRanger', ['$window', function($window){ 276 | return { 277 | restrict: 'E', 278 | replace: true, 279 | templateUrl: 'angular-ranger.html', 280 | scope:{ 281 | min: '@', 282 | max: '@', 283 | step: '@', 284 | minValue: '=?', 285 | maxValue: '=?', 286 | value: '=?' 287 | }, 288 | link: function(scope, el, attrs){ 289 | // Private Variables 290 | //================================ 291 | var scale = el[0].querySelector('.ranger-scale'), 292 | markers = { 293 | min: angular.element(scale.children[0]), 294 | max: angular.element(scale.children[2]) 295 | }, 296 | fill = angular.element(scale.children[1]), 297 | range = Math.abs(scope.min - scope.max), 298 | maxPx = scale.clientWidth, 299 | step = parseFloat(scope.step) || 1, 300 | currentX = {min: 0, max: scale.clientWidth}, 301 | singleValue = scope.minValue == null, 302 | moveX = null, 303 | moveTarget = null, 304 | disabled = false, 305 | rAFIndex = null; 306 | 307 | // Public Variables 308 | //================================ 309 | if(singleValue) 310 | markers.min.css('display', 'none'); 311 | 312 | // Private Methods 313 | //================================ 314 | function getClosestMarker(x){ 315 | if(singleValue) return 'max'; 316 | var fromMin = Math.abs(x-currentX.min); 317 | var fromMax = Math.abs(x-currentX.max); 318 | if(fromMin == fromMax) return x currentX.max) moveX = currentX.max; 345 | if(moveTarget == 'min' && moveX < 0) moveX = 0; 346 | if(moveTarget == 'max' && moveX < currentX.min) moveX = currentX.min; 347 | if(moveTarget == 'max' && moveX > maxPx) moveX = maxPx; 348 | } 349 | function updatePositionWithX() { 350 | if (!moveX) return; 351 | setValidPosition(); 352 | 353 | currentX[moveTarget] = moveX; 354 | markers[moveTarget].attr('marker-value', getNearestStep()); 355 | updatePosition(); 356 | } 357 | function updatePosition(){ 358 | if(moveTarget == null || markers[moveTarget] == null) return; 359 | markers[moveTarget].css('left', currentX[moveTarget]+'px'); 360 | if(moveTarget == 'min'){ 361 | fill.css('left', currentX.min+'px'); 362 | }else{ 363 | fill.css('right', (maxPx - currentX.max)+'px'); 364 | } 365 | } 366 | function updatePositionPercentage(){ 367 | var percentages = { 368 | min: (Math.abs(singleValue? 0 : scope.min - scope.minValue)/range)*100, 369 | max: (Math.abs(scope.min - (singleValue? scope.value : scope.maxValue))/range)*100 370 | }; 371 | markers.min.css('left', percentages.min+'%'); 372 | markers.max.css('left', percentages.max+'%'); 373 | fill.css({ 374 | 'left': percentages.min+'%', 375 | 'right': (100-percentages.max)+'%' 376 | }); 377 | } 378 | function updatePositionWithValue() { 379 | updateLimits(); 380 | updatePositionPercentage(); 381 | } 382 | function updateLimits(){ 383 | maxPx = scale.clientWidth; 384 | var minValue = scope.minValue || scope.min || 0; 385 | var maxValue = scope.maxValue || scope.max || 0; 386 | if(scope.max < (singleValue? scope.value : scope.maxValue)) scope[singleValue? 'value' : 'maxValue'] = scope.max; 387 | if(scope.min > (singleValue? scope.value : scope.minValue)) scope[singleValue? 'value' : 'minValue'] = scope.min; 388 | currentX.min = (Math.abs(scope.min - minValue)/range) * scale.clientWidth; 389 | currentX.max = (Math.abs(scope.min - maxValue)/range) * scale.clientWidth; 390 | } 391 | 392 | // Watchers 393 | //================================ 394 | angular.element($window).bind('resize', function () { 395 | updateLimits(); 396 | updatePosition(); 397 | scope.$apply(); 398 | }); 399 | scope.$watch('min', updateRange); 400 | scope.$watch('max', updateRange); 401 | scope.$watch('minValue', updatePositionWithValue); 402 | scope.$watch('maxValue', updatePositionWithValue); 403 | scope.$watch('value', updatePositionWithValue); 404 | scope.$watch('step', updateStep); 405 | attrs.$observe('disabled', updateDisabled) 406 | 407 | // Click/Drag Bindings 408 | //================================ 409 | PointerDraw( 410 | scale, 411 | function (target, pointerId, x, y, e) { // mousedown 412 | if(disabled) return; 413 | if (maxPx < 1) updateLimits(); //Set limits if range was previously collapsed 414 | moveTarget = getClosestMarker(x); 415 | markers[moveTarget][0].focus(); 416 | moveX = x; 417 | updatePositionWithX(); 418 | }, 419 | function (target, pointerId, x, y, e) { // mousemove 420 | if(disabled) return; 421 | moveX = x; 422 | cancelAnimationFrame(rAFIndex); 423 | rAFIndex = requestAnimationFrame(updatePositionWithX); 424 | }, 425 | function (target, pointerId, e) { // mouseup 426 | if(disabled) return; 427 | markers[moveTarget][0].blur(); 428 | setValidPosition(); 429 | snapToNearestStep(); 430 | cancelAnimationFrame(rAFIndex); 431 | } 432 | ); 433 | } 434 | }; 435 | }]); 436 | angular.module("angular-ranger").run(["$templateCache", function($templateCache) {$templateCache.put("angular-ranger.html","
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
");}]); -------------------------------------------------------------------------------- /angular-ranger.min.css: -------------------------------------------------------------------------------- 1 | .ranger{-webkit-box-sizing:border-box;box-sizing:border-box;background:#ddd;height:50px;padding:0 30px;border-radius:6px}.ranger-scale{width:100%;height:100%;position:relative;padding:21.5px 0}.ranger-scale:after{content:"";position:absolute;width:100%;height:7px;border:1px solid #444;border-radius:3px}.ranger-marker{position:absolute;width:30px;height:30px;border-radius:15px;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);background:#444;z-index:2}.ranger-marker:last-child{left:100%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.ranger-marker:after{display:none;content:attr(marker-value);position:absolute;left:50%;margin-left:-15px;top:-39px;background:#444;color:#fff;width:30px;height:30px;line-height:30px;text-align:center;border-radius:50% 50% 50% 0;-webkit-transform:rotate(-45deg);-ms-transform:rotate(-45deg);transform:rotate(-45deg)}.ranger-marker:focus{outline:0}.ranger-marker:focus:after,.ranger-marker:focus:before{display:block}.ranger-fill{position:absolute;top:21.5px;bottom:21.5px;background:#444} -------------------------------------------------------------------------------- /angular-ranger.min.js: -------------------------------------------------------------------------------- 1 | function PointerDraw(e,n,t,a,r){function i(e){if(Object.keys)return Object.keys(e).length;var n=0;for(var t in e)++n;return n}function o(e){e.preventDefault&&e.preventDefault(),e.preventManipulation&&e.preventManipulation(),e.preventMouseEvent&&e.preventMouseEvent()}function u(e){for(var n=0,t=0,a=e;null!=a;a=a.offsetParent)navigator.userAgent.match(/\bMSIE\b/)&&(!document.documentMode||document.documentMode<8)&&"relative"==a.currentStyle.position&&a.offsetParent&&"relative"==a.offsetParent.currentStyle.position&&a.offsetLeft==a.offsetParent.offsetLeft?t+=a.offsetTop:(n+=a.offsetLeft,t+=a.offsetTop);return{x:n,y:t}}function m(n){if("undefined"==typeof n.pageX)if(n.pageX=n.offsetX+f.x,n.pageY=n.offsetY+f.y,n.srcElement.offsetParent==e&&document.documentMode&&8==document.documentMode&&"mousedown"==n.type)n.pageX+=n.srcElement.offsetLeft,n.pageY+=n.srcElement.offsetTop;else if(n.srcElement!=e&&!document.documentMode||document.documentMode<8){for(var t=-2,a=-2,r=n.srcElement;null!=r;r=r.parentNode)t+=r.scrollLeft?r.scrollLeft:0,a+=r.scrollTop?r.scrollTop:0;n.pageX=n.clientX+t,n.pageY=n.clientY+a}}function l(n){return Math.max(0,Math.min(n-f.x,e.offsetWidth))}function s(n){return Math.max(0,Math.min(n-f.y,e.offsetHeight))}function c(p){if("mousemove"!=p.type||0!=i(d)){o(p);for(var h=p.changedTouches?p.changedTouches:[p],g=0;gM.max&&(L=M.max),"min"==V&&L<0&&(L=0),"max"==V&&Ly&&(L=y)}function c(){L&&(s(),M[V]=L,g[V].attr("marker-value",m()),d())}function d(){null!=V&&null!=g[V]&&(g[V].css("left",M[V]+"px"),"min"==V?x.css("left",M.min+"px"):x.css("right",y-M.max+"px"))}function f(){var e={min:Math.abs(b?0:n.min-n.minValue)/w*100,max:Math.abs(n.min-(b?n.value:n.maxValue))/w*100};g.min.css("left",e.min+"%"),g.max.css("left",e.max+"%"),x.css({left:e.min+"%",right:100-e.max+"%"})}function v(){p(),f()}function p(){y=h.clientWidth;var e=n.minValue||n.min||0,t=n.maxValue||n.max||0;n.max<(b?n.value:n.maxValue)&&(n[b?"value":"maxValue"]=n.max),n.min>(b?n.value:n.minValue)&&(n[b?"value":"minValue"]=n.min),M.min=Math.abs(n.min-e)/w*h.clientWidth,M.max=Math.abs(n.min-t)/w*h.clientWidth}var h=t[0].querySelector(".ranger-scale"),g={min:angular.element(h.children[0]),max:angular.element(h.children[2])},x=angular.element(h.children[1]),w=Math.abs(n.min-n.max),y=h.clientWidth,E=parseFloat(n.step)||1,M={min:0,max:h.clientWidth},b=null==n.minValue,L=null,V=null,P=!1,$=null;b&&g.min.css("display","none"),angular.element(e).bind("resize",function(){p(),d(),n.$apply()}),n.$watch("min",i),n.$watch("max",i),n.$watch("minValue",v),n.$watch("maxValue",v),n.$watch("value",v),n.$watch("step",o),a.$observe("disabled",u),PointerDraw(h,function(e,n,t,a,i){P||(y<1&&p(),V=r(t),g[V][0].focus(),L=t,c())},function(e,n,t,a,r){P||(L=t,cancelAnimationFrame($),$=requestAnimationFrame(c))},function(e,n,t){P||(g[V][0].blur(),s(),l(),cancelAnimationFrame($))})}}}]),angular.module("angular-ranger").run(["$templateCache",function(e){e.put("angular-ranger.html",'
\r\n\t
\r\n\t\t
\r\n\t\t
\r\n\t\t
\r\n\t
\r\n
')}]); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ranger", 3 | "version": "0.2.1", 4 | "authors": [ 5 | "Justin Maier " 6 | ], 7 | "description": "A mobile friendly, super-fast, range slider. He's the hero Your App deserves...", 8 | "main": [ 9 | "angular-ranger.js", 10 | "angular-ranger.css" 11 | ], 12 | "keywords": [ 13 | "angular", 14 | "range slider", 15 | "slider", 16 | "ranger" 17 | ], 18 | "license": "MIT", 19 | "homepage": "http://github.com/justmaier/angular-ranger", 20 | "ignore": [ 21 | "**/.*", 22 | "node_modules", 23 | "bower_components", 24 | "test", 25 | "tests" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var 2 | gulp = require('gulp') 3 | , plumber = require('gulp-plumber') 4 | , concat = require('gulp-concat') 5 | , rename = require('gulp-rename') 6 | , uglify = require('gulp-uglify') 7 | , uglifyCss = require('gulp-uglifycss') 8 | , templateCache = require('gulp-angular-templatecache') 9 | , sass = require('gulp-sass') 10 | , autoprefixer = require('gulp-autoprefixer') 11 | , del = require('del') 12 | , livereload = require('gulp-livereload') 13 | ; 14 | 15 | gulp.task('sass', function(){ 16 | var stream = gulp.src('src/angular-ranger.scss') 17 | .pipe(plumber()) 18 | .pipe(sass()) 19 | .pipe(autoprefixer('last 10 versions')) 20 | .pipe(gulp.dest('./')) 21 | .pipe(livereload()); 22 | 23 | return stream; 24 | }); 25 | 26 | gulp.task('templates', function(){ 27 | var stream = gulp.src('src/**/*.html') 28 | .pipe(plumber()) 29 | .pipe(templateCache({ 30 | root: '', 31 | module: 'angular-ranger' 32 | })) 33 | .pipe(gulp.dest('./')); 34 | 35 | return stream; 36 | }); 37 | 38 | gulp.task('js', ['templates'], function(){ 39 | var stream = gulp.src(['src/lib/**/*.js','src/**/*.js','templates.js']) 40 | .pipe(plumber()) 41 | .pipe(concat('angular-ranger.js')) 42 | .pipe(gulp.dest('./')) 43 | .pipe(livereload()); 44 | 45 | return stream; 46 | }) 47 | 48 | gulp.task('minify-js', ['js'], function(){ 49 | var stream = gulp.src('angular-ranger.js') 50 | .pipe(plumber()) 51 | .pipe(uglify()) 52 | .pipe(rename({suffix:'.min'})) 53 | .pipe(gulp.dest('./')); 54 | 55 | return stream; 56 | }) 57 | 58 | gulp.task('minify-css', ['sass'], function(){ 59 | var stream = gulp.src('angular-ranger.css') 60 | .pipe(plumber()) 61 | .pipe(uglifyCss()) 62 | .pipe(rename({suffix:'.min'})) 63 | .pipe(gulp.dest('./')); 64 | 65 | return stream; 66 | }) 67 | 68 | gulp.task('minify', ['minify-css', 'minify-js']); 69 | 70 | gulp.task('clean', function(cb){ 71 | del([ 72 | 'angular-ranger.*', 73 | 'templates.js' 74 | ], cb) 75 | }) 76 | 77 | gulp.task('build', ['clean', 'minify'], function(cb){ 78 | del(['templates.js'], cb); 79 | }); 80 | 81 | gulp.task('default', ['build']); 82 | 83 | gulp.task('watch', ['build'], function(){ 84 | livereload.listen(); 85 | 86 | gulp.watch('src/**/*.scss', ['sass']); 87 | gulp.watch([ 88 | 'src/**/*.html', 89 | 'src/**/*.js', 90 | ], ['js']); 91 | }); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Ranger 7 | 8 | 9 | 87 | 88 | 89 |
90 |
91 |

Angular Ranger

92 |

A mobile friendly, super-fast, range slider.
He's the hero Your App deserves...

93 |

94 | Code on Github 95 |

96 |
    97 |
  • 98 | 100 |
  • 101 |
  • 102 | 104 |
  • 105 |
106 |
107 |
108 | 109 |
110 |

Range Selection

111 | 112 | 113 |
114 |
115 |

Values

116 | 117 |
118 |
119 |

Settings

120 | 121 |
122 |
123 | 124 |

Single Value Selection

125 | 126 |
127 |
128 |

Values

129 | 130 |
131 |
132 |

Settings

133 | 134 |
135 |
136 |
137 | 138 | 139 | 140 | 141 | 142 | 143 | 173 | 174 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./angular-ranger'); 2 | 3 | module.exports = 'angular-ranger'; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ranger", 3 | "version": "0.2.1", 4 | "description": "A mobile friendly, super-fast, range slider. He's the hero Your App deserves...", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "http://github.com/justmaier/angular-ranger.git" 12 | }, 13 | "keywords": [ 14 | "angular" 15 | ], 16 | "author": "Justin Maier (http://justinmaier.com/)", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/justmaier/angular-ranger/issues" 20 | }, 21 | "homepage": "https://github.com/justmaier/angular-ranger", 22 | "devDependencies": { 23 | "del": "^1.1.0", 24 | "gulp": "*", 25 | "gulp-angular-templatecache": "^1.8.0", 26 | "gulp-autoprefixer": "^3.1.0", 27 | "gulp-concat": "^2.5.2", 28 | "gulp-livereload": "^3.8.0", 29 | "gulp-plumber": "^1.0.1", 30 | "gulp-rename": "^1.2.2", 31 | "gulp-sass": "^2.1.0", 32 | "gulp-uglify": "^1.5.1", 33 | "gulp-uglifycss": "^1.0.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/angular-ranger.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
-------------------------------------------------------------------------------- /src/angular-ranger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('angular-ranger',[]) 4 | .directive('angularRanger', ['$window', function($window){ 5 | return { 6 | restrict: 'E', 7 | replace: true, 8 | templateUrl: 'angular-ranger.html', 9 | scope:{ 10 | min: '@', 11 | max: '@', 12 | step: '@', 13 | minValue: '=?', 14 | maxValue: '=?', 15 | value: '=?' 16 | }, 17 | link: function(scope, el, attrs){ 18 | // Private Variables 19 | //================================ 20 | var scale = el[0].querySelector('.ranger-scale'), 21 | markers = { 22 | min: angular.element(scale.children[0]), 23 | max: angular.element(scale.children[2]) 24 | }, 25 | fill = angular.element(scale.children[1]), 26 | range = Math.abs(scope.min - scope.max), 27 | maxPx = scale.clientWidth, 28 | step = parseFloat(scope.step) || 1, 29 | currentX = {min: 0, max: scale.clientWidth}, 30 | singleValue = scope.minValue == null, 31 | moveX = null, 32 | moveTarget = null, 33 | disabled = false, 34 | rAFIndex = null; 35 | 36 | // Public Variables 37 | //================================ 38 | if(singleValue) 39 | markers.min.css('display', 'none'); 40 | 41 | // Private Methods 42 | //================================ 43 | function getClosestMarker(x){ 44 | if(singleValue) return 'max'; 45 | var fromMin = Math.abs(x-currentX.min); 46 | var fromMax = Math.abs(x-currentX.max); 47 | if(fromMin == fromMax) return x currentX.max) moveX = currentX.max; 74 | if(moveTarget == 'min' && moveX < 0) moveX = 0; 75 | if(moveTarget == 'max' && moveX < currentX.min) moveX = currentX.min; 76 | if(moveTarget == 'max' && moveX > maxPx) moveX = maxPx; 77 | } 78 | function updatePositionWithX() { 79 | if (!moveX) return; 80 | setValidPosition(); 81 | 82 | currentX[moveTarget] = moveX; 83 | markers[moveTarget].attr('marker-value', getNearestStep()); 84 | updatePosition(); 85 | } 86 | function updatePosition(){ 87 | if(moveTarget == null || markers[moveTarget] == null) return; 88 | markers[moveTarget].css('left', currentX[moveTarget]+'px'); 89 | if(moveTarget == 'min'){ 90 | fill.css('left', currentX.min+'px'); 91 | }else{ 92 | fill.css('right', (maxPx - currentX.max)+'px'); 93 | } 94 | } 95 | function updatePositionPercentage(){ 96 | var percentages = { 97 | min: (Math.abs(singleValue? 0 : scope.min - scope.minValue)/range)*100, 98 | max: (Math.abs(scope.min - (singleValue? scope.value : scope.maxValue))/range)*100 99 | }; 100 | markers.min.css('left', percentages.min+'%'); 101 | markers.max.css('left', percentages.max+'%'); 102 | fill.css({ 103 | 'left': percentages.min+'%', 104 | 'right': (100-percentages.max)+'%' 105 | }); 106 | } 107 | function updatePositionWithValue() { 108 | updateLimits(); 109 | updatePositionPercentage(); 110 | } 111 | function updateLimits(){ 112 | maxPx = scale.clientWidth; 113 | var minValue = scope.minValue || scope.min || 0; 114 | var maxValue = scope.maxValue || scope.max || 0; 115 | if(scope.max < (singleValue? scope.value : scope.maxValue)) scope[singleValue? 'value' : 'maxValue'] = scope.max; 116 | if(scope.min > (singleValue? scope.value : scope.minValue)) scope[singleValue? 'value' : 'minValue'] = scope.min; 117 | currentX.min = (Math.abs(scope.min - minValue)/range) * scale.clientWidth; 118 | currentX.max = (Math.abs(scope.min - maxValue)/range) * scale.clientWidth; 119 | } 120 | 121 | // Watchers 122 | //================================ 123 | angular.element($window).bind('resize', function () { 124 | updateLimits(); 125 | updatePosition(); 126 | scope.$apply(); 127 | }); 128 | scope.$watch('min', updateRange); 129 | scope.$watch('max', updateRange); 130 | scope.$watch('minValue', updatePositionWithValue); 131 | scope.$watch('maxValue', updatePositionWithValue); 132 | scope.$watch('value', updatePositionWithValue); 133 | scope.$watch('step', updateStep); 134 | attrs.$observe('disabled', updateDisabled) 135 | 136 | // Click/Drag Bindings 137 | //================================ 138 | PointerDraw( 139 | scale, 140 | function (target, pointerId, x, y, e) { // mousedown 141 | if(disabled) return; 142 | if (maxPx < 1) updateLimits(); //Set limits if range was previously collapsed 143 | moveTarget = getClosestMarker(x); 144 | markers[moveTarget][0].focus(); 145 | moveX = x; 146 | updatePositionWithX(); 147 | }, 148 | function (target, pointerId, x, y, e) { // mousemove 149 | if(disabled) return; 150 | moveX = x; 151 | cancelAnimationFrame(rAFIndex); 152 | rAFIndex = requestAnimationFrame(updatePositionWithX); 153 | }, 154 | function (target, pointerId, e) { // mouseup 155 | if(disabled) return; 156 | markers[moveTarget][0].blur(); 157 | setValidPosition(); 158 | snapToNearestStep(); 159 | cancelAnimationFrame(rAFIndex); 160 | } 161 | ); 162 | } 163 | }; 164 | }]); -------------------------------------------------------------------------------- /src/angular-ranger.scss: -------------------------------------------------------------------------------- 1 | // Ranger 2 | //========================================= 3 | $ranger-height: 50px !default; 4 | $ranger-scale-height: 7px !default; 5 | $ranger-marker-size: 30px !default; 6 | $ranger-marker-tooltip-size: $ranger-marker-size !default; 7 | $ranger-padding: $ranger-marker-size !default; 8 | $ranger-border-radius: 6px !default; 9 | $ranger-scale-border-radius: $ranger-border-radius / 2 !default; 10 | $ranger-background:#ddd !default; 11 | $ranger-color:#444 !default; 12 | $ranger-scale-color: $ranger-color !default; 13 | $ranger-marker-color: $ranger-color !default; 14 | $ranger-marker-tooltip-color: $ranger-color !default; 15 | $ranger-fill-color: $ranger-color !default; 16 | $ranger-marker-radius: $ranger-marker-size / 2 !default; 17 | $ranger-marker-triangle: $ranger-marker-size / 3; 18 | 19 | .ranger{ 20 | box-sizing:border-box; 21 | background: $ranger-background; 22 | height: $ranger-height; 23 | padding: 0 $ranger-padding; 24 | border-radius: $ranger-border-radius; 25 | &-scale{ 26 | width: 100%; 27 | height: 100%; 28 | position:relative; 29 | padding: (($ranger-height - $ranger-scale-height) / 2) 0; 30 | &:after{ 31 | content:""; 32 | position:absolute; 33 | width:100%; 34 | height: $ranger-scale-height; 35 | border: 1px solid $ranger-scale-color; 36 | border-radius: $ranger-scale-border-radius; 37 | } 38 | } 39 | &-marker{ 40 | position: absolute; 41 | width: $ranger-marker-size; 42 | height: $ranger-marker-size; 43 | border-radius: $ranger-marker-radius; 44 | top:50%; 45 | transform: translate(-50%, -50%); 46 | background:$ranger-marker-color; 47 | z-index:2; 48 | &:last-child{ 49 | left:100%; 50 | transform: translate(-50%, -50%); 51 | } 52 | &:after{ 53 | display: none; 54 | content: attr(marker-value); 55 | position:absolute; 56 | left:50%; 57 | margin-left: -($ranger-marker-tooltip-size /2); 58 | top:-($ranger-marker-tooltip-size * 1.3); 59 | background:$ranger-marker-tooltip-color; 60 | color:#fff; 61 | width: $ranger-marker-tooltip-size; 62 | height: $ranger-marker-tooltip-size; 63 | line-height: $ranger-marker-tooltip-size; 64 | text-align: center; 65 | border-radius: 50% 50% 50% 0; 66 | transform: rotate(-45deg); 67 | } 68 | &:focus{ 69 | outline:none; 70 | &:after,&:before{ 71 | display: block; 72 | } 73 | } 74 | } 75 | &-fill{ 76 | position: absolute; 77 | top:(($ranger-height - $ranger-scale-height) / 2); 78 | bottom:(($ranger-height - $ranger-scale-height) / 2); 79 | background: $ranger-fill-color; 80 | } 81 | } -------------------------------------------------------------------------------- /src/lib/PointerDraw.js: -------------------------------------------------------------------------------- 1 | // this javascript function abstracts mouse, pointer, and touch events 2 | // 3 | // invoke with: 4 | // target - the HTML element object which is the target of the drawing 5 | // startDraw - a function called with four parameters (target, pointerId, x, y) when the drawing begins. x and y are guaranteed to be within target's rectange 6 | // extendDraw - a function called with four parameters (target, pointerId, x, y) when the drawing is extended. x and y are guaranteed to be within target's rectange 7 | // endDraw - a function called with two parameters (target, pointerId) when the drawing is ended 8 | // logMessage - a function called with one parameter (string message) that can be logged as the caller desires. multiple line strings separated by \n may be sent 9 | // 10 | // all parameters expect target are optional 11 | // 12 | // target element cannot move within the document during drawing 13 | // 14 | function PointerDraw(target, startDraw, extendDraw, endDraw, logMessage) { 15 | 16 | // an object to keep track of the last x/y positions of the mouse/pointer/touch point 17 | // used to reject redundant moves and as a flag to determine if we're in the "down" state 18 | var lastXYById = {}; 19 | 20 | // an audit function to see if we're keeping lastXYById clean 21 | if (logMessage) { 22 | window.setInterval(function () { 23 | var logthis = false; 24 | var msg = "Current pointerId array contains:"; 25 | 26 | for (var key in lastXYById) { 27 | logthis = true; 28 | msg += " " + key; 29 | } 30 | 31 | if (logthis) { 32 | logMessage(msg); 33 | } 34 | }, 1000); 35 | } 36 | 37 | // Opera doesn't have Object.keys so we use this wrapper 38 | function NumberOfKeys(theObject) { 39 | if (Object.keys) 40 | return Object.keys(theObject).length; 41 | 42 | var n = 0; 43 | for (var key in theObject) 44 | ++n; 45 | 46 | return n; 47 | } 48 | 49 | // IE10's implementation in the Windows Developer Preview requires doing all of this 50 | // Not all of these methods remain in the Windows Consumer Preview, hence the tests for method existence. 51 | function PreventDefaultManipulationAndMouseEvent(evtObj) { 52 | if (evtObj.preventDefault) 53 | evtObj.preventDefault(); 54 | 55 | if (evtObj.preventManipulation) 56 | evtObj.preventManipulation(); 57 | 58 | if (evtObj.preventMouseEvent) 59 | evtObj.preventMouseEvent(); 60 | } 61 | 62 | // we send target-relative coordinates to the draw functions 63 | // this calculates the delta needed to convert pageX/Y to offsetX/Y because offsetX/Y don't exist in the TouchEvent object or in Firefox's MouseEvent object 64 | function ComputeDocumentToElementDelta(theElement) { 65 | var elementLeft = 0; 66 | var elementTop = 0; 67 | 68 | for (var offsetElement = theElement; offsetElement != null; offsetElement = offsetElement.offsetParent) { 69 | // the following is a major hack for versions of IE less than 8 to avoid an apparent problem on the IEBlog with double-counting the offsets 70 | // this may not be a general solution to IE7's problem with offsetLeft/offsetParent 71 | if (navigator.userAgent.match(/\bMSIE\b/) && (!document.documentMode || document.documentMode < 8) && offsetElement.currentStyle.position == "relative" && offsetElement.offsetParent && offsetElement.offsetParent.currentStyle.position == "relative" && offsetElement.offsetLeft == offsetElement.offsetParent.offsetLeft) { 72 | // add only the top 73 | elementTop += offsetElement.offsetTop; 74 | } 75 | else { 76 | elementLeft += offsetElement.offsetLeft; 77 | elementTop += offsetElement.offsetTop; 78 | } 79 | } 80 | 81 | return { x: elementLeft, y: elementTop }; 82 | } 83 | 84 | // function needed because IE versions before 9 did not define pageX/Y in the MouseEvent object 85 | function EnsurePageXY(eventObj) { 86 | if (typeof eventObj.pageX == 'undefined') { 87 | // initialize assuming our source element is our target 88 | eventObj.pageX = eventObj.offsetX + documentToTargetDelta.x; 89 | eventObj.pageY = eventObj.offsetY + documentToTargetDelta.y; 90 | 91 | if (eventObj.srcElement.offsetParent == target && document.documentMode && document.documentMode == 8 && eventObj.type == "mousedown") { 92 | // source element is a child piece of VML, we're in IE8, and we've not called setCapture yet - add the origin of the source element 93 | eventObj.pageX += eventObj.srcElement.offsetLeft; 94 | eventObj.pageY += eventObj.srcElement.offsetTop; 95 | } 96 | else if (eventObj.srcElement != target && !document.documentMode || document.documentMode < 8) { 97 | // source element isn't the target (most likely it's a child piece of VML) and we're in a version of IE before IE8 - 98 | // the offsetX/Y values are unpredictable so use the clientX/Y values and adjust by the scroll offsets of its parents 99 | // to get the document-relative coordinates (the same as pageX/Y) 100 | var sx = -2, sy = -2; // adjust for old IE's 2-pixel border 101 | for (var scrollElement = eventObj.srcElement; scrollElement != null; scrollElement = scrollElement.parentNode) { 102 | sx += scrollElement.scrollLeft ? scrollElement.scrollLeft : 0; 103 | sy += scrollElement.scrollTop ? scrollElement.scrollTop : 0; 104 | } 105 | 106 | eventObj.pageX = eventObj.clientX + sx; 107 | eventObj.pageY = eventObj.clientY + sy; 108 | } 109 | } 110 | } 111 | 112 | // cache the delta from the document to our event target (reinitialized each mousedown/MSPointerDown/touchstart) 113 | var documentToTargetDelta = ComputeDocumentToElementDelta(target); 114 | 115 | // functions to convert document-relative coordinates to target-relative and constrain them to be within the target 116 | function targetRelativeX(px) { return Math.max(0, Math.min(px - documentToTargetDelta.x, target.offsetWidth)); }; 117 | function targetRelativeY(py) { return Math.max(0, Math.min(py - documentToTargetDelta.y, target.offsetHeight)); }; 118 | 119 | // common event handler for the mouse/pointer/touch models and their down/start, move, up/end, and cancel events 120 | function DoEvent(theEvtObj) { 121 | 122 | // optimize rejecting mouse moves when mouse is up 123 | if (theEvtObj.type == "mousemove" && NumberOfKeys(lastXYById) == 0) 124 | return; 125 | 126 | PreventDefaultManipulationAndMouseEvent(theEvtObj); 127 | 128 | var pointerList = theEvtObj.changedTouches ? theEvtObj.changedTouches : [theEvtObj]; 129 | for (var i = 0; i < pointerList.length; ++i) { 130 | var pointerObj = pointerList[i]; 131 | var pointerId = (typeof pointerObj.identifier != 'undefined') ? pointerObj.identifier : (typeof pointerObj.pointerId != 'undefined') ? pointerObj.pointerId : 1; 132 | 133 | // use the pageX/Y coordinates to compute target-relative coordinates when we have them (in ie < 9, we need to do a little work to put them there) 134 | EnsurePageXY(pointerObj); 135 | var pageX = pointerObj.pageX; 136 | var pageY = pointerObj.pageY; 137 | 138 | if (theEvtObj.type.match(/(start|down)$/i)) { 139 | // clause for processing MSPointerDown, touchstart, and mousedown 140 | 141 | // refresh the document-to-target delta on start in case the target has moved relative to document 142 | documentToTargetDelta = ComputeDocumentToElementDelta(target); 143 | 144 | // protect against failing to get an up or end on this pointerId 145 | if (lastXYById[pointerId]) { 146 | if (endDraw) 147 | endDraw(target, pointerId); 148 | delete lastXYById[pointerId]; 149 | if (logMessage) 150 | logMessage("Ended draw on pointer " + pointerId + " in " + theEvtObj.type); 151 | } 152 | 153 | if (startDraw) 154 | startDraw(target, pointerId, targetRelativeX(pageX), targetRelativeY(pageY)); 155 | 156 | // init last page positions for this pointer 157 | lastXYById[pointerId] = { x: pageX, y: pageY }; 158 | 159 | // in the Microsoft pointer model, set the capture for this pointer 160 | // in the mouse model, set the capture or add a document-level event handlers if this is our first down point 161 | // nothing is required for the iOS touch model because capture is implied on touchstart 162 | if (target.msSetPointerCapture) 163 | target.msSetPointerCapture(pointerId); 164 | else if (theEvtObj.type == "mousedown" && NumberOfKeys(lastXYById) == 1) { 165 | if (useSetReleaseCapture) 166 | target.setCapture(true); 167 | else { 168 | document.addEventListener("mousemove", DoEvent, false); 169 | document.addEventListener("mouseup", DoEvent, false); 170 | } 171 | } 172 | } 173 | else if (theEvtObj.type.match(/move$/i)) { 174 | // clause handles mousemove, MSPointerMove, and touchmove 175 | 176 | if (lastXYById[pointerId] && !(lastXYById[pointerId].x == pageX && lastXYById[pointerId].y == pageY)) { 177 | // only extend if the pointer is down and it's not the same as the last point 178 | 179 | if (extendDraw) 180 | extendDraw(target, pointerId, targetRelativeX(pageX), targetRelativeY(pageY)); 181 | 182 | // update last page positions for this pointer 183 | lastXYById[pointerId].x = pageX; 184 | lastXYById[pointerId].y = pageY; 185 | } 186 | } 187 | else if (lastXYById[pointerId] && theEvtObj.type.match(/(up|end|cancel)$/i)) { 188 | // clause handles up/end/cancel 189 | 190 | if (endDraw) 191 | endDraw(target, pointerId); 192 | 193 | // delete last page positions for this pointer 194 | delete lastXYById[pointerId]; 195 | 196 | // in the Microsoft pointer model, release the capture for this pointer 197 | // in the mouse model, release the capture or remove document-level event handlers if there are no down points 198 | // nothing is required for the iOS touch model because capture is implied on touchstart 199 | if (target.msReleasePointerCapture) 200 | target.msReleasePointerCapture(pointerId); 201 | else if (theEvtObj.type == "mouseup" && NumberOfKeys(lastXYById) == 0) { 202 | if (useSetReleaseCapture) 203 | target.releaseCapture(); 204 | else { 205 | document.removeEventListener("mousemove", DoEvent, false); 206 | document.removeEventListener("mouseup", DoEvent, false); 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | var useSetReleaseCapture = false; 214 | 215 | if (window.navigator.msPointerEnabled) { 216 | // Microsoft pointer model 217 | target.addEventListener("MSPointerDown", DoEvent, false); 218 | target.addEventListener("MSPointerMove", DoEvent, false); 219 | target.addEventListener("MSPointerUp", DoEvent, false); 220 | target.addEventListener("MSPointerCancel", DoEvent, false); 221 | 222 | // css way to prevent panning in our target area 223 | if (typeof target.style.msContentZooming != 'undefined') 224 | target.style.msContentZooming = "none"; 225 | 226 | // new in Windows Consumer Preview: css way to prevent all built-in touch actions on our target 227 | // without this, you cannot touch draw on the element because IE will intercept the touch events 228 | if (typeof target.style.msTouchAction != 'undefined') 229 | target.style.msTouchAction = "none"; 230 | 231 | if (logMessage) 232 | logMessage("Using Microsoft pointer model"); 233 | } 234 | else if (target.addEventListener) { 235 | // iOS touch model 236 | target.addEventListener("touchstart", DoEvent, false); 237 | target.addEventListener("touchmove", DoEvent, false); 238 | target.addEventListener("touchend", DoEvent, false); 239 | target.addEventListener("touchcancel", DoEvent, false); 240 | 241 | // mouse model 242 | target.addEventListener("mousedown", DoEvent, false); 243 | 244 | // mouse model with capture 245 | // rejecting gecko because, unlike ie, firefox does not send events to target when the mouse is outside target 246 | if (target.setCapture && !window.navigator.userAgent.match(/\bGecko\b/)) { 247 | useSetReleaseCapture = true; 248 | 249 | target.addEventListener("mousemove", DoEvent, false); 250 | target.addEventListener("mouseup", DoEvent, false); 251 | 252 | if (logMessage) 253 | logMessage("Using mouse model with capture"); 254 | } 255 | } 256 | else if (target.attachEvent && target.setCapture) { 257 | // legacy IE mode - mouse with capture 258 | useSetReleaseCapture = true; 259 | target.attachEvent("onmousedown", function () { DoEvent(window.event); window.event.returnValue = false; return false; }); 260 | target.attachEvent("onmousemove", function () { DoEvent(window.event); window.event.returnValue = false; return false; }); 261 | target.attachEvent("onmouseup", function () { DoEvent(window.event); window.event.returnValue = false; return false; }); 262 | 263 | if (logMessage) 264 | logMessage("Using legacy IE mode - mouse model with capture"); 265 | } 266 | else { 267 | if (logMessage) 268 | logMessage("Unexpected combination of supported features"); 269 | } 270 | 271 | } --------------------------------------------------------------------------------