Version 3.17.2
Show:

File: slider/js/value-range.js

/**
 * Adds value support for Slider as a range of integers between a configured
 * minimum and maximum value.  For use with <code>Y.Base.build(..)</code> to
 * add the plumbing to <code>Y.SliderBase</code>.
 *
 * @module slider
 * @submodule slider-value-range
 */

// Constants for compression or performance
var MIN       = 'min',
    MAX       = 'max',
    VALUE     = 'value',
//     MINORSTEP = 'minorStep',
//     MAJORSTEP = 'majorStep',

    round = Math.round;

/**
 * One class of value algorithm that can be built onto SliderBase.  By default,
 * values range between 0 and 100, but you can configure these on the
 * built Slider class by setting the <code>min</code> and <code>max</code>
 * configurations.  Set the initial value (will cause the thumb to move to the
 * appropriate location on the rail) in configuration as well if appropriate.
 *
 * @class SliderValueRange
 */
function SliderValueRange() {
    this._initSliderValueRange();
}

Y.SliderValueRange = Y.mix( SliderValueRange, {

    // Prototype properties and methods that will be added onto host class
    prototype: {

        /**
         * Factor used to translate value -&gt; position -&gt; value.
         *
         * @property _factor
         * @type {Number}
         * @protected
         */
        _factor: 1,

        /**
         * Stub for construction logic.  Override if extending this class and
         * you need to set something up during the initializer phase.
         *
         * @method _initSliderValueRange
         * @protected
         */
        _initSliderValueRange: function () {},

        /**
         * Override of stub method in SliderBase that is called at the end of
         * its bindUI stage of render().  Subscribes to internal events to
         * trigger UI and related state updates.
         *
         * @method _bindValueLogic
         * @protected
         */
        _bindValueLogic: function () {
            this.after( {
                minChange  : this._afterMinChange,
                maxChange  : this._afterMaxChange,
                valueChange: this._afterValueChange
            } );
        },

        /**
         * Move the thumb to appropriate position if necessary.  Also resets
         * the cached offsets and recalculates the conversion factor to
         * translate position to value.
         *
         * @method _syncThumbPosition
         * @protected
         */
        _syncThumbPosition: function () {
            this._calculateFactor();

            this._setPosition( this.get( VALUE ) );
        },

        /**
         * Calculates and caches
         * (range between max and min) / (rail length)
         * for fast runtime calculation of position -&gt; value.
         *
         * @method _calculateFactor
         * @protected
         */
        _calculateFactor: function () {
            var length    = this.get( 'length' ),
                thumbSize = this.thumb.getStyle( this._key.dim ),
                min       = this.get( MIN ),
                max       = this.get( MAX );

            // The default thumb width is based on Sam skin's thumb dimension.
            // This attempts to allow for rendering off-DOM, then attaching
            // without the need to call syncUI().  It is still recommended
            // to call syncUI() in these cases though, just to be sure.
            length = parseFloat( length ) || 150;
            thumbSize = parseFloat( thumbSize ) || 15;

            this._factor = ( max - min ) / ( length - thumbSize );

            Y.log("Calculating factor(~" + this._factor.toFixed(3) + " = (max(" + max + ") - min(" + min + ")) / (length(" + length + ") - thumb size(" + thumbSize + "))","info","slider");
        },

        /**
         * Dispatch the new position of the thumb into the value setting
         * operations.
         *
         * @method _defThumbMoveFn
         * @param e { EventFacade } The host's thumbMove event
         * @protected
         */
        _defThumbMoveFn: function ( e ) {
            // To prevent set('value', x) from looping back around
            if (e.source !== 'set') {
                this.set(VALUE, this._offsetToValue(e.offset));
            }
        },

        /**
         * <p>Converts a pixel position into a value.  Calculates current
         * thumb offset from the leading edge of the rail multiplied by the
         * ratio of <code>(max - min) / (constraining dim)</code>.</p>
         *
         * <p>Override this if you want to use a different value mapping
         * algorithm.</p>
         *
         * @method _offsetToValue
         * @param offset { Number } X or Y pixel offset
         * @return { mixed } Value corresponding to the provided pixel offset
         * @protected
         */
        _offsetToValue: function ( offset ) {

            var value = round( offset * this._factor ) + this.get( MIN );

            Y.log("Offset: " + offset + " => Value: " + value, "info", "slider");
            return round( this._nearestValue( value ) );
        },

        /**
         * Converts a value into a pixel offset for use in positioning
         * the thumb according to the reverse of the
         * <code>_offsetToValue( xy )</code> operation.
         *
         * @method _valueToOffset
         * @param val { Number } The value to map to pixel X or Y position
         * @return { Number } The pixel offset
         * @protected
         */
        _valueToOffset: function ( value ) {
            var offset = round( ( value - this.get( MIN ) ) / this._factor );

            Y.log("Value: " + value + " => Offset: " + offset, "info", "slider");
            return offset;
        },

        /**
         * Returns the current value.  Override this if you want to introduce
         * output formatting. Otherwise equivalent to slider.get( "value" );
         *
         * @method getValue
         * @return {Number}
         */
        getValue: function () {
            return this.get( VALUE );
        },

        /**
         * Updates the current value.  Override this if you want to introduce
         * input value parsing or preprocessing.  Otherwise equivalent to
         * slider.set( "value", v );
         *
         * @method setValue
         * @param val {Number} The new value
         * @return {Slider}
         * @chainable
         */
        setValue: function ( val ) {
            return this.set( VALUE, val );
        },

        /**
         * Update position according to new min value.  If the new min results
         * in the current value being out of range, the value is set to the
         * closer of min or max.
         *
         * @method _afterMinChange
         * @param e { EventFacade } The <code>min</code> attribute change event.
         * @protected
         */
        _afterMinChange: function ( e ) {
            this._verifyValue();

            this._syncThumbPosition();
        },

        /**
         * Update position according to new max value.  If the new max results
         * in the current value being out of range, the value is set to the
         * closer of min or max.
         *
         * @method _afterMaxChange
         * @param e { EventFacade } The <code>max</code> attribute change event.
         * @protected
         */
        _afterMaxChange: function ( e ) {
            this._verifyValue();

            this._syncThumbPosition();
        },

        /**
         * Verifies that the current value is within the min - max range.  If
         * not, value is set to either min or max, depending on which is
         * closer.
         *
         * @method _verifyValue
         * @protected
         */
        _verifyValue: function () {
            var value   = this.get( VALUE ),
                nearest = this._nearestValue( value );

            if ( value !== nearest ) {
                // @TODO Can/should valueChange, minChange, etc be queued
                // events? To make dd.set( 'min', n ); execute after minChange
                // subscribers before on/after valueChange subscribers.
                this.set( VALUE, nearest );
            }
        },

        /**
         * Propagate change to the thumb position unless the change originated
         * from the thumbMove event.
         *
         * @method _afterValueChange
         * @param e { EventFacade } The <code>valueChange</code> event.
         * @protected
         */
        _afterValueChange: function ( e ) {
            var val = e.newVal;
            Y.log("Positioning thumb after set('value',x)","info","slider");
            this._setPosition( val, { source: 'set' } );
        },

        /**
         * Positions the thumb and its ARIA attributes in accordance with the
         * translated value.
         *
         * @method _setPosition
         * @param value {Number} Value to translate to a pixel position
         * @param [options] {Object} Details object to pass to `_uiMoveThumb`
         * @protected
         */
        _setPosition: function ( value, options ) {
            this._uiMoveThumb( this._valueToOffset( value ), options );
            this.thumb.set('aria-valuenow', value);
            this.thumb.set('aria-valuetext', value);
        },

        /**
         * Validates new values assigned to <code>min</code> attribute.  Numbers
         * are acceptable.  Override this to enforce different rules.
         *
         * @method _validateNewMin
         * @param value {Any} Value assigned to <code>min</code> attribute.
         * @return {Boolean} True for numbers.  False otherwise.
         * @protected
         */
        _validateNewMin: function ( value ) {
            return Y.Lang.isNumber( value );
        },

        /**
         * Validates new values assigned to <code>max</code> attribute.  Numbers
         * are acceptable.  Override this to enforce different rules.
         *
         * @method _validateNewMax
         * @param value { mixed } Value assigned to <code>max</code> attribute.
         * @return { Boolean } True for numbers.  False otherwise.
         * @protected
         */
        _validateNewMax: function ( value ) {
            return Y.Lang.isNumber( value );
        },

        /**
         * Restricts new values assigned to <code>value</code> attribute to be
         * between the configured <code>min</code> and <code>max</code>.
         * Rounds to nearest integer value.
         *
         * @method _setNewValue
         * @param value { Number } Value assigned to <code>value</code> attribute
         * @return { Number } Normalized and constrained value
         * @protected
         */
        _setNewValue: function ( value ) {
            if ( Y.Lang.isNumber( value ) ) {
                return round( this._nearestValue( value ) );
            }
            return Y.Attribute.INVALID_VALUE;
        },

        /**
         * Returns the nearest valid value to the value input.  If the provided
         * value is outside the min - max range, accounting for min > max
         * scenarios, the nearest of either min or max is returned.  Otherwise,
         * the provided value is returned.
         *
         * @method _nearestValue
         * @param value { mixed } Value to test against current min - max range
         * @return { Number } Current min, max, or value if within range
         * @protected
         */
        _nearestValue: function ( value ) {
            var min = this.get( MIN ),
                max = this.get( MAX ),
                tmp;

            // Account for reverse value range (min > max)
            tmp = ( max > min ) ? max : min;
            min = ( max > min ) ? min : max;
            max = tmp;

            return ( value < min ) ?
                    min :
                    ( value > max ) ?
                        max :
                        value;
        }

    },

    /**
     * Attributes that will be added onto host class.
     *
     * @property ATTRS
     * @type {Object}
     * @static
     * @protected
     */
    ATTRS: {
        /**
         * The value associated with the farthest top, left position of the
         * rail.  Can be greater than the configured <code>max</code> if you
         * want values to increase from right-to-left or bottom-to-top.
         *
         * @attribute min
         * @type { Number }
         * @default 0
         */
        min: {
            value    : 0,
            validator: '_validateNewMin'
        },

        /**
         * The value associated with the farthest bottom, right position of
         * the rail.  Can be less than the configured <code>min</code> if
         * you want values to increase from right-to-left or bottom-to-top.
         *
         * @attribute max
         * @type { Number }
         * @default 100
         */
        max: {
            value    : 100,
            validator: '_validateNewMax'
        },

        /**
         * amount to increment/decrement the Slider value
         * when the arrow up/down/left/right keys are pressed
         *
         * @attribute minorStep
         * @type {Number}
         * @default 1
         */
        minorStep : {
            value: 1
        },

        /**
         * amount to increment/decrement the Slider value
         * when the page up/down keys are pressed
         *
         * @attribute majorStep
         * @type {Number}
         * @default 10
         */
        majorStep : {
            value: 10
        },

        /**
         * The value associated with the thumb's current position on the
         * rail. Defaults to the value inferred from the thumb's current
         * position. Specifying value in the constructor will move the
         * thumb to the position that corresponds to the supplied value.
         *
         * @attribute value
         * @type { Number }
         * @default (inferred from current thumb position)
         */
        value: {
            value : 0,
            setter: '_setNewValue'
        }
    }
}, true );