1/** 2 * Angular JS slider directive 3 * 4 * (c) Rafal Zajac <[email protected]> 5 * http://github.com/rzajac/angularjs-slider 6 * 7 * Version: v1.0.0 8 * 9 * Licensed under the MIT license 10 */ 11 12/*jslint unparam: true */ 13/*global angular: false, console: false, define, module */ 14(function (root, factory) { 15 'use strict'; 16 if (typeof define === 'function' && define.amd) { 17 // AMD. Register as an anonymous module. 18 define(['angular'], factory); 19 } else if (typeof module === 'object' && module.exports) { 20 // Node. Does not work with strict CommonJS, but 21 // only CommonJS-like environments that support module.exports, 22 // like Node. 23 // to support bundler like browserify 24 module.exports = factory(require('angular')); 25 } else { 26 // Browser globals (root is window) 27 factory(root.angular); 28 } 29 30}(this, function (angular) { 31 'use strict'; 32var module = angular.module('rzModule', []) 33 34.value('throttle', 35 /** 36 * throttle 37 * 38 * Taken from underscore project 39 * 40 * @param {Function} func 41 * @param {number} wait 42 * @param {ThrottleOptions} options 43 * @returns {Function} 44 */ 45function throttle(func, wait, options) { 46 'use strict'; 47 var getTime = (Date.now || function() { 48 return new Date().getTime(); 49 }); 50 var context, args, result; 51 var timeout = null; 52 var previous = 0; 53 options = options || {}; 54 var later = function() { 55 previous = options.leading === false ? 0 : getTime(); 56 timeout = null; 57 result = func.apply(context, args); 58 context = args = null; 59 }; 60 return function() { 61 var now = getTime(); 62 if (!previous && options.leading === false) { previous = now; } 63 var remaining = wait - (now - previous); 64 context = this; 65 args = arguments; 66 if (remaining <= 0) { 67 clearTimeout(timeout); 68 timeout = null; 69 previous = now; 70 result = func.apply(context, args); 71 context = args = null; 72 } else if (!timeout && options.trailing !== false) { 73 timeout = setTimeout(later, remaining); 74 } 75 return result; 76 }; 77}) 78 79.factory('RzSlider', ['$timeout', '$document', '$window', 'throttle', function($timeout, $document, $window, throttle) 80{ 81 'use strict'; 82 83 /** 84 * Slider 85 * 86 * @param {ngScope} scope The AngularJS scope 87 * @param {Element} sliderElem The slider directive element wrapped in jqLite 88 * @param {*} attributes The slider directive attributes 89 * @constructor 90 */ 91 var Slider = function(scope, sliderElem, attributes) 92 { 93 /** 94 * The slider's scope 95 * 96 * @type {ngScope} 97 */ 98 this.scope = scope; 99 100 /** 101 * The slider attributes 102 * 103 * @type {Object} 104 */ 105 this.attributes = attributes; 106 107 /** 108 * Slider element wrapped in jqLite 109 * 110 * @type {jqLite} 111 */ 112 this.sliderElem = sliderElem; 113 114 /**
115 * Slider type 116 * 117 * @type {boolean} Set to true for range slider 118 */ 119 this.range = attributes.rzSliderHigh !== undefined && attributes.rzSliderModel !== undefined; 120 121 /** 122 * Whether to allow draggable range 123 * 124 * @type {boolean} Set to true for draggable range slider 125 */ 126 this.dragRange = this.range && attributes.rzSliderDraggableRange === 'true'; 127 128 /** 129 * Values recorded when first dragging the bar 130 * 131 * @type {Object} 132 */ 133 this.dragging = { 134 active: false, 135 value: 0, 136 difference: 0, 137 offset: 0, 138 lowDist: 0, 139 highDist: 0 140 }; 141 142 /** 143 * Half of the width of the slider handles 144 * 145 * @type {number} 146 */ 147 this.handleHalfWidth = 0; 148 149 /** 150 * Always show selection bar 151 * 152 * @type {boolean} 153 */ 154 this.alwaysShowBar = !!attributes.rzSliderAlwaysShowBar; 155 156 /** 157 * Maximum left the slider handle can have 158 * 159 * @type {number} 160 */ 161 this.maxLeft = 0; 162 163 /** 164 * Precision 165 * 166 * @type {number} 167 */ 168 this.precision = 0; 169 170 /** 171 * Step 172 * 173 * @type {number} 174 */ 175 this.step = 0; 176 177 /** 178 * The name of the handle we are currently tracking 179 * 180 * @type {string} 181 */ 182 this.tracking = ''; 183 184 /** 185 * Minimum value (floor) of the model 186 * 187 * @type {number} 188 */ 189 this.minValue = 0; 190 191 /** 192 * Maximum value (ceiling) of the model 193 * 194 * @type {number} 195 */ 196 this.maxValue = 0; 197 198 /** 199 * Hide limit labels 200 * 201 * @type {boolean} 202 */ 203 this.hideLimitLabels = !!attributes.rzSliderHideLimitLabels; 204 205 /** 206 * Only present model values 207 * 208 * Do not allow to change values 209 * 210 * @type {boolean} 211 */ 212 this.presentOnly = attributes.rzSliderPresentOnly === 'true'; 213 214 /** 215 * Display ticks on each possible value. 216 * 217 * @type {boolean} 218 */ 219 this.showTicks = attributes.rzSliderShowTicks || attributes.rzSliderShowTicksValue; 220 221 /** 222 * Display the value on each tick. 223 * 224 * @type {boolean} 225 */ 226 this.showTicksValue = attributes.rzSliderShowTicksValue; 227 228 /** 229 * Disable the slider 230 * 231 * @type {boolean} 232 */ 233 this.disabled = this.scope.rzSliderDisabled; 234 235 /** 236 * The delta between min and max value 237 * 238 * @type {number} 239 */ 240 this.valueRange = 0; 241 242 /** 243 * Set to true if init method already executed 244 * 245 * @type {boolean} 246 */ 247 this.initHasRun = false; 248 249 /** 250 * Custom translate function 251 * 252 * @type {function} 253 */ 254 this.customTrFn = this.scope.rzSliderTranslate() || function(value) { return String(value); }; 255 256 /** 257 * Array of de-registration functions to call on $destroy 258 * 259 * @type {Array.<Function>} 260 */ 261 this.deRegFuncs = []; 262 263 // Slider DOM elements wrapped in jqLite 264 this.fullBar = null; // The whole slider bar 265 this.selBar = null; // Highlight between two handles 266 this.minH = null; // Left slider handle 267 this.maxH = null; // Right slider handle 268 this.flrLab = null; // Floor label 269 this.ceilLab = null; // Ceiling label 270 this.minLab = null; // Label above the low value 271 this.maxLab = null; // Label above the high value 272 this.cmbLab = null; // Combined label 273 this.ticks = null; // The ticks 274 275 // Initialize slider 276 this.init(); 277 }; 278 279 // Add instance methods 280 Slider.prototype = { 281 282 /** 283 * Initialize slider 284 * 285 * @returns {undefined} 286 */ 287 init: function() 288 { 289 var thrLow, thrHigh, unRegFn, 290 calcDimFn = angular.bind(this, this.calcViewDimensions), 291 self = this; 292 293 this.initElemHandles(); 294 this.addAccessibility(); 295 this.setDisabledState(); 296 this.calcViewDimensions(); 297 this.setMinAndMax(); 298 299 this.precision = this.scope.rzSliderPrecision === undefined ? 0 : +this.scope.rzSliderPrecision; 300 this.step = this.scope.rzSliderStep === undefined ? 1 : +this.scope.rzSliderStep; 301 302 $timeout(function() 303 { 304 self.updateCeilLab(); 305 self.updateFloorLab(); 306 self.initHandles(); 307 self.bindEvents(); 308 }); 309 310 // Recalculate slider view dimensions 311 unRegFn = this.scope.$on('reCalcViewDimensions', calcDimFn); 312 this.deRegFuncs.push(unRegFn); 313 314 // Recalculate stuff if view port dimensions have changed 315 angular.element($window).on('resize', calcDimFn); 316 317 this.initHasRun = true; 318 319 // Watch for changes to the model 320 321 thrLow = throttle(function() 322 { 323 self.setMinAndMax(); 324 self.updateLowHandle(self.valueToOffset(self.scope.rzSliderModel)); 325 self.updateSelectionBar(); 326 self.updateTicksScale(); 327 328 if(self.range) 329 { 330 self.updateCmbLabel(); 331 } 332 333 }, 350, { leading: false }); 334 335 thrHigh = throttle(function() 336 { 337 self.setMinAndMax(); 338 self.updateHighHandle(self.valueToOffset(self.scope.rzSliderHigh)); 339 self.updateSelectionBar(); 340 self.updateTicksScale(); 341 self.updateCmbLabel(); 342 }, 350, { leading: false }); 343 344 this.scope.$on('rzSliderForceRender', function() 345 { 346 self.resetLabelsValue(); 347 thrLow(); 348 if(self.range) { thrHigh(); } 349 self.resetSlider(); 350 }); 351 352 // Watchers 353 354 unRegFn = this.scope.$watch('rzSliderModel', function(newValue, oldValue) 355 { 356 if(newValue === oldValue) { return; } 357 thrLow(); 358 }); 359 this.deRegFuncs.push(unRegFn); 360 361 unRegFn = this.scope.$watch('rzSliderHigh', function(newValue, oldValue) 362 { 363 if(newValue === oldValue) { return; } 364 thrHigh(); 365 }); 366 this.deRegFuncs.push(unRegFn); 367 368 this.scope.$watch('rzSliderFloor', function(newValue, oldValue) 369 { 370 if(newValue === oldValue) { return; } 371 self.resetSlider(); 372 }); 373 this.deRegFuncs.push(unRegFn); 374 375 unRegFn = this.scope.$watch('rzSliderCeil', function(newValue, oldValue) 376 { 377 if(newValue === oldValue) { return; } 378 self.resetSlider(); 379 }); 380 this.deRegFuncs.push(unRegFn); 381 382 unRegFn = this.scope.$watch('rzSliderShowTicks', function(newValue, oldValue) 383 { 384 if(newValue === oldValue) { return; } 385 self.resetSlider(); 386 }); 387 this.deRegFuncs.push(unRegFn); 388 389 unRegFn = this.scope.$watch('rzSliderShowTicksValue', function(newValue, oldValue) 390 { 391 if(newValue === oldValue) { return; } 392 self.resetSlider(); 393 }); 394 this.deRegFuncs.push(unRegFn); 395 396 unRegFn = this.scope.$watch('rzSliderDisabled', function(newValue, oldValue) 397 { 398 if(newValue === oldValue) { return; } 399 self.resetSlider(); 400 if(self.disabled) 401 self.unbindEvents(); 402 else 403 self.bindEvents(); 404 }); 405 this.deRegFuncs.push(unRegFn); 406 407 this.scope.$on('$destroy', function() 408 { 409 self.unbindEvents(); 410 angular.element($window).off('resize', calcDimFn); 411 self.deRegFuncs.map(function(unbind) { unbind(); }); 412 }); 413 }, 414 415 /** 416 * Resets slider 417 * 418 * @returns {undefined} 419 */ 420 resetSlider: function() 421 { 422 this.setMinAndMax(); 423 this.updateCeilLab(); 424 this.updateFloorLab(); 425 this.setDisabledState(); 426 this.calcViewDimensions(); 427 }, 428 429 /** 430 * Set the disabled state based on rzSliderDisabled 431 * 432 * @returns {undefined} 433 */ 434 setDisabledState: function() 435 { 436 this.disabled = this.scope.rzSliderDisabled; 437 if(this.disabled) { 438 this.sliderElem.attr('disabled', 'disabled'); 439 } 440 else { 441 this.sliderElem.attr('disabled', null); 442 } 443 444 }, 445 446 /** 447 * Reset label values 448 * 449 * @return {undefined} 450 */ 451 resetLabelsValue: function() 452 { 453 this.minLab.rzsv = undefined; 454 this.maxLab.rzsv = undefined; 455 }, 456 457 /** 458 * Initialize slider handles positions and labels 459 * 460 * Run only once during initialization and every time view port
460changes size 461 * 462 * @returns {undefined} 463 */ 464 initHandles: function() 465 { 466 this.updateLowHandle(this.valueToOffset(this.scope.rzSliderModel)); 467 468 if(this.range) 469 { 470 this.updateHighHandle(this.valueToOffset(this.scope.rzSliderHigh)); 471 this.updateCmbLabel(); 472 } 473 474 this.updateSelectionBar(); 475 this.updateTicksScale(); 476 }, 477 478 /** 479 * Translate value to human readable format 480 * 481 * @param {number|string} value 482 * @param {jqLite} label 483 * @param {boolean} [useCustomTr] 484 * @returns {undefined} 485 */ 486 translateFn: function(value, label, useCustomTr) 487 { 488 useCustomTr = useCustomTr === undefined ? true : useCustomTr; 489 490 var valStr = (useCustomTr ? this.customTrFn(value) : value).toString(), 491 getWidth = false; 492 493 if(label.rzsv === undefined || label.rzsv.length !== valStr.length || (label.rzsv.length > 0 && label.rzsw === 0)) 494 { 495 getWidth = true; 496 label.rzsv = valStr; 497 } 498 499 label.text(valStr); 500 501 // Update width only when length of the label have changed 502 if(getWidth) { this.getWidth(label); } 503 }, 504 505 /** 506 * Set maximum and minimum values for the slider 507 * 508 * @returns {undefined} 509 */ 510 setMinAndMax: function() 511 { 512 if(this.scope.rzSliderFloor) 513 { 514 this.minValue = +this.scope.rzSliderFloor; 515 } 516 else 517 { 518 this.minValue = this.scope.rzSliderFloor = 0; 519 } 520 521 if(this.scope.rzSliderCeil) 522 { 523 this.maxValue = +this.scope.rzSliderCeil; 524 } 525 else 526 { 527 this.maxValue = this.scope.rzSliderCeil = this.range ? this.scope.rzSliderHigh : this.scope.rzSliderModel; 528 } 529 530 if(this.scope.rzSliderStep) 531 { 532 this.step = +this.scope.rzSliderStep; 533 } 534 535 this.valueRange = this.maxValue - this.minValue; 536 }, 537 538 /** 539 * Set the slider children to variables for easy access 540 * 541 * Run only once during initialization 542 * 543 * @returns {undefined} 544 */ 545 initElemHandles: function() 546 { 547 // Assign all slider elements to object properties for easy access 548 angular.forEach(this.sliderElem.children(), function(elem, index) 549 { 550 var jElem = angular.element(elem); 551 552 switch(index) 553 { 554 case 0: this.fullBar = jElem; break; 555 case 1: this.selBar = jElem; break; 556 case 2: this.minH = jElem; break; 557 case 3: this.maxH = jElem; break; 558 case 4: this.flrLab = jElem; break; 559 case 5: this.ceilLab = jElem; break; 560 case 6: this.minLab = jElem; break; 561 case 7: this.maxLab = jElem; break; 562 case 8: this.cmbLab = jElem; break; 563 case 9: this.ticks = jElem; break; 564 } 565 566 }, this); 567 568 // Initialize offset cache properties 569 this.selBar.rzsl = 0; 570 this.minH.rzsl = 0; 571 this.maxH.rzsl = 0; 572 this.flrLab.rzsl = 0; 573 this.ceilLab.rzsl = 0; 574 this.minLab.rzsl = 0; 575 this.maxLab.rzsl = 0; 576 this.cmbLab.rzsl = 0; 577 578 // Hide limit labels 579 if(this.hideLimitLabels) 580 { 581 this.flrLab.rzAlwaysHide = true; 582 this.ceilLab.rzAlwaysHide = true; 583 this.hideEl(this.flrLab); 584 this.hideEl(this.ceilLab); 585 } 586 587 if(this.showTicksValue) { 588 this.flrLab.rzAlwaysHide = true; 589 this.ceilLab.rzAlwaysHide = true; 590 this.minLab.rzAlwaysHide = true; 591 this.maxLab.rzAlwaysHide = true; 592 this.hideEl(this.flrLab); 593 this.hideEl(this.ceilLab); 594 this.hideEl(this.minLab); 595 this.hideEl(this.maxLab); 596 } 597 598 // Remove stuff not needed in single slider 599 if(this.range === false) 600 { 601 this.cmbLab.remove(); 602 this.maxLab.remove(); 603 604 // Hide max handle 605 this.maxH.rzAlwaysHide = true; 606 this.maxH[0].style.zIndex = '-1000'; 607 this.hideEl(this.maxH); 608 } 609 610 // Show selection bar for single slider or not 611 if(this.range === false && this.alwaysShowBar === false) 612 { 613 this.maxH.remove(); 614 this.selBar.remove(); 615 } 616 617 // If using draggable range, use appropriate cursor for this.selBar. 618 if (this.dragRange) 619 { 620 this.selBar.css('cursor', 'move'); 621 this.selBar.addClass('rz-draggable'); 622 } 623 }, 624 625 /** 626 * Adds accessibility atributes 627 * 628 * Run only once during initialization 629 * 630 * @returns {undefined} 631 */ 632 addAccessibility: function () 633 { 634 this.sliderElem.attr("role", "slider"); 635 }, 636 637 /** 638 * Calculate dimensions that are dependent on view port size 639 *
640 * Run once during initialization and every time view port changes size. 641 * 642 * @returns {undefined} 643 */ 644 calcViewDimensions: function () 645 { 646 var handleWidth = this.getWidth(this.minH); 647 648 this.handleHalfWidth = handleWidth / 2; 649 this.barWidth = this.getWidth(this.fullBar); 650 651 this.maxLeft = this.barWidth - handleWidth; 652 653 this.getWidth(this.sliderElem); 654 this.sliderElem.rzsl = this.sliderElem[0].getBoundingClientRect().left; 655 656 if(this.initHasRun) 657 { 658 this.updateCeilLab(); 659 this.initHandles(); 660 } 661 }, 662 663 /** 664 * Update the ticks position 665 * 666 * @returns {undefined} 667 */ 668 updateTicksScale: function() { 669 if(!this.showTicks) return; 670 if(!this.step) return; //if step is 0, the following loop will be endless. 671 672 var positions = '', 673 ticksCount = Math.round((this.maxValue - this.minValue) / this.step) + 1; 674 for (var i = 0; i < ticksCount; i++) { 675 var selectedClass = this.isTickSelected(i) ? 'selected': ''; 676 positions += '<li class="tick '+ selectedClass +'">'; 677 if(this.showTicksValue) 678 positions += '<span class="tick-value">'+ this.getDisplayValue(i) +'</span>'; 679 positions += '</li>'; 680 } 681 this.ticks.html(positions); 682 }, 683 684 isTickSelected: function(value) { 685 var tickLeft = this.valueToOffset(value); 686 if(!this.range && this.alwaysShowBar && value <= this.scope.rzSliderModel) 687 return true; 688 if(this.range && value >= this.scope.rzSliderModel && value <= this.scope.rzSliderHigh) 689 return true; 690 return false; 691 }, 692 693 /** 694 * Update position of the ceiling label 695 * 696 * @returns {undefined} 697 */ 698 updateCeilLab: function() 699 { 700 this.translateFn(this.scope.rzSliderCeil, this.ceilLab); 701 this.setLeft(this.ceilLab, this.barWidth - this.ceilLab.rzsw); 702 this.getWidth(this.ceilLab); 703 }, 704 705 /** 706 * Update position of the floor label 707 * 708 * @returns {undefined} 709 */ 710 updateFloorLab: function() 711 { 712 this.translateFn(this.scope.rzSliderFloor, this.flrLab); 713 this.getWidth(this.flrLab); 714 }, 715 716 /** 717 * Call the onStart callback if defined 718 * 719 * @returns {undefined} 720 */ 721 callOnStart: function() { 722 if(this.scope.rzSliderOnStart) { 723 var self = this; 724 $timeout(function() { 725 self.scope.rzSliderOnStart(); 726 }); 727 } 728 }, 729 730 /** 731 * Call the onChange callback if defined 732 * 733 * @returns {undefined} 734 */ 735 callOnChange: function() { 736 if(this.scope.rzSliderOnChange) { 737 var self = this; 738 $timeout(function() { 739 self.scope.rzSliderOnChange(); 740 }); 741 } 742 }, 743 744 /** 745 * Call the onEnd callback if defined 746 * 747 * @returns {undefined} 748 */ 749 callOnEnd: function() { 750 if(this.scope.rzSliderOnEnd) { 751 var self = this; 752 $timeout(function() { 753 self.scope.rzSliderOnEnd(); 754 }); 755 } 756 }, 757 758 /** 759 * Update slider handles and label positions 760 * 761 * @param {string} which 762 * @param {number} newOffset 763 */ 764 updateHandles: function(which, newOffset) 765 { 766 if(which === 'rzSliderModel') 767 { 768 this.updateLowHandle(newOffset); 769 this.updateSelectionBar(); 770 this.updateTicksScale(); 771 772 if(this.range) 773 { 774 this.updateCmbLabel(); 775 } 776 return; 777 } 778 779 if(which === 'rzSliderHigh') 780 { 781 this.updateHighHandle(newOffset); 782 this.updateSelectionBar(); 783 this.updateTicksScale(); 784 785 if(this.range) 786 { 787 this.updateCmbLabel(); 788 } 789 return; 790 } 791 792 // Update both 793 this.updateLowHandle(newOffset); 794 this.updateHighHandle(newOffset); 795 this.updateSelectionBar(); 796 this.updateTicksScale(); 797 this.updateCmbLabel(); 798 }, 799 800 /** 801 * Update low slider handle position and label 802 * 803 * @param {number} newOffset 804 * @returns {undefined} 805 */ 806 updateLowHandle: function(newOffset) 807 { 808 var delta = Math.abs(this.minH.rzsl - newOffset); 809 810 if(this.minLab.rzsv && delta < 1) { return; } 811 812 this.setLeft(this.minH, newOffset); 813 this.translateFn(this.scope.rzSliderModel, this.minLab); 814 this.setLeft(this.minLab, newOffset - this.minLab.rzsw / 2 + this.handleHalfWidth); 815 816 this.shFloorCeil(); 817 }, 818 819 /** 820 * Update high slider handle position and label 821 * 822 * @param {number} newOffset 823 * @returns {undefined} 824 */ 825 updateHighHandle: function(newOffset) 826 { 827 this.setLeft(this.maxH, newOffset); 828 this.translateFn(this.scope.rzSliderHigh, this.maxLab); 829 this.setLeft(this.maxLab, newOffset - this.maxLab.rzsw / 2 + this.handleHalfWidth); 830 831 this.shFloorCeil(); 832 }, 833 834 /** 835 * Show / hide floor / ceiling label 836 * 837 * @returns {undefined} 838 */ 839 shFloorCeil: function() 840 { 841 var flHidden = false, clHidden = false;
842 843 if(this.minLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + 5) 844 { 845 flHidden = true; 846 this.hideEl(this.flrLab); 847 } 848 else 849 { 850 flHidden = false; 851 this.showEl(this.flrLab); 852 } 853 854 if(this.minLab.rzsl + this.minLab.rzsw >= this.ceilLab.rzsl - this.handleHalfWidth - 10) 855 { 856 clHidden = true; 857 this.hideEl(this.ceilLab); 858 } 859 else 860 { 861 clHidden = false; 862 this.showEl(this.ceilLab); 863 } 864 865 if(this.range) 866 { 867 if(this.maxLab.rzsl + this.maxLab.rzsw >= this.ceilLab.rzsl - 10) 868 { 869 this.hideEl(this.ceilLab); 870 } 871 else if( ! clHidden) 872 { 873 this.showEl(this.ceilLab); 874 } 875 876 // Hide or show floor label 877 if(this.maxLab.rzsl <= this.flrLab.rzsl + this.flrLab.rzsw + this.handleHalfWidth) 878 { 879 this.hideEl(this.flrLab); 880 } 881 else if( ! flHidden) 882 { 883 this.showEl(this.flrLab); 884 } 885 } 886 }, 887 888 /** 889 * Update slider selection bar, combined label and range label 890 * 891 * @returns {undefined} 892 */ 893 updateSelectionBar: function() 894 { 895 this.setWidth(this.selBar, Math.abs(this.maxH.rzsl - this.minH.rzsl) + this.handleHalfWidth); 896 this.setLeft(this.selBar, this.range ? this.minH.rzsl + this.handleHalfWidth : 0); 897 }, 898 899 /** 900 * Update combined label position and value 901 * 902 * @returns {undefined} 903 */ 904 updateCmbLabel: function() 905 { 906 var lowTr, highTr; 907 908 if(this.minLab.rzsl + this.minLab.rzsw + 10 >= this.maxLab.rzsl) 909 { 910 lowTr = this.getDisplayValue(this.scope.rzSliderModel); 911 highTr = this.getDisplayValue(this.scope.rzSliderHigh); 912 913 this.translateFn(lowTr + ' - ' + highTr, this.cmbLab, false); 914 this.setLeft(this.cmbLab, this.selBar.rzsl + this.selBar.rzsw / 2 - this.cmbLab.rzsw / 2); 915 this.hideEl(this.minLab); 916 this.hideEl(this.maxLab); 917 this.showEl(this.cmbLab); 918 } 919 else 920 { 921 this.showEl(this.maxLab); 922 this.showEl(this.minLab); 923 this.hideEl(this.cmbLab); 924 } 925 }, 926 927 /** 928 * Return the translated value if a translate function is provided else the original value 929 * @param value 930 * @returns {*} 931 */ 932 getDisplayValue: function(value) { 933 return this.customTrFn ? this.customTrFn(value): value; 934 }, 935 936 /** 937 * Round value to step and precision 938 * 939 * @param {number} value 940 * @returns {number} 941 */ 942 roundStep: function(value) 943 { 944 var step = this.step, 945 remainder = +((value - this.minValue) % step).toFixed(3), 946 steppedValue = remainder > (step / 2) ? value + step - remainder : value - remainder; 947 948 steppedValue = steppedValue.toFixed(this.precision); 949 return +steppedValue; 950 }, 951 952 /** 953 * Hide element 954 * 955 * @param element 956 * @returns {jqLite} The jqLite wrapped DOM element 957 */ 958 hideEl: function (element) 959 { 960 return element.css({opacity: 0}); 961 }, 962 963 /** 964 * Show element 965 * 966 * @param element The jqLite wrapped DOM element 967 * @returns {jqLite} The jqLite 968 */ 969 showEl: function (element) 970 { 971 if(!!element.rzAlwaysHide) { return element; } 972 973 return element.css({opacity: 1}); 974 }, 975 976 /** 977 * Set element left offset 978 * 979 * @param {jqLite} elem The jqLite wrapped DOM element 980 * @param {number} left 981 * @returns {number} 982 */ 983 setLeft: function (elem, left) 984 { 985 elem.rzsl = left; 986 elem.css({left: left + 'px'}); 987 return left; 988 }, 989 990 /** 991 * Get element width 992 * 993 * @param {jqLite} elem The jqLite wrapped DOM element 994 * @returns {number} 995 */ 996 getWidth: function(elem) 997 { 998 var val = elem[0].getBoundingClientRect(); 999 elem.rzsw = val.right - val.left; 1000 return elem.rzsw; 1001 }, 1002 1003 /** 1004 * Set element width 1005 * 1006 * @param {jqLite} elem The jqLite wrapped DOM element 1007 * @param {number} width 1008 * @returns {number} 1009 */ 1010 setWidth: function(elem, width) 1011 { 1012 elem.rzsw = width; 1013 elem.css({width: width + 'px'}); 1014 return width; 1015 }, 1016 1017 /** 1018 * Translate value to pixel offset 1019 * 1020 * @param {number} val 1021 * @returns {number} 1022 */ 1023 valueToOffset: function(val) 1024 { 1025 return (val - this.minValue) * this.maxLeft / this.valueRange || 0; 1026 }, 1027 1028 /** 1029 * Translate offset to model value 1030 * 1031 * @param {number} offset 1032 * @returns {number} 1033 */ 1034 offsetToValue: function(offset) 1035 { 1036 return (offset / this.maxLeft) * this.valueRange + this.minValue; 1037 }, 1038 1039 // Events 1040 1041 /** 1042 * Get the X-coordinate of an event 1043 * 1044 * @param {Object} event The event 1045 * @returns {number} 1046 */ 1047 getEventX: function(event) 1048 { 1049 /* http://stackoverflow.com/a/12336075/282882 */ 1050 //noinspection JSLint 1051 if('clientX' in event) 1052 { 1053 return event.clientX; 1054 } 1055 1056 return event.originalEvent === undefined ? 1057 event.touches[0].clientX 1058 : event.originalEvent.touches[0].clientX; 1059 }, 1060 1061 /** 1062 * Get the handle closest to an event. 1063 * 1064 * @param event {Event} The event 1065 * @returns {jqLite} The handle closest to the event. 1066 */ 1067 getNearestHandle: function(event) 1068 { 1069 if (!this.range) { return this.minH; } 1070 var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth; 1071 return Math.abs(offset - this.minH.rzsl) < Math.abs(offset - this.maxH.rzsl) ? this.minH : this.maxH; 1072 }, 1073 1074 /** 1075 * Bind mouse and touch events to slider handles 1076 * 1077 * @returns {undefined} 1078 */ 1079 bindEvents: function() 1080 { 1081 if(this.presentOnly || this.disabled) return; 1082 var barTracking, barStart, barMove; 1083 1084 if (this.dragRange) 1085 { 1086 barTracking = 'rzSliderDrag'; 1087 barStart = this.onDragStart; 1088 barMove = this.onDragMove; 1089 } 1090 else 1091 { 1092 barTracking = 'rzSliderModel'; 1093 barStart = this.onStart; 1094 barMove = this.onMove; 1095 } 1096 1097 this.minH.on('mousedown', angular.bind(this, this.onStart, this.minH, 'rzSliderModel')); 1098 if(this.range) { this.maxH.on('mousedown', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')); } 1099 this.fullBar.on('mousedown', angular.bind(this, this.onStart, null, null)); 1100 this.fullBar.on('mousedown', angular.bind(this, this.onMove, this.fullBar)); 1101 this.selBar.on('mousedown', angular.bind(this, barStart, null, barTracking)); 1102 this.selBar.on('mousedown', angular.bind(this, barMove, this.selBar)); 1103 this.ticks.on('mousedown', angular.bind(this, this.onStart, null, null)); 1104 this.ticks.on('mousedown', angular.bind(this, this.onMove, this.ticks)); 1105 1106 this.minH.on('touchstart', angular.bind(this, this.onStart, this.minH, 'rzSliderModel')); 1107 if(this.range) { this.maxH.on('touchstart', angular.bind(this, this.onStart, this.maxH, 'rzSliderHigh')); } 1108 this.fullBar.on('touchstart', angular.bind(this, this.onStart, null, null)); 1109 this.fullBar.on('touchstart', angular.bind(this, this.onMove, this.fullBar)); 1110 this.selBar.on('touchstart', angular.bind(this, barStart, null, barTracking)); 1111 this.selBar.on('touchstart', angular.bind(this, barMove, this.selBar)); 1112 this.ticks.on('touchstart', angular.bind(this, this.onStart, null, null)); 1113 this.ticks.on('touchstart', angular.bind(this, this.onMove, this.ticks)); 1114 }, 1115 1116 /** 1117 * Unbind mouse and touch events to slider handles 1118 * 1119 * @returns {undefined} 1120 */ 1121 unbindEvents: function() 1122 { 1123 this.minH.off(); 1124 this.maxH.off(); 1125 this.fullBar.off(); 1126 this.selBar.off(); 1127 this.ticks.off(); 1128 }, 1129 1130 /** 1131 * onStart event handler 1132 * 1133 * @param {?Object} pointer The jqLite wrapped DOM element; if null, the closest handle is used 1134 * @param {?string} ref The name of the handle being changed; if null, the closest handle's value is modified 1135 * @param {Event} event The event 1136 * @returns {undefined} 1137 */ 1138 onStart: function (pointer, ref, event) 1139 { 1140 var ehMove, ehEnd, 1141 eventNames = this.getEventNames(event); 1142 1143 event.stopPropagation(); 1144 event.preventDefault(); 1145 1146 if(this.tracking !== '') { return; } 1147 1148 // We have to do this in case the HTML where the sliders are on 1149 // have been animated into view. 1150 this.calcViewDimensions(); 1151 1152 if(pointer) 1153 { 1154 this.tracking = ref; 1155 } 1156 else 1157 { 1158 pointer = this.getNearestHandle(event); 1159 this.tracking = pointer === this.minH ? 'rzSliderModel' : 'rzSliderHigh'; 1160 } 1161 1162 pointer.addClass('rz-active'); 1163 1164 ehMove = angular.bind(this, this.dragging.active ? this.onDragMove : this.onMove, pointer); 1165 ehEnd = angular.bind(this, this.onEnd, ehMove); 1166 1167 $document.on(eventNames.moveEvent, ehMove); 1168 $document.one(eventNames.endEvent, ehEnd); 1169 this.callOnStart(); 1170 }, 1171 1172 /** 1173 * onMove event handler 1174 * 1175 * @param {jqLite} pointer 1176 * @param {Event} event The event 1177 * @returns {undefined} 1178 */ 1179 onMove: function (pointer, event) 1180 { 1181 var eventX = this.getEventX(event), 1182 sliderLO, newOffset, newValue; 1183 1184 sliderLO = this.sliderElem.rzsl; 1185 newOffset = eventX - sliderLO - this.handleHalfWidth; 1186 1187 if(newOffset <= 0) 1188 { 1189 if(pointer.rzsl === 0) 1190 return; 1191 newValue = this.minValue; 1192 newOffset = 0; 1193 } 1194 else if(newOffset >= this.maxLeft) 1195 { 1196 if(pointer.rzsl === this.maxLeft) 1197 return; 1198 newValue = this.maxValue; 1199 newOffset = this.maxLeft; 1200 } 1201 else { 1202 newValue = this.offsetToValue(newOffset); 1203 newValue = this.roundStep(newValue); 1204 newOffset = this.valueToOffset(newValue); 1205 } 1206 this.positionTrackingHandle(newValue, newOffset); 1207 }, 1208 1209 /** 1210 * onDragStart event handler 1211 * 1212 * Handles dragging of the middle bar. 1213 * 1214 * @param {Object} pointer The jqLite wrapped DOM element 1215 * @param {string} ref One of the refLow, refHigh values 1216 * @param {Event} event The event 1217 * @returns {undefined} 1218 */ 1219 onDragStart: function(pointer, ref, event) 1220 { 1221 var offset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth; 1222 this.dragging = { 1223 active: true, 1224 value: this.offsetToValue(offset), 1225 difference: this.scope.rzSliderHigh - this.scope.rzSliderModel, 1226 offset: offset, 1227 lowDist: offset - this.minH.rzsl, 1228 highDist: this.maxH.rzsl - offset 1229 }; 1230 this.minH.addClass('rz-active'); 1231 this.maxH.addClass('rz-active'); 1232 1233 this.onStart(pointer, ref, event); 1234 }, 1235 1236 /** 1237 * onDragMove event handler 1238 * 1239 * Handles dragging of the middle bar. 1240 * 1241 * @param {jqLite} pointer 1242 * @param {Event} event The event 1243 * @returns {undefined} 1244 */ 1245 onDragMove: function(pointer, event) 1246 { 1247 var newOffset = this.getEventX(event) - this.sliderElem.rzsl - this.handleHalfWidth, 1248 newMinOffset, newMaxOffset, 1249 newMinValue, newMaxValue; 1250 1251 if (newOffset <= this.dragging.lowDist) 1252 { 1253 if (pointer.rzsl === this.dragging.lowDist) { return; } 1254 newMinValue = this.minValue; 1255 newMinOffset = 0; 1256 newMaxValue = this.dragging.difference; 1257 newMaxOffset = this.valueToOffset(newMaxValue); 1258 } 1259 else if (newOffset >= this.maxLeft - this.dragging.highDist) 1260 { 1261 if (pointer.rzsl === this.dragging.highDist) { return; } 1262 newMaxValue = this.maxValue; 1263 newMaxOffset = this.maxLeft; 1264 newMinValue = this.maxValue - this.dragging.difference; 1265 newMinOffset = this.valueToOffset(newMinValue); 1266 } 1267 else 1268 { 1269 newMinValue = this.offsetToValue(newOffset - this.dragging.lowDist); 1270 newMinValue = this.roundStep(newMinValue); 1271 newMinOffset = this.valueToOffset(newMinValue); 1272 newMaxValue = newMinValue + this.dragging.difference; 1273 newMaxOffset = this.valueToOffset(newMaxValue); 1274 } 1275 1276 this.positionTrackingBar(newMinValue, newMaxValue, newMinOffset, newMaxOffset); 1277 }, 1278 1279 /** 1280 * Set the new value and offset for the entire bar 1281 * 1282 * @param {number} newMinValue the new minimum value 1283 * @param {number} newMaxValue the new maximum value 1284 * @param {number} newMinOffset the new minimum offset 1285 * @param {number} newMaxOffset the new maximum offset 1286 */ 1287 positionTrackingBar: function(newMinValue, newMaxValue, newMinOffset, newMaxOffset) 1288 { 1289 this.scope.rzSliderModel = newMinValue; 1290 this.scope.rzSliderHigh = newMaxValue; 1291 this.updateHandles('rzSliderModel', newMinOffset); 1292 this.updateHandles('rzSliderHigh', newMaxOffset); 1293 this.scope.$apply(); 1294 this.callOnChange(); 1295 }, 1296 1297 /** 1298 * Set the new value and offset to the current tracking handle 1299 * 1300 * @param {number} newValue new model value 1301 * @param {number} newOffset new offset value 1302 */ 1303 positionTrackingHandle: function(newValue, newOffset) 1304 { 1305 if(this.range) 1306 { 1307 /* This is to check if we need to switch the min and max handles*/ 1308 if (this.tracking === 'rzSliderModel' && newValue >= this.scope.rzSliderHigh) 1309 { 1310 this.scope[this.tracking] = this.scope.rzSliderHigh; 1311 this.updateHandles(this.tracking, this.maxH.rzsl); 1312 this.tracking = 'rzSliderHigh'; 1313 this.minH.removeClass('rz-active'); 1314 this.maxH.addClass('rz-active'); 1315 /* We need to apply here because we are not sure that we will enter the next block */ 1316 this.scope.$apply(); 1317 this.callOnChange(); 1318 } 1319 else if(this.tracking === 'rzSliderHigh' && newValue <= this.scope.rzSliderModel) 1320 { 1321 this.scope[this.tracking] = this.scope.rzSliderModel; 1322 this.updateHandles(this.tracking, this.minH.rzsl); 1323 this.tracking = 'rzSliderModel'; 1324 this.maxH.removeClass('rz-active'); 1325 this.minH.addClass('rz-active'); 1326 /* We need to apply here because we are not sure that we will enter the next block */ 1327 this.scope.$apply(); 1328 this.callOnChange(); 1329 } 1330 } 1331 1332 if(this.scope[this.tracking] !== newValue) 1333 { 1334 this.scope[this.tracking] = newValue; 1335 this.updateHandles(this.tracking, newOffset); 1336 this.scope.$apply(); 1337 this.callOnChange(); 1338 } 1339 }, 1340 1341 /** 1342 * onEnd event handler 1343 * 1344 * @param {Event} event The event 1345 * @param {Function} ehMove The the bound move event handler 1346 * @returns {undefined} 1347 */ 1348 onEnd: function(ehMove, event) 1349 { 1350 var moveEventName = this.getEventNames(event).moveEvent; 1351 1352 this.minH.removeClass('rz-active'); 1353 this.maxH.removeClass('rz-active'); 1354 1355 $document.off(moveEventName, ehMove); 1356 1357 this.scope.$emit('slideEnded'); 1358 this.tracking = ''; 1359 1360 this.dragging.active = false;
1361 this.callOnEnd(); 1362 }, 1363 1364 /** 1365 * Get event names for move and event end 1366 * 1367 * @param {Event} event The event 1368 * 1369 * @return {{moveEvent: string, endEvent: string}} 1370 */ 1371 getEventNames: function(event) 1372 { 1373 var eventNames = {moveEvent: '', endEvent: ''}; 1374 1375 if(event.touches || (event.originalEvent !== undefined && event.originalEvent.touches)) 1376 { 1377 eventNames.moveEvent = 'touchmove'; 1378 eventNames.endEvent = 'touchend'; 1379 } 1380 else 1381 { 1382 eventNames.moveEvent = 'mousemove'; 1383 eventNames.endEvent = 'mouseup'; 1384 } 1385 1386 return eventNames; 1387 } 1388 }; 1389 1390 return Slider; 1391}]) 1392 1393.directive('rzslider', ['RzSlider', function(RzSlider) 1394{ 1395 'use strict'; 1396 1397 return { 1398 restrict: 'E', 1399 scope: { 1400 rzSliderFloor: '=?', 1401 rzSliderCeil: '=?', 1402 rzSliderStep: '@', 1403 rzSliderPrecision: '@', 1404 rzSliderModel: '=?', 1405 rzSliderHigh: '=?', 1406 rzSliderDraggable: '@', 1407 rzSliderTranslate: '&', 1408 rzSliderHideLimitLabels: '=?', 1409 rzSliderAlwaysShowBar: '=?', 1410 rzSliderPresentOnly: '@', 1411 rzSliderOnStart: '&', 1412 rzSliderOnChange: '&', 1413 rzSliderOnEnd: '&', 1414 rzSliderShowTicks: '=?', 1415 rzSliderShowTicksValue: '=?', 1416 rzSliderDisabled: '=?', 1417 }, 1418 1419 /** 1420 * Return template URL 1421 * 1422 * @param {jqLite} elem 1423 * @param {Object} attrs 1424 * @return {string} 1425 */ 1426 templateUrl: function(elem, attrs) { 1427 //noinspection JSUnresolvedVariable 1428 return attrs.rzSliderTplUrl || 'rzSliderTpl.html'; 1429 }, 1430 1431 link: function(scope, elem, attr) 1432 { 1433 return new RzSlider(scope, elem, attr); 1434 } 1435 }; 1436}]); 1437 1438// IDE assist 1439 1440/** 1441 * @name ngScope 1442 * 1443 * @property {number} rzSliderModel 1444 * @property {number} rzSliderHigh 1445 * @property {number} rzSliderCeil 1446 */ 1447 1448/** 1449 * @name jqLite 1450 * 1451 * @property {number|undefined} rzsl rzslider label left offset 1452 * @property {number|undefined} rzsw rzslider element width 1453 * @property {string|undefined} rzsv rzslider label value/text 1454 * @property {Function} css 1455 * @property {Function} text 1456 */ 1457 1458/** 1459 * @name Event 1460 * @property {Array} touches 1461 * @property {Event} originalEvent 1462 */ 1463 1464/** 1465 * @name ThrottleOptions 1466 * 1467 * @property {boolean} leading 1468 * @property {boolean} trailing 1469 */ 1470 1471 module.run(['$templateCache', function($templateCache) { 1472 'use strict'; 1473 1474 $templateCache.put('rzSliderTpl.html', 1475 "<span class=rz-bar-wrapper><span class=rz-bar></span></span> <span class=rz-bar-wrapper><span class=\"rz-bar rz-selection\"></span></span> <span class=rz-pointer></span> <span class=rz-pointer></span> <span class=\"rz-bubble rz-limit\"></span> <span class=\"rz-bubble rz-limit\"></span> <span class=rz-bubble></span> <span class=rz-bubble></span> <span class=rz-bubble></span><ul class=rz-ticks></ul>" 1476 ); 1477 1478}]); 1479 1480 return module 1481}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.