1/*! angularjs-slider - v5.5.1 - 2 (c) Rafal Zajac <[email protected]>, Valentin Hervieu <[email protected]>, Jussi Saarivirta <[email protected]>, Angelin Sirbu <[email protected]> - 3 https://github.com/angular-slider/angularjs-slider - 4 2016-09-22 */ 5/*jslint unparam: true */ 6/*global angular: false, console: false, define, module */ 7(function(root, factory) { 8 'use strict'; 9 /* istanbul ignore next */ 10 if (typeof define === 'function' && define.amd) { 11 // AMD. Register as an anonymous module. 12 define(['angular'], factory); 13 } else if (typeof module === 'object' && module.exports) { 14 // Node. Does not work with strict CommonJS, but 15 // only CommonJS-like environments that support module.exports, 16 // like Node. 17 // to support bundler like browserify 18 module.exports = factory(require('angular')); 19 } else { 20 // Browser globals (root is window) 21 factory(root.angular); 22 } 23 24}(this, function(angular) { 25 'use strict'; 26 var module = angular.module('rzModule', []) 27 28 .factory('RzSliderOptions', function() { 29 var defaultOptions = { 30 floor: 0, 31 ceil: null, //defaults to rz-slider-model 32 step: 1, 33 precision: 0, 34 minRange: null, 35 maxRange: null, 36 pushRange: false, 37 minLimit: null, 38 maxLimit: null, 39 id: null, 40 translate: null, 41 getLegend: null, 42 stepsArray: null, 43 bindIndexForStepsArray: false, 44 draggableRange: false, 45 draggableRangeOnly: false, 46 showSelectionBar: false, 47 showSelectionBarEnd: false, 48 showSelectionBarFromValue: null, 49 hidePointerLabels: false, 50 hideLimitLabels: false, 51 autoHideLimitLabels: true, 52 readOnly: false, 53 disabled: false, 54 interval: 350, 55 showTicks: false, 56 showTicksValues: false, 57 ticksTooltip: null, 58 ticksValuesTooltip: null, 59 vertical: false, 60 getSelectionBarColor: null, 61 getTickColor: null, 62 getPointerColor: null, 63 keyboardSupport: true, 64 scale: 1, 65 enforceStep: true, 66 enforceRange: false, 67 noSwitching: false, 68 onlyBindHandles: false, 69 onStart: null, 70 onChange: null, 71 onEnd: null, 72 rightToLeft: false, 73 boundPointerLabels: true, 74 mergeRangeLabelsIfSame: false, 75 customTemplateScope: null 76 }; 77 var globalOptions = {}; 78 79 var factory = {}; 80 /** 81 * `options({})` allows global configuration of all sliders in the 82 * application. 83 * 84 * var app = angular.module( 'App', ['rzModule'], function( RzSliderOptions ) { 85 * // show ticks for all sliders 86 * RzSliderOptions.options( { showTicks: true } ); 87 * }); 88 */ 89 factory.options = function(value) { 90 angular.extend(globalOptions, value); 91 }; 92 93 factory.getOptions = function(options) { 94 return angular.extend({}, defaultOptions, globalOptions, options); 95 }; 96 97 return factory; 98 }) 99 100 .factory('rzThrottle', ['$timeout', function($timeout) { 101 /** 102 * rzThrottle 103 * 104 * Taken from underscore project 105 * 106 * @param {Function} func 107 * @param {number} wait 108 * @param {ThrottleOptions} options 109 * @returns {Function} 110 */ 111 return function(func, wait, options) { 112 'use strict'; 113 /* istanbul ignore next */ 114 var getTime = (Date.now || function() { 115 return new Date().getTime(); 116 }); 117 var context, args, result; 118 var timeout = null; 119 var previous = 0; 120 options = options || {}; 121 var later = function() { 122 previous = getTime(); 123 timeout = null; 124 result = func.apply(context, args); 125 context = args = null; 126 }; 127 return function() { 128 var now = getTime(); 129 var remaining = wait - (now - previous); 130 context = this; 131 args = arguments; 132 if (remaining <= 0) { 133 $timeout.cancel(timeout); 134 timeout = null; 135 previous = now;
136 result = func.apply(context, args); 137 context = args = null; 138 } else if (!timeout && options.trailing !== false) { 139 timeout = $timeout(later, remaining); 140 } 141 return result; 142 }; 143 } 144 }]) 145 146 .factory('RzSlider', ['$timeout', '$document', '$window', '$compile', 'RzSliderOptions', 'rzThrottle', function($timeout, $document, $window, $compile, RzSliderOptions, rzThrottle) { 147 'use strict'; 148 149 /** 150 * Slider 151 * 152 * @param {ngScope} scope The AngularJS scope 153 * @param {Element} sliderElem The slider directive element wrapped in jqLite 154 * @constructor 155 */ 156 var Slider = function(scope, sliderElem) { 157 /** 158 * The slider's scope 159 * 160 * @type {ngScope} 161 */ 162 this.scope = scope; 163 164 /** 165 * The slider inner low value (linked to rzSliderModel) 166 * @type {number} 167 */ 168 this.lowValue = 0; 169 170 /** 171 * The slider inner high value (linked to rzSliderHigh) 172 * @type {number} 173 */ 174 this.highValue = 0; 175 176 /** 177 * Slider element wrapped in jqLite 178 * 179 * @type {jqLite} 180 */ 181 this.sliderElem = sliderElem; 182 183 /**
184 * Slider type 185 * 186 * @type {boolean} Set to true for range slider 187 */ 188 this.range = this.scope.rzSliderModel !== undefined && this.scope.rzSliderHigh !== undefined; 189 190 /** 191 * Values recorded when first dragging the bar 192 * 193 * @type {Object} 194 */ 195 this.dragging = { 196 active: false, 197 value: 0, 198 difference: 0, 199 offset: 0, 200 lowLimit: 0, 201 highLimit: 0 202 }; 203 204 /** 205 * property that handle position (defaults to left for horizontal) 206 * @type {string} 207 */ 208 this.positionProperty = 'left'; 209 210 /** 211 * property that handle dimension (defaults to width for horizontal) 212 * @type {string} 213 */ 214 this.dimensionProperty = 'width'; 215 216 /** 217 * Half of the width or height of the slider handles 218 * 219 * @type {number} 220 */ 221 this.handleHalfDim = 0; 222 223 /** 224 * Maximum position the slider handle can have 225 * 226 * @type {number} 227 */ 228 this.maxPos = 0; 229 230 /** 231 * Precision 232 * 233 * @type {number} 234 */ 235 this.precision = 0; 236 237 /** 238 * Step 239 * 240 * @type {number} 241 */ 242 this.step = 1; 243 244 /** 245 * The name of the handle we are currently tracking 246 * 247 * @type {string} 248 */ 249 this.tracking = ''; 250 251 /** 252 * Minimum value (floor) of the model 253 * 254 * @type {number} 255 */ 256 this.minValue = 0; 257 258 /** 259 * Maximum value (ceiling) of the model 260 * 261 * @type {number} 262 */ 263 this.maxValue = 0; 264 265 266 /** 267 * The delta between min and max value 268 * 269 * @type {number} 270 */ 271 this.valueRange = 0; 272 273 274 /** 275 * If showTicks/showTicksValues options are number. 276 * In this case, ticks values should be displayed below the slider. 277 * @type {boolean} 278 */ 279 this.intermediateTicks = false; 280 281 /** 282 * Set to true if init method already executed 283 * 284 * @type {boolean} 285 */ 286 this.initHasRun = false; 287 288 /** 289 * Used to call onStart on the first keydown event 290 * 291 * @type {boolean} 292 */ 293 this.firstKeyDown = false; 294 295 /** 296 * Internal flag to prevent watchers to be called when the sliders value are modified internally. 297 * @type {boolean} 298 */ 299 this.internalChange = false; 300 301 /** 302 * Internal flag to keep track of the visibility of combo label 303 * @type {boolean} 304 */ 305 this.cmbLabelShown = false; 306 307 /** 308 * Internal variable to keep track of the focus element 309 */ 310 this.currentFocusElement = null; 311 312 // Slider DOM elements wrapped in jqLite 313 this.fullBar = null; // The whole slider bar 314 this.selBar = null; // Highlight between two handles 315 this.minH = null; // Left slider handle 316 this.maxH = null; // Right slider handle 317 this.flrLab = null; // Floor label 318 this.ceilLab = null; // Ceiling label 319 this.minLab = null; // Label above the low value 320 this.maxLab = null; // Label above the high value 321 this.cmbLab = null; // Combined label 322 this.ticks = null; // The ticks 323 324 // Initialize slider 325 this.init(); 326 }; 327 328 // Add instance methods 329 Slider.prototype = { 330 331 /** 332 * Initialize slider 333 * 334 * @returns {undefined} 335 */ 336 init: function() { 337 var thrLow, thrHigh, 338 self = this; 339 340 var calcDimFn = function() { 341 self.calcViewDimensions(); 342 }; 343 344 this.applyOptions(); 345 this.syncLowValue(); 346 if (this.range) 347 this.syncHighValue(); 348 this.initElemHandles(); 349 this.manageElementsStyle(); 350 this.setDisabledState(); 351 this.calcViewDimensions(); 352 this.setMinAndMax(); 353 this.addAccessibility(); 354 this.updateCeilLab(); 355 this.updateFloorLab(); 356 this.initHandles(); 357 this.manageEventsBindings(); 358 359 // Recalculate slider view dimensions 360 this.scope.$on('reCalcViewDimensions', calcDimFn); 361 362 // Recalculate stuff if view port dimensions have changed 363 angular.element($window).on('resize', calcDimFn); 364 365 this.initHasRun = true; 366 367 // Watch for changes to the model 368 thrLow = rzThrottle(function() { 369 self.onLowHandleChange(); 370 }, self.options.interval); 371 372 thrHigh = rzThrottle(function() { 373 self.onHighHandleChange(); 374 }, self.options.interval); 375 376 this.scope.$on('rzSliderForceRender', function() { 377 self.resetLabelsValue(); 378 thrLow(); 379 if (self.range) { 380 thrHigh(); 381 } 382 self.resetSlider(); 383 }); 384 385 // Watchers (order is important because in case of simultaneous change, 386 // watchers will be called in the same order) 387 this.scope.$watch('rzSliderOptions()', function(newValue, oldValue) { 388 if (newValue === oldValue) 389 return; 390 self.applyOptions(); // need to be called before synchronizing the values 391 self.syncLowValue(); 392 if (self.range) 393 self.syncHighValue(); 394 self.resetSlider(); 395 }, true); 396 397 this.scope.$watch('rzSliderModel', function(newValue, oldValue) { 398 if (self.internalChange) 399 return; 400 if (newValue === oldValue) 401 return; 402 thrLow(); 403 }); 404 405 this.scope.$watch('rzSliderHigh', function(newValue, oldValue) { 406 if (self.internalChange) 407 return; 408 if (newValue === oldValue) 409 return; 410 if (newValue != null) 411 thrHigh(); 412 if (self.range && newValue == null || !self.range && newValue != null) { 413 self.applyOptions(); 414 self.resetSlider(); 415 } 416 }); 417 418 this.scope.$on('$destroy', function() { 419 self.unbindEvents(); 420 angular.element($window).off('resize', calcDimFn); 421 self.currentFocusElement = null; 422 }); 423 }, 424 425 findStepIndex: function(modelValue) { 426 var index = 0; 427 for (var i = 0; i < this.options.stepsArray.length; i++) { 428 var step = this.options.stepsArray[i]; 429 if (step === modelValue) { 430 index = i; 431 break; 432 } 433 else if (angular.isObject(step) && step.value === modelValue) { 434 index = i; 435 break; 436 } 437 } 438 return index; 439 }, 440 441 syncLowValue: function() { 442 if (this.options.stepsArray) { 443 if (!this.options.bindIndexForStepsArray) 444 this.lowValue = this.findStepIndex(this.scope.rzSliderModel); 445 else 446 this.lowValue = this.scope.rzSliderModel 447 } 448 else 449 this.lowValue = this.scope.rzSliderModel; 450 }, 451 452 syncHighValue: function() { 453 if (this.options.stepsArray) { 454 if (!this.options.bindIndexForStepsArray) 455 this.highValue = this.findStepIndex(this.scope.rzSliderHigh); 456 else 457 this.highValue = this.scope.rzSliderHigh 458 } 459 else 460 this.highValue = this.scope.rzSliderHigh; 461 }, 462 463 getStepValue: function(sliderValue) { 464 var step = this.options.stepsArray[sliderValue]; 465 if (angular.isObject(step)) 466 return step.value; 467 return step; 468 }, 469 470 applyLowValue: function() { 471 if (this.options.stepsArray) { 472 if (!this.options.bindIndexForStepsArray)
473 this.scope.rzSliderModel = this.getStepValue(this.lowValue); 474 else 475 this.scope.rzSliderModel = this.lowValue 476 } 477 else 478 this.scope.rzSliderModel = this.lowValue; 479 }, 480 481 applyHighValue: function() { 482 if (this.options.stepsArray) { 483 if (!this.options.bindIndexForStepsArray) 484 this.scope.rzSliderHigh = this.getStepValue(this.highValue); 485 else 486 this.scope.rzSliderHigh = this.highValue 487 } 488 else 489 this.scope.rzSliderHigh = this.highValue; 490 }, 491 492 /* 493 * Reflow the slider when the low handle changes (called with throttle) 494 */ 495 onLowHandleChange: function() { 496 this.syncLowValue(); 497 if (this.range) 498 this.syncHighValue(); 499 this.setMinAndMax(); 500 this.updateLowHandle(this.valueToOffset(this.lowValue)); 501 this.updateSelectionBar(); 502 this.updateTicksScale(); 503 this.updateAriaAttributes(); 504 if (this.range) { 505 this.updateCmbLabel(); 506 } 507 }, 508 509 /* 510 * Reflow the slider when the high handle changes (called with throttle) 511 */ 512 onHighHandleChange: function() { 513 this.syncLowValue(); 514 this.syncHighValue(); 515 this.setMinAndMax(); 516 this.updateHighHandle(this.valueToOffset(this.highValue)); 517 this.updateSelectionBar(); 518 this.updateTicksScale(); 519 this.updateCmbLabel(); 520 this.updateAriaAttributes(); 521 }, 522 523 /** 524 * Read the user options and apply them to the slider model 525 */ 526 applyOptions: function() { 527 var sliderOptions; 528 if (this.scope.rzSliderOptions) 529 sliderOptions = this.scope.rzSliderOptions(); 530 else 531 sliderOptions = {}; 532 533 this.options = RzSliderOptions.getOptions(sliderOptions); 534 535 if (this.options.step <= 0) 536 this.options.step = 1; 537 538 this.range = this.scope.rzSliderModel !== undefined && this.scope.rzSliderHigh !== undefined; 539 this.options.draggableRange = this.range && this.options.draggableRange; 540 this.options.draggableRangeOnly = this.range && this.options.draggableRangeOnly; 541 if (this.options.draggableRangeOnly) { 542 this.options.draggableRange = true; 543 } 544 545 this.options.showTicks = this.options.showTicks || this.options.showTicksValues; 546 this.scope.showTicks = this.options.showTicks; //scope is used in the template 547 if (angular.isNumber(this.options.showTicks)) 548 this.intermediateTicks = true; 549 550 this.options.showSelectionBar = this.options.showSelectionBar || this.options.showSelectionBarEnd 551 || this.options.showSelectionBarFromValue !== null; 552 553 if (this.options.stepsArray) { 554 this.parseStepsArray(); 555 } else { 556 if (this.options.translate) 557 this.customTrFn = this.options.translate; 558 else 559 this.customTrFn = function(value) { 560 return String(value); 561 }; 562 563 this.getLegend = this.options.getLegend; 564 } 565 566 if (this.options.vertical) { 567 this.positionProperty = 'bottom'; 568 this.dimensionProperty = 'height'; 569 } 570 571 if (this.options.customTemplateScope) 572 this.scope.custom = this.options.customTemplateScope; 573 }, 574 575 parseStepsArray: function() { 576 this.options.floor = 0; 577 this.options.ceil = this.options.stepsArray.length - 1; 578 this.options.step = 1; 579 580 if (this.options.translate) { 581 this.customTrFn = this.options.translate; 582 } 583 else { 584 this.customTrFn = function(modelValue) { 585 if (this.options.bindIndexForStepsArray)
586 return this.getStepValue(modelValue); 587 return modelValue; 588 }; 589 } 590 591 this.getLegend = function(index) { 592 var step = this.options.stepsArray[index]; 593 if (angular.isObject(step)) 594 return step.legend; 595 return null; 596 }; 597 }, 598 599 /** 600 * Resets slider 601 * 602 * @returns {undefined} 603 */ 604 resetSlider: function() { 605 this.manageElementsStyle(); 606 this.addAccessibility(); 607 this.setMinAndMax(); 608 this.updateCeilLab(); 609 this.updateFloorLab(); 610 this.unbindEvents(); 611 this.manageEventsBindings(); 612 this.setDisabledState(); 613 this.calcViewDimensions(); 614 this.refocusPointerIfNeeded(); 615 }, 616 617 refocusPointerIfNeeded: function() { 618 if (this.currentFocusElement) { 619 this.onPointerFocus(this.currentFocusElement.pointer, this.currentFocusElement.ref); 620 this.focusElement(this.currentFocusElement.pointer) 621 } 622 }, 623 624 /** 625 * Set the slider children to variables for easy access 626 * 627 * Run only once during initialization 628 * 629 * @returns {undefined} 630 */ 631 initElemHandles: function() { 632 // Assign all slider elements to object properties for easy access 633 angular.forEach(this.sliderElem.children(), function(elem, index) { 634 var jElem = angular.element(elem); 635 636 switch (index) { 637 case 0: 638 this.fullBar = jElem; 639 break; 640 case 1: 641 this.selBar = jElem; 642 break; 643 case 2: 644 this.minH = jElem; 645 break; 646 case 3: 647 this.maxH = jElem; 648 break; 649 case 4: 650 this.flrLab = jElem; 651 break; 652 case 5: 653 this.ceilLab = jElem; 654 break; 655 case 6: 656 this.minLab = jElem; 657 break; 658 case 7: 659 this.maxLab = jElem; 660 break; 661 case 8: 662 this.cmbLab = jElem; 663 break; 664 case 9: 665 this.ticks = jElem; 666 break; 667 } 668 669 }, this); 670 671 // Initialize offset cache properties 672 this.selBar.rzsp = 0; 673 this.minH.rzsp = 0; 674 this.maxH.rzsp = 0; 675 this.flrLab.rzsp = 0; 676 this.ceilLab.rzsp = 0; 677 this.minLab.rzsp = 0; 678 this.maxLab.rzsp = 0; 679 this.cmbLab.rzsp = 0; 680 }, 681 682 /** 683 * Update each elements style based on options 684 */ 685 manageElementsStyle: function() { 686 687 if (!this.range) 688 this.maxH.css('display', 'none'); 689 else 690 this.maxH.css('display', ''); 691 692 693 this.alwaysHide(this.flrLab, this.options.showTicksValues || this.options.hideLimitLabels); 694 this.alwaysHide(this.ceilLab, this.options.showTicksValues || this.options.hideLimitLabels); 695 696 var hideLabelsForTicks = this.options.showTicksValues && !this.intermediateTicks; 697 this.alwaysHide(this.minLab, hideLabelsForTicks || this.options.hidePointerLabels); 698 this.alwaysHide(this.maxLab, hideLabelsForTicks || !this.range || this.options.hidePointerLabels); 699 this.alwaysHide(this.cmbLab, hideLabelsForTicks || !this.range || this.options.hidePointerLabels); 700 this.alwaysHide(this.selBar, !this.range && !this.options.showSelectionBar); 701 702 if (this.options.vertical) 703 this.sliderElem.addClass('rz-vertical'); 704 705 if (this.options.draggableRange) 706 this.selBar.addClass('rz-draggable'); 707 else 708 this.selBar.removeClass('rz-draggable'); 709 710 if (this.intermediateTicks && this.options.showTicksValues) 711 this.ticks.addClass('rz-ticks-values-under'); 712 }, 713 714 alwaysHide: function(el, hide) { 715 el.rzAlwaysHide = hide; 716 if (hide) 717 this.hideEl(el); 718 else 719 this.showEl(el) 720 }, 721 722 /** 723 * Manage the events bindings based on readOnly and disabled options 724 * 725 * @returns {undefined} 726 */ 727 manageEventsBindings: function() { 728 if (this.options.disabled || this.options.readOnly) 729 this.unbindEvents(); 730 else 731 this.bindEvents(); 732 }, 733 734 /** 735 * Set the disabled state based on rzSliderDisabled 736 * 737 * @returns {undefined} 738 */ 739 setDisabledState: function() { 740 if (this.options.disabled) { 741 this.sliderElem.attr('disabled', 'disabled'); 742 } else { 743 this.sliderElem.attr('disabled', null); 744 } 745 }, 746 747 /** 748 * Reset label values 749 * 750 * @return {undefined} 751 */ 752 resetLabelsValue: function() { 753 this.minLab.rzsv = undefined; 754 this.maxLab.rzsv = undefined; 755 }, 756 757 /** 758 * Initialize slider handles positions and labels 759 * 760 * Run only once during initialization and every time view port
760changes size 761 * 762 * @returns {undefined} 763 */ 764 initHandles: function() { 765 this.updateLowHandle(this.valueToOffset(this.lowValue)); 766 767 /* 768 the order here is important since the selection bar should be 769 updated after the high handle but before the combined label 770 */ 771 if (this.range) 772 this.updateHighHandle(this.valueToOffset(this.highValue)); 773 this.updateSelectionBar(); 774 if (this.range) 775 this.updateCmbLabel(); 776 777 this.updateTicksScale(); 778 }, 779 780 /** 781 * Translate value to human readable format 782 * 783 * @param {number|string} value 784 * @param {jqLite} label 785 * @param {String} which 786 * @param {boolean} [useCustomTr] 787 * @returns {undefined} 788 */ 789 translateFn: function(value, label, which, useCustomTr) { 790 useCustomTr = useCustomTr === undefined ? true : useCustomTr; 791 792 var valStr = '', 793 getDimension = false, 794 noLabelInjection = label.hasClass('no-label-injection'); 795 796 if (useCustomTr) { 797 if (this.options.stepsArray && !this.options.bindIndexForStepsArray) 798 value = this.getStepValue(value); 799 valStr = String(this.customTrFn(value, this.options.id, which)); 800 } 801 else { 802 valStr = String(value) 803 } 804 805 if (label.rzsv === undefined || label.rzsv.length !== valStr.length || (label.rzsv.length > 0 && label.rzsd === 0)) { 806 getDimension = true; 807 label.rzsv = valStr; 808 } 809 810 if (!noLabelInjection) { 811 label.html(valStr); 812 } 813 ; 814 815 this.scope[which + 'Label'] = valStr; 816 817 // Update width only when length of the label have changed 818 if (getDimension) { 819 this.getDimension(label); 820 } 821 }, 822 823 /** 824 * Set maximum and minimum values for the slider and ensure the model and high 825 * value match these limits 826 * @returns {undefined} 827 */ 828 setMinAndMax: function() { 829 830 this.step = +this.options.step; 831 this.precision = +this.options.precision; 832 833 this.minValue = this.options.floor; 834 835 if (this.options.enforceStep) { 836 this.lowValue = this.roundStep(this.lowValue); 837 if (this.range) 838 this.highValue = this.roundStep(this.highValue); 839 } 840 841 if (this.options.ceil != null) 842 this.maxValue = this.options.ceil; 843 else 844 this.maxValue = this.options.ceil = this.range ? this.highValue : this.lowValue; 845 846 if (this.options.enforceRange) { 847 this.lowValue = this.sanitizeValue(this.lowValue); 848 if (this.range) 849 this.highValue = this.sanitizeValue(this.highValue); 850 } 851 852 this.applyLowValue(); 853 if (this.range) 854 this.applyHighValue(); 855 856 this.valueRange = this.maxValue - this.minValue; 857 }, 858 859 /** 860 * Adds accessibility attributes 861 * 862 * Run only once during initialization 863 * 864 * @returns {undefined} 865 */ 866 addAccessibility: function() { 867 this.minH.attr('role', 'slider'); 868 this.updateAriaAttributes(); 869 if (this.options.keyboardSupport && !(this.options.readOnly || this.options.disabled)) 870 this.minH.attr('tabindex', '0'); 871 else 872 this.minH.attr('tabindex', ''); 873 if (this.options.vertical) 874 this.minH.attr('aria-orientation', 'vertical'); 875 876 if (this.range) { 877 this.maxH.attr('role', 'slider'); 878 if (this.options.keyboardSupport && !(this.options.readOnly || this.options.disabled)) 879 this.maxH.attr('tabindex', '0'); 880 else 881 this.maxH.attr('tabindex', ''); 882 if (this.options.vertical) 883 this.maxH.attr('aria-orientation', 'vertical'); 884 } 885 }, 886 887 /** 888 * Updates aria attributes according to current values 889 */ 890 updateAriaAttributes: function() { 891 this.minH.attr({ 892 'aria-valuenow': this.scope.rzSliderModel, 893 'aria-valuetext': this.customTrFn(this.scope.rzSliderModel, this.options.id, 'model'), 894 'aria-valuemin': this.minValue, 895 'aria-valuemax': this.maxValue 896 }); 897 if (this.range) { 898 this.maxH.attr({ 899 'aria-valuenow': this.scope.rzSliderHigh, 900 'aria-valuetext': this.customTrFn(this.scope.rzSliderHigh, this.options.id, 'high'), 901 'aria-valuemin': this.minValue, 902 'aria-valuemax': this.maxValue 903 }); 904 } 905 }, 906 907 /** 908 * Calculate dimensions that are dependent on view port size 909 * 910 * Run once during initialization and every time view port
910changes size. 911 * 912 * @returns {undefined} 913 */ 914 calcViewDimensions: function() { 915 var handleWidth = this.getDimension(this.minH); 916 917 this.handleHalfDim = handleWidth / 2; 918 this.barDimension = this.getDimension(this.fullBar); 919 920 this.maxPos = this.barDimension - handleWidth; 921 922 this.getDimension(this.sliderElem); 923 this.sliderElem.rzsp = this.sliderElem[0].getBoundingClientRect()[this.positionProperty]; 924 925 if (this.initHasRun) { 926 this.updateFloorLab(); 927 this.updateCeilLab(); 928 this.initHandles(); 929 } 930 }, 931 932 /** 933 * Update the ticks position 934 * 935 * @returns {undefined} 936 */ 937 updateTicksScale: function() { 938 if (!this.options.showTicks) return; 939 var step = this.step; 940 if (this.intermediateTicks) 941 step = this.options.showTicks; 942 var ticksCount = Math.round((this.maxValue - this.minValue) / step) + 1; 943 this.scope.ticks = []; 944 for (var i = 0; i < ticksCount; i++) { 945 var value = this.roundStep(this.minValue + i * step); 946 var tick = { 947 selected: this.isTickSelected(value) 948 }; 949 if (tick.selected && this.options.getSelectionBarColor) { 950 tick.style = { 951 'background-color': this.getSelectionBarColor() 952 }; 953 } 954 if (!tick.selected && this.options.getTickColor) { 955 tick.style = { 956 'background-color': this.getTickColor(value) 957 } 958 } 959 if (this.options.ticksTooltip) { 960 tick.tooltip = this.options.ticksTooltip(value); 961 tick.tooltipPlacement = this.options.vertical ? 'right' : 'top'; 962 } 963 if (this.options.showTicksValues) { 964 tick.value = this.getDisplayValue(value, 'tick-value'); 965 if (this.options.ticksValuesTooltip) { 966 tick.valueTooltip = this.options.ticksValuesTooltip(value); 967 tick.valueTooltipPlacement = this.options.vertical ? 'right' : 'top'; 968 } 969 } 970 if (this.getLegend) { 971 var legend = this.getLegend(value, this.options.id); 972 if (legend) 973 tick.legend = legend; 974 } 975 if (!this.options.rightToLeft) { 976 this.scope.ticks.push(tick); 977 } else { 978 this.scope.ticks.unshift(tick); 979 } 980 } 981 }, 982 983 isTickSelected: function(value) { 984 if (!this.range) { 985 if (this.options.showSelectionBarFromValue !== null) { 986 var center = this.options.showSelectionBarFromValue; 987 if (this.lowValue > center && value >= center && value <= this.lowValue) 988 return true; 989 else if (this.lowValue < center && value <= center && value >= this.lowValue) 990 return true; 991 } 992 else if (this.options.showSelectionBarEnd) { 993 if (value >= this.lowValue) 994 return true; 995 } 996 else if (this.options.showSelectionBar && value <= this.lowValue) 997 return true; 998 } 999 if (this.range && value >= this.lowValue && value <= this.highValue) 1000 return true; 1001 return false; 1002 }, 1003 1004 /** 1005 * Update position of the floor label 1006 * 1007 * @returns {undefined} 1008 */ 1009 updateFloorLab: function() { 1010 this.translateFn(this.minValue, this.flrLab, 'floor'); 1011 this.getDimension(this.flrLab); 1012 var position = this.options.rightToLeft ? this.barDimension - this.flrLab.rzsd : 0; 1013 this.setPosition(this.flrLab, position); 1014 }, 1015 1016 /** 1017 * Update position of the ceiling label 1018 * 1019 * @returns {undefined} 1020 */ 1021 updateCeilLab: function() { 1022 this.translateFn(this.maxValue, this.ceilLab, 'ceil'); 1023 this.getDimension(this.ceilLab); 1024 var position = this.options.rightToLeft ? 0 : this.barDimension - this.ceilLab.rzsd; 1025 this.setPosition(this.ceilLab, position); 1026 }, 1027 1028 /** 1029 * Update slider handles and label positions 1030 * 1031 * @param {string} which 1032 * @param {number} newOffset 1033 */ 1034 updateHandles: function(which, newOffset) { 1035 if (which === 'lowValue') 1036 this.updateLowHandle(newOffset); 1037 else 1038 this.updateHighHandle(newOffset); 1039 1040 this.updateSelectionBar(); 1041 this.updateTicksScale(); 1042 if (this.range) 1043 this.updateCmbLabel(); 1044 }, 1045 1046 /** 1047 * Helper function to work out the position for handle labels depending on RTL or not 1048 * 1049 * @param {string} labelName maxLab or minLab 1050 * @param newOffset 1051 * 1052 * @returns {number} 1053 */ 1054 getHandleLabelPos: function(labelName, newOffset) { 1055 var labelRzsd = this[labelName].rzsd, 1056 nearHandlePos = newOffset - labelRzsd / 2 + this.handleHalfDim, 1057 endOfBarPos = this.barDimension - labelRzsd; 1058 1059 if (!this.options.boundPointerLabels) 1060 return nearHandlePos; 1061 1062 if (this.options.rightToLeft && labelName === 'minLab' || !this.options.rightToLeft && labelName === 'maxLab') { 1063 return Math.min(nearHandlePos, endOfBarPos); 1064 } else { 1065 return Math.min(Math.max(nearHandlePos, 0), endOfBarPos); 1066 } 1067 }, 1068 1069 /** 1070 * Update low slider handle position and label 1071 * 1072 * @param {number} newOffset 1073 * @returns {undefined} 1074 */ 1075 updateLowHandle: function(newOffset) { 1076 this.setPosition(this.minH, newOffset); 1077 this.translateFn(this.lowValue, this.minLab, 'model'); 1078 this.setPosition(this.minLab, this.getHandleLabelPos('minLab', newOffset)); 1079 1080 if (this.options.getPointerColor) { 1081 var pointercolor = this.getPointerColor('min'); 1082 this.scope.minPointerStyle = { 1083 backgroundColor: pointercolor 1084 }; 1085 } 1086 1087 if (this.options.autoHideLimitLabels) { 1088 this.shFloorCeil(); 1089 } 1090 }, 1091 1092 /** 1093 * Update high slider handle position and label 1094 * 1095 * @param {number} newOffset 1096 * @returns {undefined} 1097 */ 1098 updateHighHandle: function(newOffset) { 1099 this.setPosition(this.maxH, newOffset); 1100 this.translateFn(this.highValue, this.maxLab, 'high'); 1101 this.setPosition(this.maxLab, this.getHandleLabelPos('maxLab', newOffset)); 1102 1103 if (this.options.getPointerColor) { 1104 var pointercolor = this.getPointerColor('max'); 1105 this.scope.maxPointerStyle = { 1106 backgroundColor: pointercolor 1107 }; 1108 } 1109 if (this.options.autoHideLimitLabels) { 1110 this.shFloorCeil(); 1111 } 1112 1113 }, 1114 1115 /** 1116 * Show/hide floor/ceiling label 1117 * 1118 * @returns {undefined} 1119 */ 1120 shFloorCeil: function() { 1121 // Show based only on hideLimitLabels if pointer labels are hidden 1122 if (this.options.hidePointerLabels) { 1123 return; 1124 } 1125 var flHidden = false, 1126 clHidden = false, 1127 isRTL = this.options.rightToLeft, 1128 flrLabPos = this.flrLab.rzsp, 1129 flrLabDim = this.flrLab.rzsd, 1130 minLabPos = this.minLab.rzsp, 1131 minLabDim = this.minLab.rzsd, 1132 maxLabPos = this.maxLab.rzsp, 1133 maxLabDim = this.maxLab.rzsd, 1134 cmbLabPos = this.cmbLab.rzsp, 1135 cmbLabDim = this.cmbLab.rzsd,
1136 ceilLabPos = this.ceilLab.rzsp, 1137 halfHandle = this.handleHalfDim, 1138 isMinLabAtFloor = isRTL ? minLabPos + minLabDim >= flrLabPos - flrLabDim - 5 : minLabPos <= flrLabPos + flrLabDim + 5, 1139 isMinLabAtCeil = isRTL ? minLabPos - minLabDim <= ceilLabPos + halfHandle + 10 : minLabPos + minLabDim >= ceilLabPos - halfHandle - 10, 1140 isMaxLabAtFloor = isRTL ? maxLabPos >= flrLabPos - flrLabDim - halfHandle : maxLabPos <= flrLabPos + flrLabDim + halfHandle, 1141 isMaxLabAtCeil = isRTL ? maxLabPos - maxLabDim <= ceilLabPos + 10 : maxLabPos + maxLabDim >= ceilLabPos - 10, 1142 isCmbLabAtFloor = isRTL ? cmbLabPos >= flrLabPos - flrLabDim - halfHandle : cmbLabPos <= flrLabPos + flrLabDim + halfHandle, 1143 isCmbLabAtCeil = isRTL ? cmbLabPos - cmbLabDim <= ceilLabPos + 10 : cmbLabPos + cmbLabDim >= ceilLabPos - 10 1144 1145 1146 if (isMinLabAtFloor) { 1147 flHidden = true; 1148 this.hideEl(this.flrLab); 1149 } else { 1150 flHidden = false; 1151 this.showEl(this.flrLab); 1152 } 1153 1154 if (isMinLabAtCeil) { 1155 clHidden = true; 1156 this.hideEl(this.ceilLab); 1157 } else { 1158 clHidden = false; 1159 this.showEl(this.ceilLab); 1160 } 1161 1162 if (this.range) { 1163 var hideCeil = this.cmbLabelShown ? isCmbLabAtCeil : isMaxLabAtCeil; 1164 var hideFloor = this.cmbLabelShown ? isCmbLabAtFloor : isMinLabAtFloor; 1165 1166 if (hideCeil) { 1167 this.hideEl(this.ceilLab); 1168 } else if (!clHidden) { 1169 this.showEl(this.ceilLab); 1170 } 1171 1172 // Hide or show floor label 1173 if (hideFloor) { 1174 this.hideEl(this.flrLab); 1175 } else if (!flHidden) { 1176 this.showEl(this.flrLab); 1177 } 1178 } 1179 }, 1180 1181 /** 1182 * Update slider selection bar, combined label and range label 1183 * 1184 * @returns {undefined} 1185 */ 1186 updateSelectionBar: function() { 1187 var position = 0, 1188 dimension = 0, 1189 isSelectionBarFromRight = this.options.rightToLeft ? !this.options.showSelectionBarEnd : this.options.showSelectionBarEnd, 1190 positionForRange = this.options.rightToLeft ? this.maxH.rzsp + this.handleHalfDim : this.minH.rzsp + this.handleHalfDim; 1191 1192 if (this.range) { 1193 dimension = Math.abs(this.maxH.rzsp - this.minH.rzsp); 1194 position = positionForRange; 1195 } 1196 else { 1197 if (this.options.showSelectionBarFromValue !== null) { 1198 var center = this.options.showSelectionBarFromValue, 1199 centerPosition = this.valueToOffset(center), 1200 isModelGreaterThanCenter = this.options.rightToLeft ? this.lowValue <= center : this.lowValue > center; 1201 if (isModelGreaterThanCenter) { 1202 dimension = this.minH.rzsp - centerPosition; 1203 position = centerPosition + this.handleHalfDim; 1204 } 1205 else { 1206 dimension = centerPosition - this.minH.rzsp; 1207 position = this.minH.rzsp + this.handleHalfDim; 1208 } 1209 } 1210 else if (isSelectionBarFromRight) { 1211 dimension = Math.abs(this.maxPos - this.minH.rzsp) + this.handleHalfDim; 1212 position = this.minH.rzsp + this.handleHalfDim; 1213 } else { 1214 dimension = Math.abs(this.maxH.rzsp - this.minH.rzsp) + this.handleHalfDim; 1215 position = 0; 1216 } 1217 } 1218 this.setDimension(this.selBar, dimension); 1219 this.setPosition(this.selBar, position); 1220 if (this.options.getSelectionBarColor) { 1221 var color = this.getSelectionBarColor(); 1222 this.scope.barStyle = { 1223 backgroundColor: color 1224 }; 1225 } 1226 }, 1227 1228 /** 1229 * Wrapper around the getSelectionBarColor of the user to pass to 1230 * correct parameters 1231 */ 1232 getSelectionBarColor: function() { 1233 if (this.range) 1234 return this.options.getSelectionBarColor(this.scope.rzSliderModel, this.scope.rzSliderHigh); 1235 return this.options.getSelectionBarColor(this.scope.rzSliderModel); 1236 }, 1237 1238 /** 1239 * Wrapper around the getPointerColor of the user to pass to 1240 * correct parameters 1241 */ 1242 getPointerColor: function(pointerType) { 1243 if (pointerType === 'max') { 1244 return this.options.getPointerColor(this.scope.rzSliderHigh, pointerType); 1245 } 1246 return this.options.getPointerColor(this.scope.rzSliderModel, pointerType); 1247 }, 1248 1249 /** 1250 * Wrapper around the getTickColor of the user to pass to 1251 * correct parameters 1252 */ 1253 getTickColor: function(value) { 1254 return this.options.getTickColor(value); 1255 }, 1256 1257 /** 1258 * Update combined label position and value 1259 * 1260 * @returns {undefined} 1261 */ 1262 updateCmbLabel: function() { 1263 var isLabelOverlap = null; 1264 if (this.options.rightToLeft) { 1265 isLabelOverlap = this.minLab.rzsp - this.minLab.rzsd - 10 <= this.maxLab.rzsp; 1266 } else { 1267 isLabelOverlap = this.minLab.rzsp + this.minLab.rzsd + 10 >= this.maxLab.rzsp; 1268 } 1269 1270 if (isLabelOverlap) { 1271 var lowTr = this.getDisplayValue(this.lowValue, 'model'), 1272 highTr = this.getDisplayValue(this.highValue, 'high'), 1273 labelVal = ''; 1274 if (this.options.mergeRangeLabelsIfSame && lowTr === highTr) { 1275 labelVal = lowTr; 1276 } else { 1277 labelVal = this.options.rightToLeft ? highTr + ' - ' + lowTr : lowTr + ' - ' + highTr;
1278 } 1279 1280 this.translateFn(labelVal, this.cmbLab, 'cmb', false); 1281 var pos = this.options.boundPointerLabels ? Math.min( 1282 Math.max( 1283 this.selBar.rzsp + this.selBar.rzsd / 2 - this.cmbLab.rzsd / 2, 1284 0 1285 ), 1286 this.barDimension - this.cmbLab.rzsd 1287 ) : this.selBar.rzsp + this.selBar.rzsd / 2 - this.cmbLab.rzsd / 2; 1288 1289 this.setPosition(this.cmbLab, pos); 1290 this.cmbLabelShown = true; 1291 this.hideEl(this.minLab); 1292 this.hideEl(this.maxLab); 1293 this.showEl(this.cmbLab); 1294 } else { 1295 this.cmbLabelShown = false; 1296 this.showEl(this.maxLab); 1297 this.showEl(this.minLab); 1298 this.hideEl(this.cmbLab); 1299 } 1300 }, 1301 1302 /** 1303 * Return the translated value if a translate function is provided else the original value 1304 * @param value 1305 * @param which if it's min or max handle 1306 * @returns {*} 1307 */ 1308 getDisplayValue: function(value, which) { 1309 if (this.options.stepsArray && !this.options.bindIndexForStepsArray) { 1310 value = this.getStepValue(value); 1311 } 1312 return this.customTrFn(value, this.options.id, which); 1313 }, 1314 1315 /** 1316 * Round value to step and precision based on minValue 1317 * 1318 * @param {number} value 1319 * @param {number} customStep a custom step to override the defined step 1320 * @returns {number} 1321 */ 1322 roundStep: function(value, customStep) { 1323 var step = customStep ? customStep : this.step, 1324 steppedDifference = parseFloat((value - this.minValue) / step).toPrecision(12); 1325 steppedDifference = Math.round(+steppedDifference) * step; 1326 var newValue = (this.minValue + steppedDifference).toFixed(this.precision); 1327 return +newValue; 1328 }, 1329 1330 /** 1331 * Hide element 1332 * 1333 * @param element 1334 * @returns {jqLite} The jqLite wrapped DOM element 1335 */ 1336 hideEl: function(element) { 1337 return element.css({ 1338 visibility: 'hidden' 1339 }); 1340 }, 1341 1342 /** 1343 * Show element 1344 * 1345 * @param element The jqLite wrapped DOM element 1346 * @returns {jqLite} The jqLite 1347 */ 1348 showEl: function(element) { 1349 if (!!element.rzAlwaysHide) { 1350 return element; 1351 } 1352 1353 return element.css({ 1354 visibility: 'visible' 1355 }); 1356 }, 1357 1358 /** 1359 * Set element left/top offset depending on whether slider is horizontal or vertical 1360 * 1361 * @param {jqLite} elem The jqLite wrapped DOM element 1362 * @param {number} pos 1363 * @returns {number} 1364 */ 1365 setPosition: function(elem, pos) { 1366 elem.rzsp = pos; 1367 var css = {}; 1368 css[this.positionProperty] = pos + 'px'; 1369 elem.css(css); 1370 return pos; 1371 }, 1372 1373 /** 1374 * Get element width/height depending on whether slider is horizontal or vertical 1375 * 1376 * @param {jqLite} elem The jqLite wrapped DOM element 1377 * @returns {number} 1378 */ 1379 getDimension: function(elem) { 1380 var val = elem[0].getBoundingClientRect(); 1381 if (this.options.vertical) 1382 elem.rzsd = (val.bottom - val.top) * this.options.scale; 1383 else 1384 elem.rzsd = (val.right - val.left) * this.options.scale; 1385 return elem.rzsd; 1386 }, 1387 1388 /** 1389 * Set element width/height depending on whether slider is horizontal or vertical 1390 * 1391 * @param {jqLite} elem The jqLite wrapped DOM element 1392 * @param {number} dim 1393 * @returns {number} 1394 */ 1395 setDimension: function(elem, dim) {
1396 elem.rzsd = dim; 1397 var css = {}; 1398 css[this.dimensionProperty] = dim + 'px'; 1399 elem.css(css); 1400 return dim; 1401 }, 1402 1403 /** 1404 * Translate value to pixel offset 1405 * 1406 * @param {number} val 1407 * @returns {number} 1408 */ 1409 valueToOffset: function(val) { 1410 if (this.options.rightToLeft) { 1411 return (this.maxValue - this.sanitizeValue(val)) * this.maxPos / this.valueRange || 0; 1412 } 1413 return (this.sanitizeValue(val) - this.minValue) * this.maxPos / this.valueRange || 0; 1414 }, 1415 1416 /** 1417 * Returns a value that is within slider range 1418 * 1419 * @param {number} val 1420 * @returns {number} 1421 */ 1422 sanitizeValue: function(val) { 1423 return Math.min(Math.max(val, this.minValue), this.maxValue); 1424 }, 1425 1426 /** 1427 * Translate offset to model value 1428 * 1429 * @param {number} offset 1430 * @returns {number} 1431 */ 1432 offsetToValue: function(offset) { 1433 if (this.options.rightToLeft) { 1434 return (1 - (offset / this.maxPos)) * this.valueRange + this.minValue; 1435 } 1436 return (offset / this.maxPos) * this.valueRange + this.minValue; 1437 }, 1438 1439 // Events 1440 1441 /** 1442 * Get the X-coordinate or Y-coordinate of an event 1443 * 1444 * @param {Object} event The event 1445 * @returns {number} 1446 */ 1447 getEventXY: function(event) { 1448 /* http://stackoverflow.com/a/12336075/282882 */ 1449 //noinspection JSLint 1450 var clientXY = this.options.vertical ? 'clientY' : 'clientX'; 1451 if (event[clientXY] !== undefined) { 1452 return event[clientXY]; 1453 } 1454 1455 return event.originalEvent === undefined ? 1456 event.touches[0][clientXY] : event.originalEvent.touches[0][clientXY]; 1457 }, 1458 1459 /** 1460 * Compute the event position depending on whether the slider is horizontal or vertical 1461 * @param event 1462 * @returns {number} 1463 */ 1464 getEventPosition: function(event) { 1465 var sliderPos = this.sliderElem.rzsp, 1466 eventPos = 0; 1467 if (this.options.vertical) 1468 eventPos = -this.getEventXY(event) + sliderPos; 1469 else 1470 eventPos = this.getEventXY(event) - sliderPos; 1471 return (eventPos - this.handleHalfDim) * this.options.scale; 1472 }, 1473 1474 /** 1475 * Get event names for move and event end 1476 * 1477 * @param {Event} event The event 1478 * 1479 * @return {{moveEvent: string, endEvent: string}} 1480 */ 1481 getEventNames: function(event) { 1482 var eventNames = { 1483 moveEvent: '', 1484 endEvent: '' 1485 }; 1486 1487 if (event.touches || (event.originalEvent !== undefined && event.originalEvent.touches)) { 1488 eventNames.moveEvent = 'touchmove'; 1489 eventNames.endEvent = 'touchend'; 1490 } else { 1491 eventNames.moveEvent = 'mousemove'; 1492 eventNames.endEvent = 'mouseup'; 1493 } 1494 1495 return eventNames; 1496 }, 1497 1498 /** 1499 * Get the handle closest to an event. 1500 * 1501 * @param event {Event} The event 1502 * @returns {jqLite} The handle closest to the event. 1503 */ 1504 getNearestHandle: function(event) { 1505 if (!this.range) { 1506 return this.minH; 1507 } 1508 var offset = this.getEventPosition(event), 1509 distanceMin = Math.abs(offset - this.minH.rzsp), 1510 distanceMax = Math.abs(offset - this.maxH.rzsp); 1511 if (distanceMin < distanceMax) 1512 return this.minH; 1513 else if (distanceMin > distanceMax) 1514 return this.maxH; 1515 else if (!this.options.rightToLeft) 1516 //if event is at the same distance from min/max then if it's at left of minH, we return minH else maxH 1517 return offset < this.minH.rzsp ? this.minH : this.maxH; 1518 else 1519 //reverse in rtl 1520 return offset > this.minH.rzsp ? this.minH : this.maxH; 1521 }, 1522 1523 /** 1524 * Wrapper function to focus an angular element 1525 * 1526 * @param el {AngularElement} the element to focus 1527 */ 1528 focusElement: function(el) { 1529 var DOM_ELEMENT = 0; 1530 el[DOM_ELEMENT].focus(); 1531 }, 1532 1533 /** 1534 * Bind mouse and touch events to slider handles 1535 * 1536 * @returns {undefined} 1537 */ 1538 bindEvents: function() { 1539 var barTracking, barStart, barMove; 1540 1541 if (this.options.draggableRange) { 1542 barTracking = 'rzSliderDrag'; 1543 barStart = this.onDragStart; 1544 barMove = this.onDragMove; 1545 } else { 1546 barTracking = 'lowValue'; 1547 barStart = this.onStart; 1548 barMove = this.onMove; 1549 } 1550 1551 if (!this.options.onlyBindHandles) { 1552 this.selBar.on('mousedown', angular.bind(this, barStart, null, barTracking)); 1553 this.selBar.on('mousedown', angular.bind(this, barMove, this.selBar)); 1554 } 1555 1556 if (this.options.draggableRangeOnly) { 1557 this.minH.on('mousedown', angular.bind(this, barStart, null, barTracking)); 1558 this.maxH.on('mousedown', angular.bind(this, barStart, null, barTracking)); 1559 } else { 1560 this.minH.on('mousedown', angular.bind(this, this.onStart, this.minH, 'lowValue')); 1561 if (this.range) { 1562 this.maxH.on('mousedown', angular.bind(this, this.onStart, this.maxH, 'highValue')); 1563 } 1564 if (!this.options.onlyBindHandles) { 1565 this.fullBar.on('mousedown', angular.bind(this, this.onStart, null, null)); 1566 this.fullBar.on('mousedown', angular.bind(this, this.onMove, this.fullBar)); 1567 this.ticks.on('mousedown', angular.bind(this, this.onStart, null, null)); 1568 this.ticks.on('mousedown', angular.bind(this, this.onTickClick, this.ticks)); 1569 } 1570 } 1571 1572 if (!this.options.onlyBindHandles) { 1573 this.selBar.on('touchstart', angular.bind(this, barStart, null, barTracking)); 1574 this.selBar.on('touchstart', angular.bind(this, barMove, this.selBar)); 1575 } 1576 if (this.options.draggableRangeOnly) { 1577 this.minH.on('touchstart', angular.bind(this, barStart, null, barTracking)); 1578 this.maxH.on('touchstart', angular.bind(this, barStart, null, barTracking)); 1579 } else { 1580 this.minH.on('touchstart', angular.bind(this, this.onStart, this.minH, 'lowValue')); 1581 if (this.range) { 1582 this.maxH.on('touchstart', angular.bind(this, this.onStart, this.maxH, 'highValue')); 1583 } 1584 if (!this.options.onlyBindHandles) { 1585 this.fullBar.on('touchstart', angular.bind(this, this.onStart, null, null)); 1586 this.fullBar.on('touchstart', angular.bind(this, this.onMove, this.fullBar)); 1587 this.ticks.on('touchstart', angular.bind(this, this.onStart, null, null)); 1588 this.ticks.on('touchstart', angular.bind(this, this.onTickClick, this.ticks)); 1589 } 1590 } 1591 1592 if (this.options.keyboardSupport) { 1593 this.minH.on('focus', angular.bind(this, this.onPointerFocus, this.minH, 'lowValue')); 1594 if (this.range) { 1595 this.maxH.on('focus', angular.bind(this, this.onPointerFocus, this.maxH, 'highValue')); 1596 } 1597 } 1598 }, 1599 1600 /** 1601 * Unbind mouse and touch events to slider handles 1602 * 1603 * @returns {undefined} 1604 */ 1605 unbindEvents: function() { 1606 this.minH.off(); 1607 this.maxH.off(); 1608 this.fullBar.off(); 1609 this.selBar.off(); 1610 this.ticks.off(); 1611 }, 1612 1613 /** 1614 * onStart event handler 1615 * 1616 * @param {?Object} pointer The jqLite wrapped DOM element; if null, the closest handle is used 1617 * @param {?string} ref The name of the handle being changed; if null, the closest handle's value is modified 1618 * @param {Event} event The event 1619 * @returns {undefined} 1620 */ 1621 onStart: function(pointer, ref, event) { 1622 var ehMove, ehEnd, 1623 eventNames = this.getEventNames(event); 1624 1625 event.stopPropagation(); 1626 event.preventDefault(); 1627 1628 // We have to do this in case the HTML where the sliders are on 1629 // have been animated into view. 1630 this.calcViewDimensions(); 1631 1632 if (pointer) { 1633 this.tracking = ref; 1634 } else { 1635 pointer = this.getNearestHandle(event); 1636 this.tracking = pointer === this.minH ? 'lowValue' : 'highValue'; 1637 } 1638 1639 pointer.addClass('rz-active'); 1640 1641 if (this.options.keyboardSupport) 1642 this.focusElement(pointer); 1643 1644 ehMove = angular.bind(this, this.dragging.active ? this.onDragMove : this.onMove, pointer); 1645 ehEnd = angular.bind(this, this.onEnd, ehMove); 1646 1647 $document.on(eventNames.moveEvent, ehMove); 1648 $document.one(eventNames.endEvent, ehEnd); 1649 this.callOnStart(); 1650 }, 1651 1652 /** 1653 * onMove event handler 1654 * 1655 * @param {jqLite} pointer 1656 * @param {Event} event The event 1657 * @param {boolean} fromTick if the event occured on a tick or not 1658 * @returns {undefined} 1659 */ 1660 onMove: function(pointer, event, fromTick) { 1661 var newOffset = this.getEventPosition(event), 1662 newValue, 1663 ceilValue = this.options.rightToLeft ? this.minValue : this.maxValue, 1664 flrValue = this.options.rightToLeft ? this.maxValue : this.minValue; 1665 1666 if (newOffset <= 0) { 1667 newValue = flrValue; 1668 } else if (newOffset >= this.maxPos) { 1669 newValue = ceilValue; 1670 } else { 1671 newValue = this.offsetToValue(newOffset); 1672 if (fromTick && angular.isNumber(this.options.showTicks)) 1673 newValue = this.roundStep(newValue, this.options.showTicks); 1674 else 1675 newValue = this.roundStep(newValue); 1676 } 1677 this.positionTrackingHandle(newValue); 1678 }, 1679 1680 /** 1681 * onEnd event handler 1682 * 1683 * @param {Event} event The event 1684 * @param {Function} ehMove The the bound move event handler 1685 * @returns {undefined} 1686 */ 1687 onEnd: function(ehMove, event) { 1688 var moveEventName = this.getEventNames(event).moveEvent; 1689 1690 if (!this.options.keyboardSupport) { 1691 this.minH.removeClass('rz-active'); 1692 this.maxH.removeClass('rz-active'); 1693 this.tracking = ''; 1694 } 1695 this.dragging.active = false;
1696 1697 $document.off(moveEventName, ehMove); 1698 this.callOnEnd(); 1699 }, 1700 1701 onTickClick: function(pointer, event) { 1702 this.onMove(pointer, event, true); 1703 }, 1704 1705 onPointerFocus: function(pointer, ref) { 1706 this.tracking = ref; 1707 pointer.one('blur', angular.bind(this, this.onPointerBlur, pointer)); 1708 pointer.on('keydown', angular.bind(this, this.onKeyboardEvent)); 1709 pointer.on('keyup', angular.bind(this, this.onKeyUp)); 1710 this.firstKeyDown = true; 1711 pointer.addClass('rz-active'); 1712 1713 this.currentFocusElement = { 1714 pointer: pointer, 1715 ref: ref 1716 }; 1717 }, 1718 1719 onKeyUp: function() { 1720 this.firstKeyDown = true; 1721 this.callOnEnd(); 1722 }, 1723 1724 onPointerBlur: function(pointer) { 1725 pointer.off('keydown'); 1726 pointer.off('keyup'); 1727 this.tracking = ''; 1728 pointer.removeClass('rz-active'); 1729 this.currentFocusElement = null 1730 }, 1731 1732 /** 1733 * Key actions helper function 1734 * 1735 * @param {number} currentValue value of the slider 1736 * 1737 * @returns {?Object} action value mappings 1738 */ 1739 getKeyActions: function(currentValue) { 1740 var increaseStep = currentValue + this.step, 1741 decreaseStep = currentValue - this.step, 1742 increasePage = currentValue + this.valueRange / 10, 1743 decreasePage = currentValue - this.valueRange / 10; 1744 1745 //Left to right default actions 1746 var actions = { 1747 'UP': increaseStep, 1748 'DOWN': decreaseStep, 1749 'LEFT': decreaseStep, 1750 'RIGHT': increaseStep, 1751 'PAGEUP': increasePage, 1752 'PAGEDOWN': decreasePage, 1753 'HOME': this.minValue, 1754 'END': this.maxValue 1755 }; 1756 //right to left means swapping right and left arrows 1757 if (this.options.rightToLeft) { 1758 actions.LEFT = increaseStep; 1759 actions.RIGHT = decreaseStep; 1760 // right to left and vertical means we also swap up and down 1761 if (this.options.vertical) { 1762 actions.UP = decreaseStep; 1763 actions.DOWN = increaseStep; 1764 } 1765 } 1766 return actions; 1767 }, 1768 1769 onKeyboardEvent: function(event) { 1770 var currentValue = this[this.tracking], 1771 keyCode = event.keyCode || event.which, 1772 keys = { 1773 38: 'UP', 1774 40: 'DOWN', 1775 37: 'LEFT', 1776 39: 'RIGHT', 1777 33: 'PAGEUP', 1778 34: 'PAGEDOWN', 1779 36: 'HOME', 1780 35: 'END' 1781 }, 1782 actions = this.getKeyActions(currentValue), 1783 key = keys[keyCode], 1784 action = actions[key]; 1785 if (action == null || this.tracking === '') return; 1786 event.preventDefault(); 1787 1788 if (this.firstKeyDown) { 1789 this.firstKeyDown = false; 1790 this.callOnStart(); 1791 } 1792 1793 var self = this; 1794 $timeout(function() { 1795 var newValue = self.roundStep(self.sanitizeValue(action)); 1796 if (!self.options.draggableRangeOnly) { 1797 self.positionTrackingHandle(newValue); 1798 } 1799 else { 1800 var difference = self.highValue - self.lowValue, 1801 newMinValue, newMaxValue; 1802 if (self.tracking === 'lowValue') { 1803 newMinValue = newValue; 1804 newMaxValue = newValue + difference; 1805 if (newMaxValue > self.maxValue) { 1806 newMaxValue = self.maxValue; 1807 newMinValue = newMaxValue - difference; 1808 } 1809 } else { 1810 newMaxValue = newValue; 1811 newMinValue = newValue - difference; 1812 if (newMinValue < self.minValue) { 1813 newMinValue = self.minValue; 1814 newMaxValue = newMinValue + difference; 1815 } 1816 } 1817 self.positionTrackingBar(newMinValue, newMaxValue); 1818 } 1819 }); 1820 }, 1821 1822 /** 1823 * onDragStart event handler 1824 * 1825 * Handles dragging of the middle bar. 1826 * 1827 * @param {Object} pointer The jqLite wrapped DOM element 1828 * @param {string} ref One of the refLow, refHigh values 1829 * @param {Event} event The event 1830 * @returns {undefined} 1831 */ 1832 onDragStart: function(pointer, ref, event) { 1833 var offset = this.getEventPosition(event); 1834 this.dragging = { 1835 active: true, 1836 value: this.offsetToValue(offset), 1837 difference: this.highValue - this.lowValue, 1838 lowLimit: this.options.rightToLeft ? this.minH.rzsp - offset : offset - this.minH.rzsp,
1839 highLimit: this.options.rightToLeft ? offset - this.maxH.rzsp : this.maxH.rzsp - offset 1840 }; 1841 1842 this.onStart(pointer, ref, event); 1843 }, 1844 1845 /** 1846 * getValue helper function 1847 * 1848 * gets max or min value depending on whether the newOffset is outOfBounds above or below the bar and rightToLeft 1849 * 1850 * @param {string} type 'max' || 'min' The value we are calculating 1851 * @param {number} newOffset The new offset 1852 * @param {boolean} outOfBounds Is the new offset above or below the max/min? 1853 * @param {boolean} isAbove Is the new offset above the bar if out of bounds? 1854 * 1855 * @returns {number} 1856 */ 1857 getValue: function(type, newOffset, outOfBounds, isAbove) { 1858 var isRTL = this.options.rightToLeft, 1859 value = null; 1860 1861 if (type === 'min') { 1862 if (outOfBounds) { 1863 if (isAbove) { 1864 value = isRTL ? this.minValue : this.maxValue - this.dragging.difference; 1865 } else { 1866 value = isRTL ? this.maxValue - this.dragging.difference : this.minValue; 1867 } 1868 } else { 1869 value = isRTL ? this.offsetToValue(newOffset + this.dragging.lowLimit) : this.offsetToValue(newOffset - this.dragging.lowLimit) 1870 } 1871 } else { 1872 if (outOfBounds) { 1873 if (isAbove) { 1874 value = isRTL ? this.minValue + this.dragging.difference : this.maxValue; 1875 } else { 1876 value = isRTL ? this.maxValue : this.minValue + this.dragging.difference; 1877 } 1878 } else { 1879 if (isRTL) { 1880 value = this.offsetToValue(newOffset + this.dragging.lowLimit) + this.dragging.difference 1881 } else { 1882 value = this.offsetToValue(newOffset - this.dragging.lowLimit) + this.dragging.difference; 1883 } 1884 } 1885 } 1886 return this.roundStep(value); 1887 }, 1888 1889 /** 1890 * onDragMove event handler 1891 * 1892 * Handles dragging of the middle bar. 1893 * 1894 * @param {jqLite} pointer 1895 * @param {Event} event The event 1896 * @returns {undefined} 1897 */ 1898 onDragMove: function(pointer, event) { 1899 var newOffset = this.getEventPosition(event), 1900 newMinValue, newMaxValue, 1901 ceilLimit, flrLimit, 1902 isUnderFlrLimit, isOverCeilLimit, 1903 flrH, ceilH; 1904 1905 if (this.options.rightToLeft) { 1906 ceilLimit = this.dragging.lowLimit; 1907 flrLimit = this.dragging.highLimit; 1908 flrH = this.maxH; 1909 ceilH = this.minH; 1910 } else { 1911 ceilLimit = this.dragging.highLimit; 1912 flrLimit = this.dragging.lowLimit; 1913 flrH = this.minH; 1914 ceilH = this.maxH; 1915 } 1916 isUnderFlrLimit = newOffset <= flrLimit; 1917 isOverCeilLimit = newOffset >= this.maxPos - ceilLimit; 1918 1919 if (isUnderFlrLimit) { 1920 if (flrH.rzsp === 0) 1921 return; 1922 newMinValue = this.getValue('min', newOffset, true, false); 1923 newMaxValue = this.getValue('max', newOffset, true, false); 1924 } else if (isOverCeilLimit) { 1925 if (ceilH.rzsp === this.maxPos) 1926 return; 1927 newMaxValue = this.getValue('max', newOffset, true, true); 1928 newMinValue = this.getValue('min', newOffset, true, true); 1929 } else { 1930 newMinValue = this.getValue('min', newOffset, false); 1931 newMaxValue = this.getValue('max', newOffset, false); 1932 } 1933 this.positionTrackingBar(newMinValue, newMaxValue); 1934 }, 1935 1936 /** 1937 * Set the new value and offset for the entire bar 1938 * 1939 * @param {number} newMinValue the new minimum value 1940 * @param {number} newMaxValue the new maximum value 1941 */ 1942 positionTrackingBar: function(newMinValue, newMaxValue) { 1943 1944 if (this.options.minLimit != null && newMinValue < this.options.minLimit) { 1945 newMinValue = this.options.minLimit; 1946 newMaxValue = newMinValue + this.dragging.difference; 1947 } 1948 if (this.options.maxLimit != null && newMaxValue > this.options.maxLimit) { 1949 newMaxValue = this.options.maxLimit; 1950 newMinValue = newMaxValue - this.dragging.difference; 1951 } 1952 1953 this.lowValue = newMinValue; 1954 this.highValue = newMaxValue; 1955 this.applyLowValue(); 1956 if (this.range) 1957 this.applyHighValue(); 1958 this.applyModel(); 1959 this.updateHandles('lowValue', this.valueToOffset(newMinValue)); 1960 this.updateHandles('highValue', this.valueToOffset(newMaxValue)); 1961 }, 1962 1963 /** 1964 * Set the new value and offset to the current tracking handle 1965 * 1966 * @param {number} newValue new model value 1967 */ 1968 positionTrackingHandle: function(newValue) { 1969 var valueChanged = false;
1970 1971 newValue = this.applyMinMaxLimit(newValue); 1972 if (this.range) { 1973 if (this.options.pushRange) { 1974 newValue = this.applyPushRange(newValue); 1975 valueChanged = true; 1976 } 1977 else { 1978 if (this.options.noSwitching) { 1979 if (this.tracking === 'lowValue' && newValue > this.highValue) 1980 newValue = this.applyMinMaxRange(this.highValue); 1981 else if (this.tracking === 'highValue' && newValue < this.lowValue) 1982 newValue = this.applyMinMaxRange(this.lowValue); 1983 } 1984 newValue = this.applyMinMaxRange(newValue); 1985 /* This is to check if we need to switch the min and max handles */ 1986 if (this.tracking === 'lowValue' && newValue > this.highValue) { 1987 this.lowValue = this.highValue; 1988 this.applyLowValue(); 1989 this.updateHandles(this.tracking, this.maxH.rzsp); 1990 this.updateAriaAttributes(); 1991 this.tracking = 'highValue'; 1992 this.minH.removeClass('rz-active'); 1993 this.maxH.addClass('rz-active'); 1994 if (this.options.keyboardSupport) 1995 this.focusElement(this.maxH); 1996 valueChanged = true; 1997 } 1998 else if (this.tracking === 'highValue' && newValue < this.lowValue) { 1999 this.highValue = this.lowValue; 2000 this.applyHighValue(); 2001 this.updateHandles(this.tracking, this.minH.rzsp); 2002 this.updateAriaAttributes(); 2003 this.tracking = 'lowValue'; 2004 this.maxH.removeClass('rz-active'); 2005 this.minH.addClass('rz-active'); 2006 if (this.options.keyboardSupport) 2007 this.focusElement(this.minH); 2008 valueChanged = true; 2009 } 2010 } 2011 } 2012 2013 if (this[this.tracking] !== newValue) { 2014 this[this.tracking] = newValue; 2015 if (this.tracking === 'lowValue') 2016 this.applyLowValue(); 2017 else 2018 this.applyHighValue(); 2019 this.updateHandles(this.tracking, this.valueToOffset(newValue)); 2020 this.updateAriaAttributes(); 2021 valueChanged = true; 2022 } 2023 2024 if (valueChanged) 2025 this.applyModel(); 2026 }, 2027 2028 applyMinMaxLimit: function(newValue) { 2029 if (this.options.minLimit != null && newValue < this.options.minLimit) 2030 return this.options.minLimit; 2031 if (this.options.maxLimit != null && newValue > this.options.maxLimit) 2032 return this.options.maxLimit; 2033 return newValue; 2034 }, 2035 2036 applyMinMaxRange: function(newValue) { 2037 var oppositeValue = this.tracking === 'lowValue' ? this.highValue : this.lowValue, 2038 difference = Math.abs(newValue - oppositeValue); 2039 if (this.options.minRange != null) { 2040 if (difference < this.options.minRange) { 2041 if (this.tracking === 'lowValue') 2042 return this.highValue - this.options.minRange; 2043 else 2044 return this.lowValue + this.options.minRange; 2045 } 2046 } 2047 if (this.options.maxRange != null) { 2048 if (difference > this.options.maxRange) { 2049 if (this.tracking === 'lowValue') 2050 return this.highValue - this.options.maxRange; 2051 else 2052 return this.lowValue + this.options.maxRange; 2053 } 2054 } 2055 return newValue; 2056 }, 2057 2058 applyPushRange: function(newValue) { 2059 var difference = this.tracking === 'lowValue' ? this.highValue - newValue : newValue - this.lowValue, 2060 range = this.options.minRange !== null ? this.options.minRange : this.options.step; 2061 if (difference < range) { 2062 if (this.tracking === 'lowValue') { 2063 this.highValue = Math.min(newValue + range, this.maxValue); 2064 newValue = this.highValue - range; 2065 this.applyHighValue(); 2066 this.updateHandles('highValue', this.valueToOffset(this.highValue)); 2067 } 2068 else { 2069 this.lowValue = Math.max(newValue - range, this.minValue); 2070 newValue = this.lowValue + range; 2071 this.applyLowValue(); 2072 this.updateHandles('lowValue', this.valueToOffset(this.lowValue)); 2073 } 2074 this.updateAriaAttributes(); 2075 } 2076 return newValue; 2077 }, 2078 2079 /** 2080 * Apply the model values using scope.$apply. 2081 * We wrap it with the internalChange flag to avoid the watchers to be called 2082 */ 2083 applyModel: function() { 2084 this.internalChange = true; 2085 this.scope.$apply(); 2086 this.callOnChange(); 2087 this.internalChange = false;
2088 }, 2089 2090 /** 2091 * Call the onStart callback if defined 2092 * The callback call is wrapped in a $evalAsync to ensure that its result will be applied to the scope. 2093 * 2094 * @returns {undefined} 2095 */ 2096 callOnStart: function() { 2097 if (this.options.onStart) { 2098 var self = this, 2099 pointerType = this.tracking === 'lowValue' ? 'min' : 'max'; 2100 this.scope.$evalAsync(function() { 2101 self.options.onStart(self.options.id, self.scope.rzSliderModel, self.scope.rzSliderHigh, pointerType); 2102 }); 2103 } 2104 }, 2105 2106 /** 2107 * Call the onChange callback if defined 2108 * The callback call is wrapped in a $evalAsync to ensure that its result will be applied to the scope. 2109 * 2110 * @returns {undefined} 2111 */ 2112 callOnChange: function() { 2113 if (this.options.onChange) { 2114 var self = this, 2115 pointerType = this.tracking === 'lowValue' ? 'min' : 'max'; 2116 this.scope.$evalAsync(function() { 2117 self.options.onChange(self.options.id, self.scope.rzSliderModel, self.scope.rzSliderHigh, pointerType); 2118 }); 2119 } 2120 }, 2121 2122 /** 2123 * Call the onEnd callback if defined 2124 * The callback call is wrapped in a $evalAsync to ensure that its result will be applied to the scope. 2125 * 2126 * @returns {undefined} 2127 */ 2128 callOnEnd: function() { 2129 if (this.options.onEnd) { 2130 var self = this, 2131 pointerType = this.tracking === 'lowValue' ? 'min' : 'max'; 2132 this.scope.$evalAsync(function() { 2133 self.options.onEnd(self.options.id, self.scope.rzSliderModel, self.scope.rzSliderHigh, pointerType); 2134 }); 2135 } 2136 this.scope.$emit('slideEnded'); 2137 } 2138 }; 2139 2140 return Slider; 2141 }]) 2142 2143 .directive('rzslider', ['RzSlider', function(RzSlider) { 2144 'use strict'; 2145 2146 return { 2147 restrict: 'AE', 2148 replace: true, 2149 scope: { 2150 rzSliderModel: '=?', 2151 rzSliderHigh: '=?', 2152 rzSliderOptions: '&?', 2153 rzSliderTplUrl: '@' 2154 }, 2155 2156 /** 2157 * Return template URL 2158 * 2159 * @param {jqLite} elem 2160 * @param {Object} attrs 2161 * @return {string} 2162 */ 2163 templateUrl: function(elem, attrs) { 2164 //noinspection JSUnresolvedVariable 2165 return attrs.rzSliderTplUrl || 'rzSliderTpl.html'; 2166 }, 2167 2168 link: function(scope, elem) { 2169 scope.slider = new RzSlider(scope, elem); //attach on scope so we can test it 2170 } 2171 }; 2172 }]); 2173 2174 // IDE assist 2175 2176 /** 2177 * @name ngScope 2178 * 2179 * @property {number} rzSliderModel 2180 * @property {number} rzSliderHigh 2181 * @property {Object} rzSliderOptions 2182 */ 2183 2184 /** 2185 * @name jqLite 2186 * 2187 * @property {number|undefined} rzsp rzslider label position offset 2188 * @property {number|undefined} rzsd rzslider element dimension 2189 * @property {string|undefined} rzsv rzslider label value/text 2190 * @property {Function} css 2191 * @property {Function} text 2192 */ 2193 2194 /** 2195 * @name Event 2196 * @property {Array} touches 2197 * @property {Event} originalEvent 2198 */ 2199 2200 /** 2201 * @name ThrottleOptions 2202 * 2203 * @property {boolean} leading 2204 * @property {boolean} trailing 2205 */ 2206 2207 module.run(['$templateCache', function($templateCache) { 2208 'use strict'; 2209 2210 $templateCache.put('rzSliderTpl.html', 2211 "<div class=rzslider><span class=rz-bar-wrapper><span class=rz-bar></span></span> <span class=rz-bar-wrapper><span class=\"rz-bar rz-selection\" ng-style=barStyle></span></span> <span class=\"rz-pointer rz-pointer-min\" ng-style=minPointerStyle></span> <span class=\"rz-pointer rz-pointer-max\" ng-style=maxPointerStyle></span> <span class=\"rz-bubble rz-limit rz-floor\"></span> <span class=\"rz-bubble rz-limit rz-ceil\"></span> <span class=rz-bubble></span> <span class=rz-bubble></span> <span class=rz-bubble></span><ul ng-show=showTicks class=rz-ticks><li ng-repeat=\"t in ticks track by $index\" class=rz-tick ng-class=\"{'rz-selected': t.selected}\" ng-style=t.style ng-attr-uib-tooltip=\"{{ t.tooltip }}\" ng-attr-tooltip-placement={{t.tooltipPlacement}} ng-attr-tooltip-append-to-body=\"{{ t.tooltip ? true : undefined}}\"><span ng-if=\"t.value != null\" class=rz-tick-value ng-attr-uib-tooltip=\"{{ t.valueTooltip }}\" ng-attr-tooltip-placement={{t.valueTooltipPlacement}}>{{ t.value }}</span> <span ng-if=\"t.legend != null\" class=rz-tick-legend>{{ t.legend }}</span></li></ul></div>" 2212 ); 2213 2214 }]); 2215 2216 return module.name 2217}));
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.