text: /*! angularjs-slider - v5.4.1 - 
   (c) Rafal Zajac <rzajac@gmail.com>, Valentin Hervieu <valentin@hervieu.me>, Jussi Saarivirta <jusasi@gmail.com>, Angelin Sirbu <angelin.sirbu@gmail.com> - 
   https://github.com/angular-slider/angularjs-slider - 
   2016-08-01 */
  /*jslint unparam: true */
  /*global angular: false, console: false, define, module */
  (function(root, factory) {
    'use strict';
    /* istanbul ignore next */
    if (typeof define === 'function' && define.amd) {
      // AMD. Register as an anonymous module.
      define(['angular'], factory);
    } else if (typeof module === 'object' && module.exports) {
      // Node. Does not work with strict CommonJS, but
      // only CommonJS-like environments that support module.exports,
      // like Node.
      // to support bundler like browserify
      module.exports = factory(require('angular'));
    } else {
      // Browser globals (root is window)
      factory(root.angular);
    }
  
  }(this, function(angular) {
    'use strict';
    var module = angular.module('rzModule', [])
  
    .factory('RzSliderOptions', function() {
      var defaultOptions = {
        floor: 0,
        ceil: null, //defaults to rz-slider-model
        step: 1,
        precision: 0,
        minRange: null,
        maxRange: null,
        pushRange: false,
        minLimit: null,
        maxLimit: null,
        id: null,
        translate: null,
        getLegend: null,
        stepsArray: null,
        bindIndexForStepsArray: false,
        draggableRange: false,
        draggableRangeOnly: false,
        showSelectionBar: false,
        showSelectionBarEnd: false,
        showSelectionBarFromValue: null,
        hidePointerLabels: false,
        hideLimitLabels: false,
        readOnly: false,
        disabled: false,
        interval: 350,
        showTicks: false,
        showTicksValues: false,
        ticksTooltip: null,
        ticksValuesTooltip: null,
        vertical: false,
        getSelectionBarColor: null,
        getTickColor: null,
        getPointerColor: null,
        keyboardSupport: true,
        scale: 1,
        enforceStep: true,
        enforceRange: false,
        noSwitching: false,
        onlyBindHandles: false,
        onStart: null,
        onChange: null,
        onEnd: null,
        rightToLeft: false,
        boundPointerLabels: true,
        mergeRangeLabelsIfSame: false,
        customTemplateScope: null
      };
      var globalOptions = {};
  
      var factory = {};
      /**
       * `options({})` allows global configuration of all sliders in the
       * application.
       *
       *   var app = angular.module( 'App', ['rzModule'], function( RzSliderOptions ) {
       *     // show ticks for all sliders
       *     RzSliderOptions.options( { showTicks: true } );
       *   });
       */
      factory.options = function(value) {
        angular.extend(globalOptions, value);
      };
  
      factory.getOptions = function(options) {
        return angular.extend({}, defaultOptions, globalOptions, options);
      };
  
      return factory;
    })
  
    .factory('rzThrottle', ['$timeout', function($timeout) {
      /**
       * rzThrottle
       *
       * Taken from underscore project
       *
       * @param {Function} func
       * @param {number} wait
       * @param {ThrottleOptions} options
       * @returns {Function}
       */
      return function(func, wait, options) {
        'use strict';
        /* istanbul ignore next */
        var getTime = (Date.now || function() {
          return new Date().getTime();
        });
        var context, args, result;
        var timeout = null;
        var previous = 0;
        options = options || {};
        var later = function() {
          previous = getTime();
          timeout = null;
          result = func.apply(context, args);
          context = args = null;
        };
        return function() {
          var now = getTime();
          var remaining = wait - (now - previous);
          context = this;
          args = arguments;
          if (remaining <= 0) {
            $timeout.cancel(timeout);
            timeout = null;
            previous = now;
            result = func.apply(context, args);
            context = args = null;
          } else if (!timeout && options.trailing !== false) {
            timeout = $timeout(later, remaining);
          }
          return result;
        };
      }
    }])
  
    .factory('RzSlider', ['$timeout', '$document', '$window', '$compile', 'RzSliderOptions', 'rzThrottle', function($timeout, $document, $window, $compile, RzSliderOptions, rzThrottle) {
      'use strict';
  
      /**
       * Slider
       *
       * @param {ngScope} scope            The AngularJS scope
       * @param {Element} sliderElem The slider directive element wrapped in jqLite
       * @constructor
       */
      var Slider = function(scope, sliderElem) {
        /**
         * The slider's scope
         *
         * @type {ngScope}
         */
        this.scope = scope;
  
        /**
         * The slider inner low value (linked to rzSliderModel)
         * @type {number}
         */
        this.lowValue = 0;
  
        /**
         * The slider inner high value (linked to rzSliderHigh)
         * @type {number}
         */
        this.highValue = 0;
  
        /**
         * Slider element wrapped in jqLite
         *
         * @type {jqLite}
         */
        this.sliderElem = sliderElem;
  
        /**
         * Slider type
         *
         * @type {boolean} Set to true for range slider
         */
        this.range = this.scope.rzSliderModel !== undefined && this.scope.rzSliderHigh !== undefined;
  
        /**
         * Values recorded when first dragging the bar
         *
         * @type {Object}
         */
        this.dragging = {
          active: false,
          value: 0,
          difference: 0,
          offset: 0,
          lowLimit: 0,
          highLimit: 0
        };
  
        /**
         * property that handle position (defaults to left for horizontal)
         * @type {string}
         */
        this.positionProperty = 'left';
  
        /**
         * property that handle dimension (defaults to width for horizontal)
         * @type {string}
         */
        this.dimensionProperty = 'width';
  
        /**
         * Half of the width or height of the slider handles
         *
         * @type {number}
         */
        this.handleHalfDim = 0;
  
        /**
         * Maximum position the slider handle can have
         *
         * @type {number}
         */
        this.maxPos = 0;
  
        /**
         * Precision
         *
         * @type {number}
         */
        this.precision = 0;
  
        /**
         * Step
         *
         * @type {number}
         */
        this.step = 1;
  
        /**
         * The name of the handle we are currently tracking
         *
         * @type {string}
         */
        this.tracking = '';
  
        /**
         * Minimum value (floor) of the model
         *
         * @type {number}
         */
        this.minValue = 0;
  
        /**
         * Maximum value (ceiling) of the model
         *
         * @type {number}
         */
        this.maxValue = 0;
  
  
        /**
         * The delta between min and max value
         *
         * @type {number}
         */
        this.valueRange = 0;
  
  
        /**
         * If showTicks/showTicksValues options are number.
         * In this case, ticks values should be displayed below the slider.
         * @type {boolean}
         */
        this.intermediateTicks = false;
  
        /**
         * Set to true if init method already executed
         *
         * @type {boolean}
         */
        this.initHasRun = false;
  
        /**
         * Used to call onStart on the first keydown event
         *
         * @type {boolean}
         */
        this.firstKeyDown = false;
  
        /**
         * Internal flag to prevent watchers to be called when the sliders value are modified internally.
         * @type {boolean}
         */
        this.internalChange = false;
  
        /**
         * Internal flag to keep track of the visibility of combo label
         * @type {boolean}
         */
        this.cmbLabelShown = false;
  
        // Slider DOM elements wrapped in jqLite
        this.fullBar = null; // The whole slider bar
        this.selBar = null; // Highlight between two handles
        this.minH = null; // Left slider handle
        this.maxH = null; // Right slider handle
        this.flrLab = null; // Floor label
        this.ceilLab = null; // Ceiling label
        this.minLab = null; // Label above the low value
        this.maxLab = null; // Label above the high value
        this.cmbLab = null; // Combined label
        this.ticks = null; // The ticks
  
        // Initialize slider
        this.init();
      };
  
      // Add instance methods
      Slider.prototype = {
  
        /**
         * Initialize slider
         *
         * @returns {undefined}
         */
        init: function() {
          var thrLow, thrHigh,
            self = this;
  
          var calcDimFn = function() {
            self.calcViewDimensions();
          };
  
          this.applyOptions();
          this.syncLowValue();
          if (this.range)
            this.syncHighValue();
          this.initElemHandles();
          this.manageElementsStyle();
          this.setDisabledState();
          this.calcViewDimensions();
          this.setMinAndMax();
          this.addAccessibility();
          this.updateCeilLab();
          this.updateFloorLab();
          this.initHandles();
          this.manageEventsBindings();
  
          // Recalculate slider view dimensions
          this.scope.$on('reCalcViewDimensions', calcDimFn);
  
          // Recalculate stuff if view port dimensions have changed
          angular.element($window).on('resize', calcDimFn);
  
          this.initHasRun = true;
  
          // Watch for changes to the model
          thrLow = rzThrottle(function() {
            self.onLowHandleChange();
          }, self.options.interval);
  
          thrHigh = rzThrottle(function() {
            self.onHighHandleChange();
          }, self.options.interval);
  
          this.scope.$on('rzSliderForceRender', function() {
            self.resetLabelsValue();
            thrLow();
            if (self.range) {
              thrHigh();
            }
            self.resetSlider();
          });
  
          // Watchers (order is important because in case of simultaneous change,
          // watchers will be called in the same order)
          this.scope.$watch('rzSliderOptions()', function(newValue, oldValue) {
            if (newValue === oldValue)
              return;
            self.applyOptions(); // need to be called before synchronizing the values
            self.syncLowValue();
            if (self.range)
              self.syncHighValue();
            self.resetSlider();
          }, true);
  
          this.scope.$watch('rzSliderModel', function(newValue, oldValue) {
            if (self.internalChange)
              return;
            if (newValue === oldValue)
              return;
            thrLow();
          });
  
          this.scope.$watch('rzSliderHigh', function(newValue, oldValue) {
            if (self.internalChange)
              return;
            if (newValue === oldValue)
              return;
            if (newValue != null)
              thrHigh();
            if (self.range && newValue == null || !self.range && newValue != null) {
              self.applyOptions();
              self.resetSlider();
            }
          });
  
          this.scope.$on('$destroy', function() {
            self.unbindEvents();
            angular.element($window).off('resize', calcDimFn);
          });
        },
  
        findStepIndex: function(modelValue) {
          var index = 0;
          for (var i = 0; i < this.options.stepsArray.length; i++) {
            var step = this.options.stepsArray[i];
            if (step === modelValue) {
              index = i;
              break;
            }
            else if (angular.isObject(step) && step.value === modelValue) {
              index = i;
              break;
            }
          }
          return index;
        },
  
        syncLowValue: function() {
          if (this.options.stepsArray) {
            if (!this.options.bindIndexForStepsArray)
              this.lowValue = this.findStepIndex(this.scope.rzSliderModel);
            else
              this.lowValue = this.scope.rzSliderModel
          }
          else
            this.lowValue = this.scope.rzSliderModel;
        },
  
        syncHighValue: function() {
          if (this.options.stepsArray) {
            if (!this.options.bindIndexForStepsArray)
              this.highValue = this.findStepIndex(this.scope.rzSliderHigh);
            else
              this.highValue = this.scope.rzSliderHigh
          }
          else
            this.highValue = this.scope.rzSliderHigh;
        },
  
        getStepValue: function(sliderValue) {
          var step = this.options.stepsArray[sliderValue];
          if (angular.isObject(step))
            return step.value;
          return step;
        },
  
        applyLowValue: function() {
          if (this.options.stepsArray) {
            if (!this.options.bindIndexForStepsArray)
              this.scope.rzSliderModel = this.getStepValue(this.lowValue);
            else
              this.scope.rzSliderModel = this.lowValue
          }
          else
            this.scope.rzSliderModel = this.lowValue;
        },
  
        applyHighValue: function() {
          if (this.options.stepsArray) {
            if (!this.options.bindIndexForStepsArray)
              this.scope.rzSliderHigh = this.getStepValue(this.highValue);
            else
              this.scope.rzSliderHigh = this.highValue
          }
          else
            this.scope.rzSliderHigh = this.highValue;
        },
  
        /*
         * Reflow the slider when the low handle changes (called with throttle)
         */
        onLowHandleChange: function() {
          this.syncLowValue();
          if (this.range)
            this.syncHighValue();
          this.setMinAndMax();
          this.updateLowHandle(this.valueToOffset(this.lowValue));
          this.updateSelectionBar();
          this.updateTicksScale();
          this.updateAriaAttributes();
          if (this.range) {
            this.updateCmbLabel();
          }
        },
  
        /*
         * Reflow the slider when the high handle changes (called with throttle)
         */
        onHighHandleChange: function() {
          this.syncLowValue();
          this.syncHighValue();
          this.setMinAndMax();
          this.updateHighHandle(this.valueToOffset(this.highValue));
          this.updateSelectionBar();
          this.updateTicksScale();
          this.updateCmbLabel();
          this.updateAriaAttributes();
        },
  
        /**
         * Read the user options and apply them to the slider model
         */
        applyOptions: function() {
          var sliderOptions;
          if (this.scope.rzSliderOptions)
            sliderOptions = this.scope.rzSliderOptions();
          else
            sliderOptions = {};
  
          this.options = RzSliderOptions.getOptions(sliderOptions);
  
          if (this.options.step <= 0)
            this.options.step = 1;
  
          this.range = this.scope.rzSliderModel !== undefined && this.scope.rzSliderHigh !== undefined;
          this.options.draggableRange = this.range && this.options.draggableRange;
          this.options.draggableRangeOnly = this.range && this.options.draggableRangeOnly;
          if (this.options.draggableRangeOnly) {
            this.options.draggableRange = true;
          }
  
          this.options.showTicks = this.options.showTicks || this.options.showTicksValues;
          this.scope.showTicks = this.options.showTicks; //scope is used in the template
          if (angular.isNumber(this.options.showTicks))
            this.intermediateTicks = true;
  
          this.options.showSelectionBar = this.options.showSelectionBar || this.options.showSelectionBarEnd
            || this.options.showSelectionBarFromValue !== null;
  
          if (this.options.stepsArray) {
            this.parseStepsArray();
          } else {
            if (this.options.translate)
              this.customTrFn = this.options.translate;
            else
              this.customTrFn = function(value) {
                return String(value);
              };
  
            this.getLegend = this.options.getLegend;
          }
  
          if (this.options.vertical) {
            this.positionProperty = 'bottom';
            this.dimensionProperty = 'height';
          }
  
          if (this.options.customTemplateScope)
            this.scope.custom = this.options.customTemplateScope;
        },
  
        parseStepsArray: function() {
          this.options.floor = 0;
          this.options.ceil = this.options.stepsArray.length - 1;
          this.options.step = 1;
  
          if (this.options.translate) {
            this.customTrFn = this.options.translate;
          }
          else {
            this.customTrFn = function(modelValue) {
              if (this.options.bindIndexForStepsArray)
                return this.getStepValue(modelValue)
              return modelValue;
            };
          }
  
          this.getLegend = function(index) {
            var step = this.options.stepsArray[index];
            if (angular.isObject(step))
              return step.legend;
            return null;
          };
        },
  
        /**
         * Resets slider
         *
         * @returns {undefined}
         */
        resetSlider: function() {
          this.manageElementsStyle();
          this.addAccessibility();
          this.setMinAndMax();
          this.updateCeilLab();
          this.updateFloorLab();
          this.unbindEvents();
          this.manageEventsBindings();
          this.setDisabledState();
          this.calcViewDimensions();
        },
  
        /**
         * Set the slider children to variables for easy access
         *
         * Run only once during initialization
         *
         * @returns {undefined}
         */
        initElemHandles: function() {
          // Assign all slider elements to object properties for easy access
          angular.forEach(this.sliderElem.children(), function(elem, index) {
            var jElem = angular.element(elem);
  
            switch (index) {
              case 0:
                this.fullBar = jElem;
                break;
              case 1:
                this.selBar = jElem;
                break;
              case 2:
                this.minH = jElem;
                break;
              case 3:
                this.maxH = jElem;
                break;
              case 4:
                this.flrLab = jElem;
                break;
              case 5:
                this.ceilLab = jElem;
                break;
              case 6:
                this.minLab = jElem;
                break;
              case 7:
                this.maxLab = jElem;
                break;
              case 8:
                this.cmbLab = jElem;
                break;
              case 9:
                this.ticks = jElem;
                break;
            }
  
          }, this);
  
          // Initialize offset cache properties
          this.selBar.rzsp = 0;
          this.minH.rzsp = 0;
          this.maxH.rzsp = 0;
          this.flrLab.rzsp = 0;
          this.ceilLab.rzsp = 0;
          this.minLab.rzsp = 0;
          this.maxLab.rzsp = 0;
          this.cmbLab.rzsp = 0;
        },
  
        /**
         * Update each elements style based on options
         */
        manageElementsStyle: function() {
  
          if (!this.range)
            this.maxH.css('display', 'none');
          else
            this.maxH.css('display', '');
  
  
          this.alwaysHide(this.flrLab, this.options.showTicksValues || this.options.hideLimitLabels);
          this.alwaysHide(this.ceilLab, this.options.showTicksValues || this.options.hideLimitLabels);
  
          var hideLabelsForTicks = this.options.showTicksValues && !this.intermediateTicks;
          this.alwaysHide(this.minLab, hideLabelsForTicks || this.options.hidePointerLabels);
          this.alwaysHide(this.maxLab, hideLabelsForTicks || !this.range || this.options.hidePointerLabels);
          this.alwaysHide(this.cmbLab, hideLabelsForTicks || !this.range || this.options.hidePointerLabels);
          this.alwaysHide(this.selBar, !this.range && !this.options.showSelectionBar);
  
          if (this.options.vertical)
            this.sliderElem.addClass('rz-vertical');
  
          if (this.options.draggableRange)
            this.selBar.addClass('rz-draggable');
          else
            this.selBar.removeClass('rz-draggable');
  
          if (this.intermediateTicks && this.options.showTicksValues)
            this.ticks.addClass('rz-ticks-values-under');
        },
  
        alwaysHide: function(el, hide) {
          el.rzAlwaysHide = hide;
          if (hide)
            this.hideEl(el);
          else
            this.showEl(el)
        },
  
        /**
         * Manage the events bindings based on readOnly and disabled options
         *
         * @returns {undefined}
         */
        manageEventsBindings: function() {
          if (this.options.disabled || this.options.readOnly)
            this.unbindEvents();
          else
            this.bindEvents();
        },
  
        /**
         * Set the disabled state based on rzSliderDisabled
         *
         * @returns {undefined}
         */
        setDisabledState: function() {
          if (this.options.disabled) {
            this.sliderElem.attr('disabled', 'disabled');
          } else {
            this.sliderElem.attr('disabled', null);
          }
        },
  
        /**
         * Reset label values
         *
         * @return {undefined}
         */
        resetLabelsValue: function() {
          this.minLab.rzsv = undefined;
          this.maxLab.rzsv = undefined;
        },
  
        /**
         * Initialize slider handles positions and labels
         *
         * Run only once during initialization and every time view port changes size
         *
         * @returns {undefined}
         */
        initHandles: function() {
          this.updateLowHandle(this.valueToOffset(this.lowValue));
  
          /*
           the order here is important since the selection bar should be
           updated after the high handle but before the combined label
           */
          if (this.range)
            this.updateHighHandle(this.valueToOffset(this.highValue));
          this.updateSelectionBar();
          if (this.range)
            this.updateCmbLabel();
  
          this.updateTicksScale();
        },
  
        /**
         * Translate value to human readable format
         *
         * @param {number|string} value
         * @param {jqLite} label
         * @param {String} which
         * @param {boolean} [useCustomTr]
         * @returns {undefined}
         */
        translateFn: function(value, label, which, useCustomTr) {
          useCustomTr = useCustomTr === undefined ? true : useCustomTr;
  
          var valStr = '',
            getDimension = false,
            noLabelInjection = label.hasClass('no-label-injection');
  
          if (useCustomTr) {
            if (this.options.stepsArray && !this.options.bindIndexForStepsArray)
              value = this.getStepValue(value);
            valStr = String(this.customTrFn(value, this.options.id, which));
          }
          else {
            valStr = String(value)
          }
  
          if (label.rzsv === undefined || label.rzsv.length !== valStr.length || (label.rzsv.length > 0 && label.rzsd === 0)) {
            getDimension = true;
            label.rzsv = valStr;
          }
  
          if (!noLabelInjection) {
            label.html(valStr);
          }
          ;
  
          this.scope[which + 'Label'] = valStr;
  
          // Update width only when length of the label have changed
          if (getDimension) {
            this.getDimension(label);
          }
        },
  
        /**
         * Set maximum and minimum values for the slider and ensure the model and high
         * value match these limits
         * @returns {undefined}
         */
        setMinAndMax: function() {
  
          this.step = +this.options.step;
          this.precision = +this.options.precision;
  
          this.minValue = this.options.floor;
  
          if (this.options.enforceStep) {
            this.lowValue = this.roundStep(this.lowValue);
            if (this.range)
              this.highValue = this.roundStep(this.highValue);
          }
  
          if (this.options.ceil != null)
            this.maxValue = this.options.ceil;
          else
            this.maxValue = this.options.ceil = this.range ? this.highValue : this.lowValue;
  
          if (this.options.enforceRange) {
            this.lowValue = this.sanitizeValue(this.lowValue);
            if (this.range)
              this.highValue = this.sanitizeValue(this.highValue);
          }
  
          this.applyLowValue();
          if (this.range)
            this.applyHighValue();
  
          this.valueRange = this.maxValue - this.minValue;
        },
  
        /**
         * Adds accessibility attributes
         *
         * Run only once during initialization
         *
         * @returns {undefined}
         */
        addAccessibility: function() {
          this.minH.attr('role', 'slider');
          this.updateAriaAttributes();
          if (this.options.keyboardSupport && !(this.options.readOnly || this.options.disabled))
            this.minH.attr('tabindex', '0');
          else
            this.minH.attr('tabindex', '');
          if (this.options.vertical)
            this.minH.attr('aria-orientation', 'vertical');
  
          if (this.range) {
            this.maxH.attr('role', 'slider');
            if (this.options.keyboardSupport && !(this.options.readOnly || this.options.disabled))
              this.maxH.attr('tabindex', '0');
            else
              this.maxH.attr('tabindex', '');
            if (this.options.vertical)
              this.maxH.attr('aria-orientation', 'vertical');
          }
        },
  
        /**
         * Updates aria attributes according to current values
         */
        updateAriaAttributes: function() {
          this.minH.attr({
            'aria-valuenow': this.scope.rzSliderModel,
            'aria-valuetext': this.customTrFn(this.scope.rzSliderModel, this.options.id, 'model'),
            'aria-valuemin': this.minValue,
            'aria-valuemax': this.maxValue
          });
          if (this.range) {
            this.maxH.attr({
              'aria-valuenow': this.scope.rzSliderHigh,
              'aria-valuetext': this.customTrFn(this.scope.rzSliderHigh, this.options.id, 'high'),
              'aria-valuemin': this.minValue,
              'aria-valuemax': this.maxValue
            });
          }
        },
  
        /**
         * Calculate dimensions that are dependent on view port size
         *
         * Run once during initialization and every time view port changes size.
         *
         * @returns {undefined}
         */
        calcViewDimensions: function() {
          var handleWidth = this.getDimension(this.minH);
  
          this.handleHalfDim = handleWidth / 2;
          this.barDimension = this.getDimension(this.fullBar);
  
          this.maxPos = this.barDimension - handleWidth;
  
          this.getDimension(this.sliderElem);
          this.sliderElem.rzsp = this.sliderElem[0].getBoundingClientRect()[this.positionProperty];
  
          if (this.initHasRun) {
            this.updateFloorLab();
            this.updateCeilLab();
            this.initHandles();
          }
        },
  
        /**
         * Update the ticks position
         *
         * @returns {undefined}
         */
        updateTicksScale: function() {
          if (!this.options.showTicks) return;
          var step = this.step;
          if (this.intermediateTicks)
            step = this.options.showTicks;
          var ticksCount = Math.round((this.maxValue - this.minValue) / step) + 1;
          this.scope.ticks = [];
          for (var i = 0; i < ticksCount; i++) {
            var value = this.roundStep(this.minValue + i * step);
            var tick = {
              selected: this.isTickSelected(value)
            };
            if (tick.selected && this.options.getSelectionBarColor) {
              tick.style = {
                'background-color': this.getSelectionBarColor()
              };
            }
            if (!tick.selected && this.options.getTickColor) {
              tick.style = {
                'background-color': this.getTickColor(value)
              }
            }
            if (this.options.ticksTooltip) {
              tick.tooltip = this.options.ticksTooltip(value);
              tick.tooltipPlacement = this.options.vertical ? 'right' : 'top';
            }
            if (this.options.showTicksValues) {
              tick.value = this.getDisplayValue(value, 'tick-value');
              if (this.options.ticksValuesTooltip) {
                tick.valueTooltip = this.options.ticksValuesTooltip(value);
                tick.valueTooltipPlacement = this.options.vertical ? 'right' : 'top';
              }
            }
            if (this.getLegend) {
              var legend = this.getLegend(value, this.options.id);
              if (legend)
                tick.legend = legend;
            }
            if (!this.options.rightToLeft) {
              this.scope.ticks.push(tick);
            } else {
              this.scope.ticks.unshift(tick);
            }
          }
        },
  
        isTickSelected: function(value) {
          if (!this.range) {
            if (this.options.showSelectionBarFromValue !== null) {
              var center = this.options.showSelectionBarFromValue;
              if (this.lowValue > center && value >= center && value <= this.lowValue)
                return true;
              else if (this.lowValue < center && value <= center && value >= this.lowValue)
                return true;
            }
            else if (this.options.showSelectionBarEnd) {
              if (value >= this.lowValue)
                return true;
            }
            else if (this.options.showSelectionBar && value <= this.lowValue)
              return true;
          }
          if (this.range && value >= this.lowValue && value <= this.highValue)
            return true;
          return false;
        },
  
        /**
         * Update position of the floor label
         *
         * @returns {undefined}
         */
        updateFloorLab: function() {
          this.translateFn(this.minValue, this.flrLab, 'floor');
          this.getDimension(this.flrLab);
          var position = this.options.rightToLeft ? this.barDimension - this.flrLab.rzsd : 0;
          this.setPosition(this.flrLab, position);
        },
  
        /**
         * Update position of the ceiling label
         *
         * @returns {undefined}
         */
        updateCeilLab: function() {
          this.translateFn(this.maxValue, this.ceilLab, 'ceil');
          this.getDimension(this.ceilLab);
          var position = this.options.rightToLeft ? 0 : this.barDimension - this.ceilLab.rzsd;
          this.setPosition(this.ceilLab, position);
        },
  
        /**
         * Update slider handles and label positions
         *
         * @param {string} which
         * @param {number} newOffset
         */
        updateHandles: function(which, newOffset) {
          if (which === 'lowValue')
            this.updateLowHandle(newOffset);
          else
            this.updateHighHandle(newOffset);
  
          this.updateSelectionBar();
          this.updateTicksScale();
          if (this.range)
            this.updateCmbLabel();
        },
  
        /**
         * Helper function to work out the position for handle labels depending on RTL or not
         *
         * @param {string} labelName maxLab or minLab
         * @param newOffset
         *
         * @returns {number}
         */
        getHandleLabelPos: function(labelName, newOffset) {
          var labelRzsd = this[labelName].rzsd,
            nearHandlePos = newOffset - labelRzsd / 2 + this.handleHalfDim,
            endOfBarPos = this.barDimension - labelRzsd;
  
          if (!this.options.boundPointerLabels)
            return nearHandlePos;
  
          if (this.options.rightToLeft && labelName === 'minLab' || !this.options.rightToLeft && labelName === 'maxLab') {
            return Math.min(nearHandlePos, endOfBarPos);
          } else {
            return Math.min(Math.max(nearHandlePos, 0), endOfBarPos);
          }
        },
  
        /**
         * Update low slider handle position and label
         *
         * @param {number} newOffset
         * @returns {undefined}
         */
        updateLowHandle: function(newOffset) {
          this.setPosition(this.minH, newOffset);
          this.translateFn(this.lowValue, this.minLab, 'model');
          this.setPosition(this.minLab, this.getHandleLabelPos('minLab', newOffset));
  
          if (this.options.getPointerColor) {
            var pointercolor = this.getPointerColor('min');
            this.scope.minPointerStyle = {
              backgroundColor: pointercolor
            };
          }
  
          this.shFloorCeil();
        },
  
        /**
         * Update high slider handle position and label
         *
         * @param {number} newOffset
         * @returns {undefined}
         */
        updateHighHandle: function(newOffset) {
          this.setPosition(this.maxH, newOffset);
          this.translateFn(this.highValue, this.maxLab, 'high');
          this.setPosition(this.maxLab, this.getHandleLabelPos('maxLab', newOffset));
  
          if (this.options.getPointerColor) {
            var pointercolor = this.getPointerColor('max');
            this.scope.maxPointerStyle = {
              backgroundColor: pointercolor
            };
          }
  
          this.shFloorCeil();
        },
  
        /**
         * Show/hide floor/ceiling label
         *
         * @returns {undefined}
         */
        shFloorCeil: function() {
          // Show based only on hideLimitLabels if pointer labels are hidden
          if (this.options.hidePointerLabels) {
            return;
          }
          var flHidden = false,
            clHidden = false,
            isRTL = this.options.rightToLeft,
            flrLabPos = this.flrLab.rzsp,
            flrLabDim = this.flrLab.rzsd,
            minLabPos = this.minLab.rzsp,
            minLabDim = this.minLab.rzsd,
            maxLabPos = this.maxLab.rzsp,
            maxLabDim = this.maxLab.rzsd,
            cmbLabPos = this.cmbLab.rzsp,
            cmbLabDim = this.cmbLab.rzsd,
            ceilLabPos = this.ceilLab.rzsp,
            halfHandle = this.handleHalfDim,
            isMinLabAtFloor = isRTL ? minLabPos + minLabDim >= flrLabPos - flrLabDim - 5 : minLabPos <= flrLabPos + flrLabDim + 5,
            isMinLabAtCeil = isRTL ? minLabPos - minLabDim <= ceilLabPos + halfHandle + 10 : minLabPos + minLabDim >= ceilLabPos - halfHandle - 10,
            isMaxLabAtFloor = isRTL ? maxLabPos >= flrLabPos - flrLabDim - halfHandle : maxLabPos <= flrLabPos + flrLabDim + halfHandle,
            isMaxLabAtCeil = isRTL ? maxLabPos - maxLabDim <= ceilLabPos + 10 : maxLabPos + maxLabDim >= ceilLabPos - 10,
            isCmbLabAtFloor = isRTL ? cmbLabPos >= flrLabPos - flrLabDim - halfHandle : cmbLabPos <= flrLabPos + flrLabDim + halfHandle,
            isCmbLabAtCeil = isRTL ? cmbLabPos - cmbLabDim <= ceilLabPos + 10 : cmbLabPos + cmbLabDim >= ceilLabPos - 10
  
  
          if (isMinLabAtFloor) {
            flHidden = true;
            this.hideEl(this.flrLab);
          } else {
            flHidden = false;
            this.showEl(this.flrLab);
          }
  
          if (isMinLabAtCeil) {
            clHidden = true;
            this.hideEl(this.ceilLab);
          } else {
            clHidden = false;
            this.showEl(this.ceilLab);
          }
  
          if (this.range) {
            var hideCeil = this.cmbLabelShown ? isCmbLabAtCeil : isMaxLabAtCeil;
            var hideFloor = this.cmbLabelShown ? isCmbLabAtFloor : isMinLabAtFloor;
  
            if (hideCeil) {
              this.hideEl(this.ceilLab);
            } else if (!clHidden) {
              this.showEl(this.ceilLab);
            }
  
            // Hide or show floor label
            if (hideFloor) {
              this.hideEl(this.flrLab);
            } else if (!flHidden) {
              this.showEl(this.flrLab);
            }
          }
        },
  
        /**
         * Update slider selection bar, combined label and range label
         *
         * @returns {undefined}
         */
        updateSelectionBar: function() {
          var position = 0,
            dimension = 0,
            isSelectionBarFromRight = this.options.rightToLeft ? !this.options.showSelectionBarEnd : this.options.showSelectionBarEnd,
            positionForRange = this.options.rightToLeft ? this.maxH.rzsp + this.handleHalfDim : this.minH.rzsp + this.handleHalfDim;
  
          if (this.range) {
            dimension = Math.abs(this.maxH.rzsp - this.minH.rzsp);
            position = positionForRange;
          }
          else {
            if (this.options.showSelectionBarFromValue !== null) {
              var center = this.options.showSelectionBarFromValue,
                centerPosition = this.valueToOffset(center),
                isModelGreaterThanCenter = this.options.rightToLeft ? this.lowValue <= center : this.lowValue > center;
              if (isModelGreaterThanCenter) {
                dimension = this.minH.rzsp - centerPosition;
                position = centerPosition + this.handleHalfDim;
              }
              else {
                dimension = centerPosition - this.minH.rzsp;
                position = this.minH.rzsp + this.handleHalfDim;
              }
            }
            else if (isSelectionBarFromRight) {
              dimension = Math.abs(this.maxPos - this.minH.rzsp) + this.handleHalfDim;
              position = this.minH.rzsp + this.handleHalfDim;
            } else {
              dimension = Math.abs(this.maxH.rzsp - this.minH.rzsp) + this.handleHalfDim;
              position = 0;
            }
          }
          this.setDimension(this.selBar, dimension);
          this.setPosition(this.selBar, position);
          if (this.options.getSelectionBarColor) {
            var color = this.getSelectionBarColor();
            this.scope.barStyle = {
              backgroundColor: color
            };
          }
        },
  
        /**
         * Wrapper around the getSelectionBarColor of the user to pass to
         * correct parameters
         */
        getSelectionBarColor: function() {
          if (this.range)
            return this.options.getSelectionBarColor(this.scope.rzSliderModel, this.scope.rzSliderHigh);
          return this.options.getSelectionBarColor(this.scope.rzSliderModel);
        },
  
        /**
         * Wrapper around the getPointerColor of the user to pass to
         * correct parameters
         */
        getPointerColor: function(pointerType) {
          if (pointerType === 'max') {
            return this.options.getPointerColor(this.scope.rzSliderHigh, pointerType);
          }
          return this.options.getPointerColor(this.scope.rzSliderModel, pointerType);
        },
  
        /**
         * Wrapper around the getTickColor of the user to pass to
         * correct parameters
         */
        getTickColor: function(value) {
          return this.options.getTickColor(value);
        },
  
        /**
         * Update combined label position and value
         *
         * @returns {undefined}
         */
        updateCmbLabel: function() {
          var isLabelOverlap = null;
          if (this.options.rightToLeft) {
            isLabelOverlap = this.minLab.rzsp - this.minLab.rzsd - 10 <= this.maxLab.rzsp;
          } else {
            isLabelOverlap = this.minLab.rzsp + this.minLab.rzsd + 10 >= this.maxLab.rzsp;
          }
  
          if (isLabelOverlap) {
            var lowTr = this.getDisplayValue(this.lowValue, 'model'),
              highTr = this.getDisplayValue(this.highValue, 'high'),
              labelVal = '';
            if (this.options.mergeRangeLabelsIfSame && lowTr === highTr) {
              labelVal = lowTr;
            } else {
              labelVal = this.options.rightToLeft ? highTr + ' - ' + lowTr : lowTr + ' - ' + highTr;
            }
  
            this.translateFn(labelVal, this.cmbLab, 'cmb', false);
            var pos = this.options.boundPointerLabels ? Math.min(
              Math.max(
                this.selBar.rzsp + this.selBar.rzsd / 2 - this.cmbLab.rzsd / 2,
                0
              ),
              this.barDimension - this.cmbLab.rzsd
            ) : this.selBar.rzsp + this.selBar.rzsd / 2 - this.cmbLab.rzsd / 2;
  
            this.setPosition(this.cmbLab, pos);
            this.cmbLabelShown = true;
            this.hideEl(this.minLab);
            this.hideEl(this.maxLab);
            this.showEl(this.cmbLab);
          } else {
            this.cmbLabelShown = false;
            this.showEl(this.maxLab);
            this.showEl(this.minLab);
            this.hideEl(this.cmbLab);
          }
        },
  
        /**
         * Return the translated value if a translate function is provided else the original value
         * @param value
         * @param which if it's min or max handle
         * @returns {*}
         */
        getDisplayValue: function(value, which) {
          if (this.options.stepsArray && !this.options.bindIndexForStepsArray) {
            value = this.getStepValue(value);
          }
          return this.customTrFn(value, this.options.id, which);
        },
  
        /**
         * Round value to step and precision based on minValue
         *
         * @param {number} value
         * @param {number} customStep a custom step to override the defined step
         * @returns {number}
         */
        roundStep: function(value, customStep) {
          var step = customStep ? customStep : this.step,
            steppedDifference = parseFloat((value - this.minValue) / step).toPrecision(12);
          steppedDifference = Math.round(+steppedDifference) * step;
          var newValue = (this.minValue + steppedDifference).toFixed(this.precision);
          return +newValue;
        },
  
        /**
         * Hide element
         *
         * @param element
         * @returns {jqLite} The jqLite wrapped DOM element
         */
        hideEl: function(element) {
          return element.css({
            visibility: 'hidden'
          });
        },
  
        /**
         * Show element
         *
         * @param element The jqLite wrapped DOM element
         * @returns {jqLite} The jqLite
         */
        showEl: function(element) {
          if (!!element.rzAlwaysHide) {
            return element;
          }
  
          return element.css({
            visibility: 'visible'
          });
        },
  
        /**
         * Set element left/top offset depending on whether slider is horizontal or vertical
         *
         * @param {jqLite} elem The jqLite wrapped DOM element
         * @param {number} pos
         * @returns {number}
         */
        setPosition: function(elem, pos) {
          elem.rzsp = pos;
          var css = {};
          css[this.positionProperty] = pos + 'px';
          elem.css(css);
          return pos;
        },
  
        /**
         * Get element width/height depending on whether slider is horizontal or vertical
         *
         * @param {jqLite} elem The jqLite wrapped DOM element
         * @returns {number}
         */
        getDimension: function(elem) {
          var val = elem[0].getBoundingClientRect();
          if (this.options.vertical)
            elem.rzsd = (val.bottom - val.top) * this.options.scale;
          else
            elem.rzsd = (val.right - val.left) * this.options.scale;
          return elem.rzsd;
        },
  
        /**
         * Set element width/height depending on whether slider is horizontal or vertical
         *
         * @param {jqLite} elem  The jqLite wrapped DOM element
         * @param {number} dim
         * @returns {number}
         */
        setDimension: function(elem, dim) {
          elem.rzsd = dim;
          var css = {};
          css[this.dimensionProperty] = dim + 'px';
          elem.css(css);
          return dim;
        },
  
        /**
         * Translate value to pixel offset
         *
         * @param {number} val
         * @returns {number}
         */
        valueToOffset: function(val) {
          if (this.options.rightToLeft) {
            return (this.maxValue - this.sanitizeValue(val)) * this.maxPos / this.valueRange || 0;
          }
          return (this.sanitizeValue(val) - this.minValue) * this.maxPos / this.valueRange || 0;
        },
  
        /**
         * Returns a value that is within slider range
         *
         * @param {number} val
         * @returns {number}
         */
        sanitizeValue: function(val) {
          return Math.min(Math.max(val, this.minValue), this.maxValue);
        },
  
        /**
         * Translate offset to model value
         *
         * @param {number} offset
         * @returns {number}
         */
        offsetToValue: function(offset) {
          if (this.options.rightToLeft) {
            return (1 - (offset / this.maxPos)) * this.valueRange + this.minValue;
          }
          return (offset / this.maxPos) * this.valueRange + this.minValue;
        },
  
        // Events
  
        /**
         * Get the X-coordinate or Y-coordinate of an event
         *
         * @param {Object} event  The event
         * @returns {number}
         */
        getEventXY: function(event) {
          /* http://stackoverflow.com/a/12336075/282882 */
          //noinspection JSLint
          var clientXY = this.options.vertical ? 'clientY' : 'clientX';
          if (event[clientXY] !== undefined) {
            return event[clientXY];
          }
  
          return event.originalEvent === undefined ?
            event.touches[0][clientXY] : event.originalEvent.touches[0][clientXY];
        },
  
        /**
         * Compute the event position depending on whether the slider is horizontal or vertical
         * @param event
         * @returns {number}
         */
        getEventPosition: function(event) {
          var sliderPos = this.sliderElem.rzsp,
            eventPos = 0;
          if (this.options.vertical)
            eventPos = -this.getEventXY(event) + sliderPos;
          else
            eventPos = this.getEventXY(event) - sliderPos;
          return (eventPos - this.handleHalfDim) * this.options.scale;
        },
  
        /**
         * Get event names for move and event end
         *
         * @param {Event}    event    The event
         *
         * @return {{moveEvent: string, endEvent: string}}
         */
        getEventNames: function(event) {
          var eventNames = {
            moveEvent: '',
            endEvent: ''
          };
  
          if (event.touches || (event.originalEvent !== undefined && event.originalEvent.touches)) {
            eventNames.moveEvent = 'touchmove';
            eventNames.endEvent = 'touchend';
          } else {
            eventNames.moveEvent = 'mousemove';
            eventNames.endEvent = 'mouseup';
          }
  
          return eventNames;
        },
  
        /**
         * Get the handle closest to an event.
         *
         * @param event {Event} The event
         * @returns {jqLite} The handle closest to the event.
         */
        getNearestHandle: function(event) {
          if (!this.range) {
            return this.minH;
          }
          var offset = this.getEventPosition(event),
            distanceMin = Math.abs(offset - this.minH.rzsp),
            distanceMax = Math.abs(offset - this.maxH.rzsp);
          if (distanceMin < distanceMax)
            return this.minH;
          else if (distanceMin > distanceMax)
            return this.maxH;
          else if (!this.options.rightToLeft)
          //if event is at the same distance from min/max then if it's at left of minH, we return minH else maxH
            return offset < this.minH.rzsp ? this.minH : this.maxH;
          else
          //reverse in rtl
            return offset > this.minH.rzsp ? this.minH : this.maxH;
        },
  
        /**
         * Wrapper function to focus an angular element
         *
         * @param el {AngularElement} the element to focus
         */
        focusElement: function(el) {
          var DOM_ELEMENT = 0;
          el[DOM_ELEMENT].focus();
        },
  
        /**
         * Bind mouse and touch events to slider handles
         *
         * @returns {undefined}
         */
        bindEvents: function() {
          var barTracking, barStart, barMove;
  
          if (this.options.draggableRange) {
            barTracking = 'rzSliderDrag';
            barStart = this.onDragStart;
            barMove = this.onDragMove;
          } else {
            barTracking = 'lowValue';
            barStart = this.onStart;
            barMove = this.onMove;
          }
  
          if (!this.options.onlyBindHandles) {
            this.selBar.on('mousedown', angular.bind(this, barStart, null, barTracking));
            this.selBar.on('mousedown', angular.bind(this, barMove, this.selBar));
          }
  
          if (this.options.draggableRangeOnly) {
            this.minH.on('mousedown', angular.bind(this, barStart, null, barTracking));
            this.maxH.on('mousedown', angular.bind(this, barStart, null, barTracking));
          } else {
            this.minH.on('mousedown', angular.bind(this, this.onStart, this.minH, 'lowValue'));
            if (this.range) {
              this.maxH.on('mousedown', angular.bind(this, this.onStart, this.maxH, 'highValue'));
            }
            if (!this.options.onlyBindHandles) {
              this.fullBar.on('mousedown', angular.bind(this, this.onStart, null, null));
              this.fullBar.on('mousedown', angular.bind(this, this.onMove, this.fullBar));
              this.ticks.on('mousedown', angular.bind(this, this.onStart, null, null));
              this.ticks.on('mousedown', angular.bind(this, this.onTickClick, this.ticks));
            }
          }
  
          if (!this.options.onlyBindHandles) {
            this.selBar.on('touchstart', angular.bind(this, barStart, null, barTracking));
            this.selBar.on('touchstart', angular.bind(this, barMove, this.selBar));
          }
          if (this.options.draggableRangeOnly) {
            this.minH.on('touchstart', angular.bind(this, barStart, null, barTracking));
            this.maxH.on('touchstart', angular.bind(this, barStart, null, barTracking));
          } else {
            this.minH.on('touchstart', angular.bind(this, this.onStart, this.minH, 'lowValue'));
            if (this.range) {
              this.maxH.on('touchstart', angular.bind(this, this.onStart, this.maxH, 'highValue'));
            }
            if (!this.options.onlyBindHandles) {
              this.fullBar.on('touchstart', angular.bind(this, this.onStart, null, null));
              this.fullBar.on('touchstart', angular.bind(this, this.onMove, this.fullBar));
              this.ticks.on('touchstart', angular.bind(this, this.onStart, null, null));
              this.ticks.on('touchstart', angular.bind(this, this.onTickClick, this.ticks));
            }
          }
  
          if (this.options.keyboardSupport) {
            this.minH.on('focus', angular.bind(this, this.onPointerFocus, this.minH, 'lowValue'));
            if (this.range) {
              this.maxH.on('focus', angular.bind(this, this.onPointerFocus, this.maxH, 'highValue'));
            }
          }
        },
  
        /**
         * Unbind mouse and touch events to slider handles
         *
         * @returns {undefined}
         */
        unbindEvents: function() {
          this.minH.off();
          this.maxH.off();
          this.fullBar.off();
          this.selBar.off();
          this.ticks.off();
        },
  
        /**
         * onStart event handler
         *
         * @param {?Object} pointer The jqLite wrapped DOM element; if null, the closest handle is used
         * @param {?string} ref     The name of the handle being changed; if null, the closest handle's value is modified
         * @param {Event}   event   The event
         * @returns {undefined}
         */
        onStart: function(pointer, ref, event) {
          var ehMove, ehEnd,
            eventNames = this.getEventNames(event);
  
          event.stopPropagation();
          event.preventDefault();
  
          // We have to do this in case the HTML where the sliders are on
          // have been animated into view.
          this.calcViewDimensions();
  
          if (pointer) {
            this.tracking = ref;
          } else {
            pointer = this.getNearestHandle(event);
            this.tracking = pointer === this.minH ? 'lowValue' : 'highValue';
          }
  
          pointer.addClass('rz-active');
  
          if (this.options.keyboardSupport)
            this.focusElement(pointer);
  
          ehMove = angular.bind(this, this.dragging.active ? this.onDragMove : this.onMove, pointer);
          ehEnd = angular.bind(this, this.onEnd, ehMove);
  
          $document.on(eventNames.moveEvent, ehMove);
          $document.one(eventNames.endEvent, ehEnd);
          this.callOnStart();
        },
  
        /**
         * onMove event handler
         *
         * @param {jqLite} pointer
         * @param {Event}  event The event
         * @param {boolean}  fromTick if the event occured on a tick or not
         * @returns {undefined}
         */
        onMove: function(pointer, event, fromTick) {
          var newOffset = this.getEventPosition(event),
            newValue,
            ceilValue = this.options.rightToLeft ? this.minValue : this.maxValue,
            flrValue = this.options.rightToLeft ? this.maxValue : this.minValue;
  
          if (newOffset <= 0) {
            newValue = flrValue;
          } else if (newOffset >= this.maxPos) {
            newValue = ceilValue;
          } else {
            newValue = this.offsetToValue(newOffset);
            if (fromTick && angular.isNumber(this.options.showTicks))
              newValue = this.roundStep(newValue, this.options.showTicks);
            else
              newValue = this.roundStep(newValue);
          }
          this.positionTrackingHandle(newValue);
        },
  
        /**
         * onEnd event handler
         *
         * @param {Event}    event    The event
         * @param {Function} ehMove   The the bound move event handler
         * @returns {undefined}
         */
        onEnd: function(ehMove, event) {
          var moveEventName = this.getEventNames(event).moveEvent;
  
          if (!this.options.keyboardSupport) {
            this.minH.removeClass('rz-active');
            this.maxH.removeClass('rz-active');
            this.tracking = '';
          }
          this.dragging.active = false;
  
          $document.off(moveEventName, ehMove);
          this.callOnEnd();
        },
  
        onTickClick: function(pointer, event) {
          this.onMove(pointer, event, true);
        },
  
        onPointerFocus: function(pointer, ref) {
          this.tracking = ref;
          pointer.one('blur', angular.bind(this, this.onPointerBlur, pointer));
          pointer.on('keydown', angular.bind(this, this.onKeyboardEvent));
          pointer.on('keyup', angular.bind(this, this.onKeyUp));
          this.firstKeyDown = true;
          pointer.addClass('rz-active');
        },
  
        onKeyUp: function() {
          this.firstKeyDown = true;
          this.callOnEnd();
        },
  
        onPointerBlur: function(pointer) {
          pointer.off('keydown');
          pointer.off('keyup');
          this.tracking = '';
          pointer.removeClass('rz-active');
        },
  
        /**
         * Key actions helper function
         *
         * @param {number} currentValue value of the slider
         *
         * @returns {?Object} action value mappings
         */
        getKeyActions: function(currentValue) {
          var increaseStep = currentValue + this.step,
            decreaseStep = currentValue - this.step,
            increasePage = currentValue + this.valueRange / 10,
            decreasePage = currentValue - this.valueRange / 10;
  
          //Left to right default actions
          var actions = {
            'UP': increaseStep,
            'DOWN': decreaseStep,
            'LEFT': decreaseStep,
            'RIGHT': increaseStep,
            'PAGEUP': increasePage,
            'PAGEDOWN': decreasePage,
            'HOME': this.minValue,
            'END': this.maxValue
          };
          //right to left means swapping right and left arrows
          if (this.options.rightToLeft) {
            actions.LEFT = increaseStep;
            actions.RIGHT = decreaseStep;
            // right to left and vertical means we also swap up and down
            if (this.options.vertical) {
              actions.UP = decreaseStep;
              actions.DOWN = increaseStep;
            }
          }
          return actions;
        },
  
        onKeyboardEvent: function(event) {
          var currentValue = this[this.tracking],
            keyCode = event.keyCode || event.which,
            keys = {
              38: 'UP',
              40: 'DOWN',
              37: 'LEFT',
              39: 'RIGHT',
              33: 'PAGEUP',
              34: 'PAGEDOWN',
              36: 'HOME',
              35: 'END'
            },
            actions = this.getKeyActions(currentValue),
            key = keys[keyCode],
            action = actions[key];
          if (action == null || this.tracking === '') return;
          event.preventDefault();
  
          if (this.firstKeyDown) {
            this.firstKeyDown = false;
            this.callOnStart();
          }
  
          var self = this;
          $timeout(function() {
            var newValue = self.roundStep(self.sanitizeValue(action));
            if (!self.options.draggableRangeOnly) {
              self.positionTrackingHandle(newValue);
            }
            else {
              var difference = self.highValue - self.lowValue,
                newMinValue, newMaxValue;
              if (self.tracking === 'lowValue') {
                newMinValue = newValue;
                newMaxValue = newValue + difference;
                if (newMaxValue > self.maxValue) {
                  newMaxValue = self.maxValue;
                  newMinValue = newMaxValue - difference;
                }
              } else {
                newMaxValue = newValue;
                newMinValue = newValue - difference;
                if (newMinValue < self.minValue) {
                  newMinValue = self.minValue;
                  newMaxValue = newMinValue + difference;
                }
              }
              self.positionTrackingBar(newMinValue, newMaxValue);
            }
          });
        },
  
        /**
         * onDragStart event handler
         *
         * Handles dragging of the middle bar.
         *
         * @param {Object} pointer The jqLite wrapped DOM element
         * @param {string} ref     One of the refLow, refHigh values
         * @param {Event}  event   The event
         * @returns {undefined}
         */
        onDragStart: function(pointer, ref, event) {
          var offset = this.getEventPosition(event);
          this.dragging = {
            active: true,
            value: this.offsetToValue(offset),
            difference: this.highValue - this.lowValue,
            lowLimit: this.options.rightToLeft ? this.minH.rzsp - offset : offset - this.minH.rzsp,
            highLimit: this.options.rightToLeft ? offset - this.maxH.rzsp : this.maxH.rzsp - offset
          };
  
          this.onStart(pointer, ref, event);
        },
  
        /**
         * getValue helper function
         *
         * gets max or min value depending on whether the newOffset is outOfBounds above or below the bar and rightToLeft
         *
         * @param {string} type 'max' || 'min' The value we are calculating
         * @param {number} newOffset  The new offset
         * @param {boolean} outOfBounds Is the new offset above or below the max/min?
         * @param {boolean} isAbove Is the new offset above the bar if out of bounds?
         *
         * @returns {number}
         */
        getValue: function(type, newOffset, outOfBounds, isAbove) {
          var isRTL = this.options.rightToLeft,
            value = null;
  
          if (type === 'min') {
            if (outOfBounds) {
              if (isAbove) {
                value = isRTL ? this.minValue : this.maxValue - this.dragging.difference;
              } else {
                value = isRTL ? this.maxValue - this.dragging.difference : this.minValue;
              }
            } else {
              value = isRTL ? this.offsetToValue(newOffset + this.dragging.lowLimit) : this.offsetToValue(newOffset - this.dragging.lowLimit)
            }
          } else {
            if (outOfBounds) {
              if (isAbove) {
                value = isRTL ? this.minValue + this.dragging.difference : this.maxValue;
              } else {
                value = isRTL ? this.maxValue : this.minValue + this.dragging.difference;
              }
            } else {
              if (isRTL) {
                value = this.offsetToValue(newOffset + this.dragging.lowLimit) + this.dragging.difference
              } else {
                value = this.offsetToValue(newOffset - this.dragging.lowLimit) + this.dragging.difference;
              }
            }
          }
          return this.roundStep(value);
        },
  
        /**
         * onDragMove event handler
         *
         * Handles dragging of the middle bar.
         *
         * @param {jqLite} pointer
         * @param {Event}  event The event
         * @returns {undefined}
         */
        onDragMove: function(pointer, event) {
          var newOffset = this.getEventPosition(event),
            newMinValue, newMaxValue,
            ceilLimit, flrLimit,
            isUnderFlrLimit, isOverCeilLimit,
            flrH, ceilH;
  
          if (this.options.rightToLeft) {
            ceilLimit = this.dragging.lowLimit;
            flrLimit = this.dragging.highLimit;
            flrH = this.maxH;
            ceilH = this.minH;
          } else {
            ceilLimit = this.dragging.highLimit;
            flrLimit = this.dragging.lowLimit;
            flrH = this.minH;
            ceilH = this.maxH;
          }
          isUnderFlrLimit = newOffset <= flrLimit;
          isOverCeilLimit = newOffset >= this.maxPos - ceilLimit;
  
          if (isUnderFlrLimit) {
            if (flrH.rzsp === 0)
              return;
            newMinValue = this.getValue('min', newOffset, true, false);
            newMaxValue = this.getValue('max', newOffset, true, false);
          } else if (isOverCeilLimit) {
            if (ceilH.rzsp === this.maxPos)
              return;
            newMaxValue = this.getValue('max', newOffset, true, true);
            newMinValue = this.getValue('min', newOffset, true, true);
          } else {
            newMinValue = this.getValue('min', newOffset, false);
            newMaxValue = this.getValue('max', newOffset, false);
          }
          this.positionTrackingBar(newMinValue, newMaxValue);
        },
  
        /**
         * Set the new value and offset for the entire bar
         *
         * @param {number} newMinValue   the new minimum value
         * @param {number} newMaxValue   the new maximum value
         */
        positionTrackingBar: function(newMinValue, newMaxValue) {
          this.lowValue = newMinValue;
          this.highValue = newMaxValue;
          this.applyLowValue();
          if (this.range)
            this.applyHighValue();
          this.applyModel();
          this.updateHandles('lowValue', this.valueToOffset(newMinValue));
          this.updateHandles('highValue', this.valueToOffset(newMaxValue));
        },
  
        /**
         * Set the new value and offset to the current tracking handle
         *
         * @param {number} newValue new model value
         */
        positionTrackingHandle: function(newValue) {
          var valueChanged = false;
  
          newValue = this.applyMinMaxLimit(newValue);
          if (this.range) {
            if (this.options.pushRange) {
              newValue = this.applyPushRange(newValue);
              valueChanged = true;
            }
            else {
              if (this.options.noSwitching) {
                if (this.tracking === 'lowValue' && newValue > this.highValue)
                  newValue = this.applyMinMaxRange(this.highValue);
                else if (this.tracking === 'highValue' && newValue < this.lowValue)
                  newValue = this.applyMinMaxRange(this.lowValue);
              }
              newValue = this.applyMinMaxRange(newValue);
              /* This is to check if we need to switch the min and max handles */
              if (this.tracking === 'lowValue' && newValue > this.highValue) {
                this.lowValue = this.highValue;
                this.applyLowValue();
                this.updateHandles(this.tracking, this.maxH.rzsp);
                this.updateAriaAttributes();
                this.tracking = 'highValue';
                this.minH.removeClass('rz-active');
                this.maxH.addClass('rz-active');
                if (this.options.keyboardSupport)
                  this.focusElement(this.maxH);
                valueChanged = true;
              }
              else if (this.tracking === 'highValue' && newValue < this.lowValue) {
                this.highValue = this.lowValue;
                this.applyHighValue();
                this.updateHandles(this.tracking, this.minH.rzsp);
                this.updateAriaAttributes();
                this.tracking = 'lowValue';
                this.maxH.removeClass('rz-active');
                this.minH.addClass('rz-active');
                if (this.options.keyboardSupport)
                  this.focusElement(this.minH);
                valueChanged = true;
              }
            }
          }
  
          if (this[this.tracking] !== newValue) {
            this[this.tracking] = newValue;
            if (this.tracking === 'lowValue')
              this.applyLowValue();
            else
              this.applyHighValue();
            this.updateHandles(this.tracking, this.valueToOffset(newValue));
            this.updateAriaAttributes();
            valueChanged = true;
          }
  
          if (valueChanged)
            this.applyModel();
        },
  
        applyMinMaxLimit: function(newValue) {
          if (this.options.minLimit != null && newValue < this.options.minLimit)
            return this.options.minLimit;
          if (this.options.maxLimit != null && newValue > this.options.maxLimit)
            return this.options.maxLimit;
          return newValue;
        },
  
        applyMinMaxRange: function(newValue) {
          var oppositeValue = this.tracking === 'lowValue' ? this.highValue : this.lowValue,
            difference = Math.abs(newValue - oppositeValue);
          if (this.options.minRange != null) {
            if (difference < this.options.minRange) {
              if (this.tracking === 'lowValue')
                return this.highValue - this.options.minRange;
              else
                return this.lowValue + this.options.minRange;
            }
          }
          if (this.options.maxRange != null) {
            if (difference > this.options.maxRange) {
              if (this.tracking === 'lowValue')
                return this.highValue - this.options.maxRange;
              else
                return this.lowValue + this.options.maxRange;
            }
          }
          return newValue;
        },
  
        applyPushRange: function(newValue) {
          var difference = this.tracking === 'lowValue' ? this.highValue - newValue : newValue - this.lowValue,
            range = this.options.minRange !== null ? this.options.minRange : this.options.step;
          if (difference < range) {
            if (this.tracking === 'lowValue') {
              this.highValue = Math.min(newValue + range, this.maxValue);
              newValue = this.highValue - range;
              this.applyHighValue();
              this.updateHandles('highValue', this.valueToOffset(this.highValue));
            }
            else {
              this.lowValue = Math.max(newValue - range, this.minValue);
              newValue = this.lowValue + range;
              this.applyLowValue();
              this.updateHandles('lowValue', this.valueToOffset(this.lowValue));
            }
            this.updateAriaAttributes();
          }
          return newValue;
        },
  
        /**
         * Apply the model values using scope.$apply.
         * We wrap it with the internalChange flag to avoid the watchers to be called
         */
        applyModel: function() {
          this.internalChange = true;
          this.scope.$apply();
          this.callOnChange();
          this.internalChange = false;
        },
  
        /**
         * Call the onStart callback if defined
         * The callback call is wrapped in a $evalAsync to ensure that its result will be applied to the scope.
         *
         * @returns {undefined}
         */
        callOnStart: function() {
          if (this.options.onStart) {
            var self = this,
              pointerType = this.tracking === 'lowValue' ? 'min' : 'max';
            this.scope.$evalAsync(function() {
              self.options.onStart(self.options.id, self.scope.rzSliderModel, self.scope.rzSliderHigh, pointerType);
            });
          }
        },
  
        /**
         * Call the onChange callback if defined
         * The callback call is wrapped in a $evalAsync to ensure that its result will be applied to the scope.
         *
         * @returns {undefined}
         */
        callOnChange: function() {
          if (this.options.onChange) {
            var self = this,
              pointerType = this.tracking === 'lowValue' ? 'min' : 'max';
            this.scope.$evalAsync(function() {
              self.options.onChange(self.options.id, self.scope.rzSliderModel, self.scope.rzSliderHigh, pointerType);
            });
          }
        },
  
        /**
         * Call the onEnd callback if defined
         * The callback call is wrapped in a $evalAsync to ensure that its result will be applied to the scope.
         *
         * @returns {undefined}
         */
        callOnEnd: function() {
          if (this.options.onEnd) {
            var self = this,
              pointerType = this.tracking === 'lowValue' ? 'min' : 'max';
            this.scope.$evalAsync(function() {
              self.options.onEnd(self.options.id, self.scope.rzSliderModel, self.scope.rzSliderHigh, pointerType);
            });
          }
          this.scope.$emit('slideEnded');
        }
      };
  
      return Slider;
    }])
  
    .directive('rzslider', ['RzSlider', function(RzSlider) {
      'use strict';
  
      return {
        restrict: 'AE',
        replace: true,
        scope: {
          rzSliderModel: '=?',
          rzSliderHigh: '=?',
          rzSliderOptions: '&?',
          rzSliderTplUrl: '@'
        },
  
        /**
         * Return template URL
         *
         * @param {jqLite} elem
         * @param {Object} attrs
         * @return {string}
         */
        templateUrl: function(elem, attrs) {
          //noinspection JSUnresolvedVariable
          return attrs.rzSliderTplUrl || 'rzSliderTpl.html';
        },
  
        link: function(scope, elem) {
          scope.slider = new RzSlider(scope, elem); //attach on scope so we can test it
        }
      };
    }]);
  
    // IDE assist
  
    /**
     * @name ngScope
     *
     * @property {number} rzSliderModel
     * @property {number} rzSliderHigh
     * @property {Object} rzSliderOptions
     */
  
    /**
     * @name jqLite
     *
     * @property {number|undefined} rzsp rzslider label position offset
     * @property {number|undefined} rzsd rzslider element dimension
     * @property {string|undefined} rzsv rzslider label value/text
     * @property {Function} css
     * @property {Function} text
     */
  
    /**
     * @name Event
     * @property {Array} touches
     * @property {Event} originalEvent
     */
  
    /**
     * @name ThrottleOptions
     *
     * @property {boolean} leading
     * @property {boolean} trailing
     */
  
    module.run(['$templateCache', function($templateCache) {
    'use strict';
  
    $templateCache.put('rzSliderTpl.html',
      "<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>"
    );
  
  }]);
  
    return module.name
  }));
  