Yahoo! UI Library

slider  3.0.0

Yahoo! UI Library > slider > slider.js (source view)
Search:
 
Filters
/**
 * Create a sliding value range input visualized as a draggable thumb on a
 * background element.
 * 
 * @module slider
 */

var SLIDER = 'slider',
    RAIL   = 'rail',
    THUMB  = 'thumb',
    VALUE  = 'value',
    MIN    = 'min',
    MAX    = 'max',
    MIN_GUTTER = 'minGutter',
    MAX_GUTTER = 'maxGutter',
    THUMB_IMAGE = 'thumbImage',
    RAIL_SIZE   = 'railSize',
    CONTENT_BOX = 'contentBox',

    SLIDE_START = 'slideStart',
    SLIDE_END   = 'slideEnd',

    THUMB_DRAG  = 'thumbDrag',
    SYNC        = 'sync',
    POSITION_THUMB = 'positionThumb',
    RENDERED    = 'rendered',
    DISABLED    = 'disabled',
    DISABLED_CHANGE = 'disabledChange',

    DOT      = '.',
    PX       = 'px',
    WIDTH    = 'width',
    HEIGHT   = 'height',
    COMPLETE = 'complete',

    L = Y.Lang,
    isBoolean= L.isBoolean,
    isString = L.isString,
    isNumber = L.isNumber,
    
    getCN    = Y.ClassNameManager.getClassName,

    IMAGE         = 'image',
    C_RAIL        = getCN(SLIDER,RAIL),
    C_THUMB       = getCN(SLIDER,THUMB),
    C_THUMB_IMAGE = getCN(SLIDER,THUMB,IMAGE),
    C_IMAGE_ERROR = getCN(SLIDER,IMAGE,'error'),

    M        = Math,
    max      = M.max,
    round    = M.round,
    floor    = M.floor;

/**
 * Create a slider to represent an integer value between a given minimum and
 * maximum.  Sliders may be aligned vertically or horizontally, based on the
 * <code>axis</code> configuration.
 *
 * @class Slider
 * @extends Widget
 * @param config {Object} Configuration object
 * @constructor
 */
function Slider() {
    Slider.superclass.constructor.apply(this,arguments);
}

Y.mix(Slider, {

    /**
     * The identity of the widget.
     *
     * @property Slider.NAME
     * @type String
     * @static
     */
    NAME : SLIDER,

    /**
     * Object property names used for respective X and Y axis Sliders (e.g.
     * &quot;left&quot; vs. &quot;top&quot; for placing the thumb according to
     * its representative value).
     *
     * @property Slider._AXIS_KEYS
     * @type Object
     * @protected
     * @static
     */
    _AXIS_KEYS : {
        x : {
            dim           : WIDTH,
            offAxisDim    : HEIGHT,
            eventPageAxis : 'pageX',
            ddStick       : 'stickX',
            xyIndex       : 0
        },
        y : {
            dim           : HEIGHT,
            offAxisDim    : WIDTH,
            eventPageAxis : 'pageY',
            ddStick       : 'stickY',
            xyIndex       : 1
        }
    },

    /**
     * Static Object hash used to capture existing markup for progressive
     * enhancement.  Keys correspond to config attribute names and values
     * are selectors used to inspect the contentBox for an existing node
     * structure.
     *
     * @property Slider.HTML_PARSER
     * @type Object
     * @protected
     * @static
     */
    HTML_PARSER : {
        rail       : DOT + C_RAIL,
        thumb      : DOT + C_THUMB,
        thumbImage : DOT + C_THUMB_IMAGE
    },

    /**
     * Static property used to define the default attribute configuration of
     * the Widget.
     *
     * @property Slider.ATTRS
     * @type Object
     * @protected
     * @static
     */
    ATTRS : {

        /**
         * Axis upon which the Slider's thumb moves.  &quot;x&quot; for
         * horizontal, &quot;y&quot; for vertical.
         *
         * @attribute axis
         * @type String
         * @default &quot;x&quot;
         * @writeOnce
         */
        axis : {
            value : 'x',
            writeOnce : true,
            validator : function (v) {
                return this._validateNewAxis(v);
            },
            setter : function (v) {
                return this._setAxisFn(v);
            }
        },

        /**
         * Value associated with the left or top most position of the thumb on
         * the rail.
         *
         * @attribute min
         * @type Number
         * @default 0
         */
        min : {
            value : 0,
            validator : function (v) {
                return this._validateNewMin(v);
            }
        },

        /**
         * Value associated with the right or bottom most position of the thumb
         * on the rail.
         *
         * @attribute max
         * @type Number
         * @default 100
         */
        max : {
            value : 100,
            validator : function (v) {
                return this._validateNewMax(v);
            }
        },

        /**
         * The current value of the Slider.  This value is interpretted into a
         * position for the thumb along the Slider's rail.
         *
         * @attribute value
         * @type Number
         * @default 0
         */
        value : {
            value : 0,
            validator : function (v) {
                return this._validateNewValue(v);
            }
        },

        /**
         * The Node representing the Slider's rail, usually visualized as a
         * bar of some sort using a background image, along which the thumb
         * moves.  This Node contains the thumb Node.
         *
         * @attribute rail
         * @type Node
         * @default null
         */
        rail : {
            value : null,
            validator : function (v) {
                return this._validateNewRail(v);
            },
            setter : function (v) {
                return this._setRailFn(v);
            }
        },

        /**
         * <p>The Node representing the Slider's thumb, usually visualized as a
         * pointer using a contained image Node (see thumbImage).  The current
         * value of the Slider is calculated from the centerpoint of this
         * Node in relation to the rail Node.  If provided, the thumbImage
         * Node is contained within this Node.</p>
         *
         * <p>If no thumbImage is provided and the Node passed as the thumb is
         * an <code>img</code> element, the assigned Node will be allocated to
         * the thumbImage and the thumb container defaulted.</p>
         *
         * @attribute thumb
         * @type Node
         * @default null
         */
        thumb : {
            value : null,
            validator : function (v) {
                return this._validateNewThumb(v);
            },
            setter : function (v) {
                return this._setThumbFn(v);
            }
        },

        /**
         * <p>The Node representing the image element to use for the Slider's
         * thumb.</p>
         *
         * <p>Alternately, an image URL can be passed and an <code>img</code>
         * Node will be generated accordingly.</p>
         *
         * <p>If no thumbImage is provided and the Node passed as the thumb is
         * an <code>img</code> element, the assigned Node will be allocated to
         * the thumbImage and the thumb container defaulted.</p>
         *
         * <p>If thumbImage is provided but its URL resolves to a 404, a default
         * style will be applied to maintain basic functionality.</p>
         *
         * @attribute thumbImage
         * @type Node|String
         * @default null
         */
        thumbImage : {
            value : null,
            validator : function (v) {
                return this._validateNewThumbImage(v);
            },
            setter : function (v) {
                return this._setThumbImageFn(v);
            }
        },

        /**
         * <p>The width or height of the rail element representing the physical
         * space along which the thumb can move.  CSS size values (e.g. '30em')
         * accepted but converted to pixels during render.</p>
         *
         * <p>Alternately, but not recommended, this attribute can be left
         * unassigned in favor of specifying height or width.</p>
         *
         * @attribute railSize
         * @type String
         * @default '0'
         */
        railSize : {
            value : '0',
            validator : function (v) {
                return this._validateNewRailSize(v);
            }
        },

        /**
         * Boolean indicating whether clicking and dragging on the rail will
         * trigger thumb movement.
         *
         * @attribute railEnabled
         * @type Boolean
         * @default true
         */
        railEnabled : {
            value : true,
            validator : isBoolean
        },

        /**
         * Like CSS padding, the distance in pixels from the inner top or left
         * edge of the rail node within which the thumb can travel.  Negative
         * values allow the edge of the thumb to escape the rail node
         * boundaries.
         *
         * @attribute minGutter
         * @type Number
         * @default 0
         */
        minGutter : {
            value : 0,
            validator : isNumber
        },

        /**
         * Like CSS padding, the distance in pixels from the inner bottom or
         * right edge of the rail node within which the thumb can travel.
         * Negative values allow the edge of the thumb to escape the rail node
         * boundaries.
         *
         * @attribute maxGutter
         * @type Number
         * @default 0
         */
        maxGutter : {
            value : 0,
            validator : isNumber
        }
    }
});

Y.extend(Slider, Y.Widget, {

    /**
     * Collection of object property names from the appropriate hash set in
     * Slider._AXIS_KEYS.
     *
     * @property _key
     * @type Object
     * @protected
     */
    _key : null,

    /**
     * Factor used to translate positional coordinates (e.g. left or top) to
     * the Slider's value.
     *
     * @property _factor
     * @type Number
     * @protected
     */
    _factor : 1,

    /**
     * Pixel dimension of the rail Node's width for X axis Sliders or height
     * for Y axis Sliders.  Used with _factor to calculate positional
     * coordinates for the thumb.
     *
     * @property _railSize
     * @type Number
     * @protected
     */
    _railSize : null,

    /**
     * Pixel dimension of the thumb Node's width for X axis Sliders or height
     * for Y axis Sliders.  Used with _factor to calculate positional
     * coordinates for the thumb.
     *
     * @property _thumbSize
     * @type Number
     * @protected
     */
    _thumbSize : null,

    /**
     * Pixel offset of the point in the thumb element from its top/left edge
     * to where the value calculation should take place.  By default, this is
     * calculated to half the width of the thumb, causing the value to be
     * marked from the center of the thumb.
     *
     * @property _thumbOffset
     * @type Number
     * @protected
     */
    _thumbOffset : 0,

    /**
     * Object returned from temporary subscription to disabledChange event to
     * defer setting the disabled state while Slider is loading the thumb
     * image.
     *
     * @property _stall
     * @type Object
     * @protected
     */
    _stall : false,

    /**
     * Deferred value for the disabled attribute when stalled (see _stall
     * property).
     *
     * @property _disabled
     * @type Boolean
     * @protected
     */
    _disabled : false,

    /**
     * Construction logic executed durint Slider instantiation. Subscribes to
     * after events for min, max, and railSize.  Publishes custom events
     * including slideStart and slideEnd.
     *
     * @method initializer
     * @protected
     */
    initializer : function () {
        this._key = Slider._AXIS_KEYS[this.get('axis')];

        this.after('minChange',      this._afterMinChange);
        this.after('maxChange',      this._afterMaxChange);

        this.after('railSizeChange', this._afterRailSizeChange);

        /**
         * Signals the beginning of a thumb drag operation.  Payload includes
         * the DD.Drag instance's drag:start event under key ddEvent.
         *
         * @event slideStart
         * @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
         *  <dl>
         *      <dt>ddEvent</dt>
         *          <dd><code>drag:start</code> event from the managed DD.Drag instance</dd>
         *  </dl>
         */
        this.publish(SLIDE_START);

        /**
         * Signals the end of a thumb drag operation.  Payload includes
         * the DD.Drag instance's drag:end event under key ddEvent.
         *
         * @event slideEnd
         * @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
         *  <dl>
         *      <dt>ddEvent</dt>
         *          <dd><code>drag:end</code> event from the managed DD.Drag instance</dd>
         *  </dl>
         */
        this.publish(SLIDE_END);

        /**
         * Communicates a request to synchronize the Slider UI with the
         * attribute state.  Links the sync request with the default sync
         * logic in _defSyncFn.
         *
         * @event sync
         * @param event {Event.Facade} Event Facade object
         * @preventable _defSyncFn
         */
        this.publish(SYNC, { defaultFn: this._defSyncFn });

        /**
         * Signals a request to reposition the thumb in response to API methods.
         * Triggers the thumb placement logic in _defPositionThumbFn.
         *
         * @event positionThumb
         * @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
         *  <dl>
         *      <dt>changeEv</dt>
         *          <dd><code>valueChange</code> event fired in response to the change in the value attribute</dd>
         *  </dl>
         * @preventable _defPositionThumbFn
         */
        this.publish(POSITION_THUMB, { defaultFn: this._defPositionThumbFn });
    },

    /**
     * Create the DOM structure for the Slider.
     *
     * @method renderUI
     * @protected
     */
    renderUI : function () {
        this._initRail();
        this._initThumb();
    },

    /**
     * Creates the rail element if not provided and not discovered via
     * HTML_PARSER.
     *
     * @method _initRail
     * @protected
     */
    _initRail : function () {
        var cb   = this.get(CONTENT_BOX),
            rail = this.get(RAIL);

        // Create rail if necessary. Make sure it's in the contentBox
        if (!rail) {
            rail = cb.appendChild(
                Y.Node.create('<div class="'+C_RAIL+'"></div>'));

            this.set(RAIL,rail);
        } else if (!cb.contains(rail)) {
            cb.appendChild(rail);
        }

        rail.addClass(C_RAIL);
        rail.addClass(this.getClassName(RAIL,this.get('axis')));
    },

    /**
     * <p>Creates the thumb element (not image) if not provided and not
     * discovered via HTML_PARSER.  If the thumb is an <code>img</code> element
     * but no thumbImage configured or discovered, reassigns the thumb element
     * to the thumbImage and defaults the thumb element as a div.</p>
     *
     * <p>Makes sure the thumb is a child of the rail element and calls
     * _initThumbImage if thumbImage is provided.</p>
     *
     * @method _initThumb
     * @protected
     */
    _initThumb : function () {
        var rail    = this.get(RAIL),
            thumb   = this.get(THUMB);

        // Passed an img element as the thumb
        if (thumb && !this.get(THUMB_IMAGE) &&
            thumb.get('nodeName').toLowerCase() === 'img') {
            this.set(THUMB_IMAGE, thumb);
            this.set(THUMB,null);
            thumb = null;
        }

        if (!thumb) {
            thumb = Y.Node.create(
                '<div class="'+C_THUMB+'"></div>');

            this.set(THUMB,thumb);
        }

        thumb.addClass(C_THUMB);

        if (!rail.contains(thumb)) {
            rail.appendChild(thumb);
        }

        if (this.get(THUMB_IMAGE)) {
            this._initThumbImage();
        }
    },

    /**
     * Ensures the thumbImage is a child of the thumb element.
     *
     * @method _initThumbImage
     * @protected
     */
    _initThumbImage : function () {
        var thumb = this.get(THUMB),
            img   = this.get(THUMB_IMAGE);

        if (img) {
            img.replaceClass(C_THUMB,C_THUMB_IMAGE);

            if (!thumb.contains(img)) {
                thumb.appendChild(img);
            }
        }
    },

    /**
     * Creates the Y.DD instance used to handle the thumb movement and binds
     * Slider interaction to the configured value model.
     *
     * @method bindUI
     * @protected
     */
    bindUI : function () {
        /**
         * Bridges user interaction with the thumb to the value attribute.
         *
         * @event thumbDrag
         * @param event {Event.Facade} An Event Facade object with the following attribute specific properties added:
         *  <dl>
         *      <dt>ddEvent</dt>
         *          <dd><code>drag:drag</code> event from the managed DD.Drag instance</dd>
         *  </dl>
         * @preventable _defThumbDragFn
         */
        this.publish(THUMB_DRAG, {defaultFn: this._defThumbDragFn});

        this._bindThumbDD();

        this.after('valueChange',      this._afterValueChange);
        this.after('thumbImageChange', this._afterThumbImageChange);
        this.after(DISABLED_CHANGE,    this._afterDisabledChange);
    },

    /**
     * Creates the Y.DD instance used to handle the thumb interaction.
     * 
     * @method _bindThumbDD
     * @protected
     */
    _bindThumbDD : function () {
        var ddConf = {
                node : this.get(THUMB),
                bubble : false
            },
            conConf = {
                constrain2node : this.get(RAIL)
            };

        conConf[this._key.ddStick] = true;

        this._dd = new Y.DD.Drag(ddConf).plug(Y.Plugin.DDConstrained, conConf);
        this._dd.on('drag:start', Y.bind(this._onDDStartDrag, this));
        this._dd.on('drag:drag',  Y.bind(this._onDDDrag,      this));
        this._dd.on('drag:end',   Y.bind(this._onDDEndDrag,   this));

        this._initRailDD();
    },

    /**
     * Subscribes to the rail Node's mousedown event to actuate the thumb when
     * backgroundEnabled is true.
     *
     * @method _initRailDD
     * @protected
     */
    _initRailDD : function () {
        this.get(RAIL).on('mousedown',Y.bind(this._handleRailMouseDown,this));
    },

    /**
     * If the Slider is not disabled and railEnabled is true, moves the thumb
     * to the mousedown position and hands control over to DD.
     *
     * @method _handleRailMouseDown
     * @param e {Event} Mousedown event facade
     * @protected
     */
    _handleRailMouseDown : function (e) {
        if (this.get('railEnabled') && !this.get(DISABLED)) {
            var dd      = this._dd,
                xyIndex = this._key.xyIndex,
                xy;

            if (dd.get('primaryButtonOnly') && e.button > 1) {
                Y.log('Mousedown was not produced by the primary button',
                      'warn', 'dd-drag');
                return false;
            }

            dd._dragThreshMet = true;

            dd._fixIEMouseDown();
            e.halt();

            Y.DD.DDM.activeDrag = dd;

            // Adjust registered starting position by half the thumb's x/y
            xy = dd.get('dragNode').getXY();
            xy[xyIndex] += this._thumbOffset;

            dd._setStartPosition(xy);
            dd.set('activeHandle',dd.get('dragNode'));

            dd.start();
            dd._alignNode([e.pageX,e.pageY]);
        }
    },

    /**
     * Synchronizes the DOM state with the attribute settings (most notably
     * railSize and value).  If thumbImage is provided and is still loading,
     * sync is delayed until it is complete, since the image's dimensions are
     * taken into consideration for calculations.
     *
     * @method syncUI
     */
    syncUI : function () {
        this.get(CONTENT_BOX).removeClass(C_IMAGE_ERROR);

        var img = this.get(THUMB_IMAGE);

        if (this._isImageLoading(img)) {
            Y.log('Thumb image loading. Scheduling sync.','info','slider');
            // Schedule the sync for when the image loads/errors
            this._scheduleSync();
        } else {
            Y.log('No thumb image, or image already loaded. Syncing immediately.','info','slider');
            this._ready(img,!this._isImageLoaded(img));
        }
    },

    /**
     * Binds to the load and error event on the thumbImage to sync the DOM
     * state with the attribute settings when the image resource is resolved.
     * The Slider is disabled while it waits.
     *
     * @method _scheduleSync
     * @protected
     */
    _scheduleSync : function () {
        var img, handler;

        if (!this._stall) {
            // disable the control until the image is loaded
            this._disabled = this.get(DISABLED);
            this.set(DISABLED,true);
            this._stall    = this.on(DISABLED_CHANGE,this._stallDisabledChange);

            img     = this.get(THUMB_IMAGE);
            handler = Y.bind(this._imageLoaded,this,img);
            img.on('load', handler);
            img.on('error',handler);
        }
    },

    /**
     * Method subscribed to the disabledChange event when thumbImage is being
     * loaded.  Prevents manually enabling the Slider until the thumbImage
     * resource is resolved.  Intended value is stored during load and set upon
     * completion.
     *
     * @method _stallDisabledChange
     * @param e {Event} Change event for the disabled attribute
     * @protected
     */
    _stallDisabledChange : function (e) {
        this._disabled = e.newVal;
        e.preventDefault();
    },

    /**
     * Event handler assigned to the thumbImage's load and error event if it
     * was not loaded prior to instantiation.  Restores the disabled value.
     *
     * @method _imageLoaded
     * @param img {Node} The thumbImage Node
     * @param e {Event} load or error event fired by the thumbImage
     * @protected
     */
    _imageLoaded : function (img,e) {
        var error = (e.type.toLowerCase().indexOf('error') > -1);

        // Need to execute inside a setTimeout because IE doesn't report
        // img.complete === true until after the img.onload handler
        // @TODO: readyState reports correctly in onload.  Lose this wrapper
        // and use that in _isImageLoaded.
        Y.later(0, this, function () {
            if (this._stall) {
                this._stall.detach();
            }

            Y.log('Thumb image '+e.type+'ed.  Syncing','info','slider');

            this._stall = false;

            this._ready(img,error);

            this.set(DISABLED,this._disabled);
        });
    },

    /**
     * Applies a class to the content box if the thumbImage failed to resolve,
     * the fires the internal sync event triggering a sync between UI and
     * state.
     *
     * @method _ready
     * @param img {Node} the thumbImage Node
     * @param error {Boolean} Indicates an error while loading the thumbImage
     * @protected
     */
    _ready : function (img,error) {
        var method = error ? 'addClass' : 'removeClass';

        // If the thumb image url results in 404, assign a class to provide
        // default thumb dimensions/UI
        this.get(CONTENT_BOX)[method](C_IMAGE_ERROR);

        this.fire(SYNC);
    },

    /**
     * The default synchronization behavior, updating the Slider's DOM state to
     * match the current attribute values.
     *
     * @method _defSyncFn
     * @param e {Event} Internal sync event
     * @protected
     */
    _defSyncFn : function (e) {
        this._uiSetThumbSize();

        this._setThumbOffset();

        this._uiSetRailSize();

        this._setRailOffsetXY();

        this._setDDGutter();

        this._resetDDCacheRegion();

        this._setFactor();

        var val = this.get(VALUE);

        this.fire(POSITION_THUMB, {
            value  : val,
            offset : this._convertValueToOffset(val)
        });

        // Forces a reflow of the bounding box to address IE8 inline-block
        // container not expanding correctly. bug 2527905
        this.get('boundingBox').toggleClass('');
    },

    /**
     * Captures the thumb's pixel height or width (depending on the Slider's
     * axis) for use in positioning calculations.
     *
     * @method _uiSetThumbSize
     * @protected
     */
    _uiSetThumbSize : function () {
        var thumb = this.get(THUMB),
            dim   = this._key.dim,
            img   = this.get(THUMB_IMAGE),
            size;

        // offsetWidth fails in hidden containers
        size = parseInt(thumb.getComputedStyle(dim),10);

        Y.log('thumb '+dim+': '+size+'px','info','slider');

        if (img && this._isImageLoaded(img)) {
            Y.log('using thumbImage '+dim+' ('+img.get(dim)+') for _thumbSize','info','slider');

            size = img.get(dim);
        }

        this._thumbSize = size;
    },

    /**
     * Establishes the point in the thumb that should align to the rail
     * position representing the calculated value.
     *
     * @method _setThumbOffset
     * @protected
     */
    _setThumbOffset : function () {
        this._thumbOffset = floor(this._thumbSize / 2);
        Y.log('_thumbOffset calculated to '+this._thumbOffset+'px','info','slider');
    },

    /**
     * Stores the rail Node's pixel height or width, depending on the Slider's
     * axis, for use in calculating thumb position from the value.
     *
     * @method _uiSetRailSize
     * @protected
     */
    _uiSetRailSize : function () {
        var rail  = this.get(RAIL),
            thumb = this.get(THUMB),
            img   = this.get(THUMB_IMAGE),
            dim   = this._key.dim,
            size  = this.get(RAIL_SIZE),
            setxy = false;

        if (parseInt(size,10)) {
            Y.log('railSize provided: '+size,'info','slider');

            // Convert to pixels
            rail.setStyle(dim,size);
            size = parseInt(rail.getComputedStyle(dim),10);

            Y.log('pixel '+dim+' of railSize: '+size+'px', 'info', 'slider');
        } else {
            Y.log('defaulting railSize from max of computed style and configured '+dim+' attribute value', 'info', 'slider');
            // Default from height or width (axis respective), or dims assigned
            // via css to the rail or thumb, whichever is largest.
            // Dear implementers, please use railSize, not height/width to
            // set the rail dims
            size = this.get(dim);
            if (parseInt(size,10)) {
                setxy = true;
                rail.setStyle(dim,size);
                size = parseInt(rail.getComputedStyle(dim),10);
            }
            size = max(
                    size|0,
                    parseInt(thumb.getComputedStyle(dim),10),
                    parseInt(rail.getComputedStyle(dim),10));

            Y.log('pixel '+dim+' of rail: '+size+'px', 'info', 'slider');

            if (img && this._isImageLoaded(img)) {
                Y.log('using max of thumbImage '+dim+' ('+img.get(dim)+' and '+size+' for railSize', 'info', 'slider');

                size = max(img.get(dim),size);
            }
        }

        rail.setStyle(dim, size + PX);

        this._railSize = size;

        // handle the (not recommended) fallback case of setting rail size via
        // widget height/width params.  This is the only case that sets the
        // off-axis rail dim in the code.
        if (setxy) {
            dim = this._key.offAxisDim;
            size = this.get(dim);
            if (size) {
                rail.set(dim,size);
            }
        }
    },

    /**
     * Store the current XY position of the rail Node on the page.  For use in
     * calculating thumb position from value.
     *
     * @method _setRailOffsetXY
     * @protected
     */
    _setRailOffsetXY : function () {
        this._offsetXY = this.get(RAIL).getXY()[this._key.xyIndex] +
                         this.get(MIN_GUTTER);
    },

   /**
    * Passes the gutter attribute value to the DDConstrain gutter attribute.
    *
    * @method _setDDGutter
    * @protected
    */
    _setDDGutter : function () {
        var gutter = this._key.xyIndex ?
            this.get(MIN_GUTTER) + " 0 " + this.get(MAX_GUTTER) :
            "0 " + this.get(MAX_GUTTER) + " 0 " + this.get(MIN_GUTTER);

        Y.log('setting DDConstrain gutter "'+gutter+'"','info','slider');

        this._dd.con.set('gutter', gutter);
    },

    /**
     * Resets the cached region inside the DD constrain instance to support
     * repositioning the Slider after instantiation.
     *
     * @method _resetDDCacheRegion
     * @protected
     */
    _resetDDCacheRegion : function () {
        // Workaround for ticket #2527964
        this._dd.con._cacheRegion();
    },

    /**
     * Calculates the multiplier used to translate the value into a thumb
     * position.
     *
     * @method _setFactor
     * @protected
     */
    _setFactor : function () {
        var range = this._railSize - this._thumbSize -
                    this.get(MIN_GUTTER) - this.get(MAX_GUTTER);

        this._factor = this._railSize ?
            (this.get(MAX) - this.get(MIN)) / range :
            1;

        Y.log('_factor set to '+this._factor,'info','slider');
    },

    /**
     * Convenience method for accessing the current value of the Slider.
     * Equivalent to <code>slider.get(&quot;value&quot;)</code>.
     *
     * @method getValue
     * @return {Number} the value
     */
    getValue : function () {
        return this.get(VALUE);
    },

    /**
     * Convenience method for updating the current value of the Slider.
     * Equivalent to <code>slider.set(&quot;value&quot;,val)</code>.
     *
     * @method setValue
     * @param val {Number} the new value
     */
    setValue : function (val) {
        this.set(VALUE,val);
    },

    /**
     * Validator applied to new values for the axis attribute. Only
     * &quot;x&quot; and &quot;y&quot; are permitted.
     *
     * @method _validateNewAxis
     * @param v {String} proposed value for the axis attribute
     * @return Boolean
     * @protected
     */
    _validateNewAxis : function (v) {
        return isString(v) && 'xXyY'.indexOf(v.charAt(0)) > -1;
    },

    /**
     * Validator applied to the min attribute.
     *
     * @method _validateNewMin
     * @param v {MIXED} proposed value for the min attribute
     * @return Boolean
     * @protected
     */
    _validateNewMin : function (v) {
        return isNumber(v);
    },

    /**
     * Validator applied to the max attribute.
     *
     * @method _validateNewMax
     * @param v {MIXED} proposed value for the max attribute
     * @return Boolean
     * @protected
     */
    _validateNewMax : function (v) {
        return isNumber(v);
    },

    /**
     * Validator applied to the value attribute.
     *
     * @method _validateNewValue
     * @param v {MIXED} proposed value for the value attribute
     * @return Boolean
     * @protected
     */
    _validateNewValue : function (v) {
        var min    = this.get(MIN),
            max    = this.get(MAX);

        return isNumber(v) &&
                (min < max ? (v >= min && v <= max) : (v >= max && v <= min));
    },

    /**
     * Validator applied to the rail attribute. Rejects all values after the
     * Slider has been rendered.
     *
     * @method _validateNewRail
     * @param v {MIXED} proposed value for the rail attribute
     * @return Boolean
     * @protected
     */
    _validateNewRail : function (v) {
        return !this.get(RENDERED) || v;
    },

    /**
     * Validator applied to the thumb attribute.  Rejects all values after the
     * Slider has been rendered.
     *
     * @method _validateNewThumb
     * @param v {MIXED} proposed value for the thumb attribute
     * @return Boolean
     * @protected
     */
    _validateNewThumb : function (v) {
        return !this.get(RENDERED) || v;
    },

    /**
     * Validator applied to the thumbImage attribute.  Rejects all values after
     * the Slider has been rendered.
     *
     * @method _validateNewThumbImage
     * @param v {MIXED} proposed value for the thumbImage attribute
     * @return Boolean
     * @protected
     */
    _validateNewThumbImage : function (v) {
        return !this.get(RENDERED) || v;
    },

    /**
     * Validator applied to the railSize attribute. Only strings of css size
     * values (e.g. '200px') are allowed.
     *
     * @method _validateNewRailSize
     * @param v {String} proposed value for the railSize attribute
     * @return Boolean
     * @protected
     */
    _validateNewRailSize : function (v) {
        return isString(v) &&
            (v === '0' || /^\d+(?:p[xtc]|%|e[mx]|in|[mc]m)$/.test(v));
    },

    /**
     * Setter applied to the input when updating the axis attribute.
     *
     * @method _setAxisFn
     * @param v {String} proposed value for the axis attribute
     * @return {String} lowercased first character of the input string
     * @protected
     */
    _setAxisFn : function (v) {
        return v.charAt(0).toLowerCase();
    },

    /**
     * Setter applied to the input when updating the rail attribute.  Input can
     * be a Node, raw HTMLElement, or a selector string to locate it.
     *
     * @method _setRailFn
     * @param v {Node|String|HTMLElement} The rail element Node or selector
     * @return {Node} The Node if found.  Otherwise null.
     * @protected
     */
    _setRailFn : function (v) {
        return Y.get(v) || null;
    },

    /**
     * Setter applied to the input when updating the thumb attribute.  Input can
     * be a Node, raw HTMLElement, or a selector string to locate it.
     *
     * @method _setThumbFn
     * @param v {Node|String|HTMLElement} The thumb element Node or selector
     * @return {Node} The Node if found.  Otherwise null.
     * @protected
     */
    _setThumbFn : function (v) {
        return Y.get(v) || null;
    },

    /**
     * Setter applied to the input when updating the thumbImage attribute.
     * Input can be a Node, raw HTMLElement, selector string to locate it, or
     * the URL for an image resource.
     *
     * String input will be treated as a selector.  If no element is found using
     * the selector, an <code>img</code> Node will be created with the string
     * used as the <code>src</code> attribute.
     *
     * @method _setThumbImageFn
     * @param v {Node|String|HTMLElement} The thumbImage element Node, selector,
     *          or image URL
     * @return {Node} The Node if found or created.  Otherwise null.
     * @protected
     */
    _setThumbImageFn : function (v) {
        return v ? Y.get(v) ||
                Y.Node.create('<img src="'+v+'" alt="Slider thumb">') :
                null;
    },


    /**
     * Caches the current page position of the rail element and fires the
     * slideStart event in response to the DD's drag:start.
     *
     * @method _onDDStartDrag
     * @param e {Event} the DD instance's drag:start custom event
     * @protected
     */
    _onDDStartDrag : function (e) {
        Y.log('slide start','info','slider');
        this._setRailOffsetXY();
        this.fire(SLIDE_START,{ ddEvent: e });
    },

    /**
     * Fires the thumbDrag event to queue Slider value update.
     *
     * @method _onDDDrag
     * @param e {Event} the DD instance's drag:drag custom event
     * @protected
     */
    _onDDDrag : function (e) {
        Y.log('thumb drag','info','slider');
        this.fire(THUMB_DRAG, { ddEvent: e });
    },

    /**
     * The default value update behavior in response to Slider thumb
     * interaction.  Calculates the value using stored offsets, the _factor
     * multiplier and the min value.
     *
     * @method _defThumbDragFn
     * @param e {Event} the internal thumbDrag event
     * @protected
     */
    _defThumbDragFn : function (e) {
        var before = this.get(VALUE),
            val    = e.ddEvent[this._key.eventPageAxis] - this._offsetXY;

        Y.log('setting value from thumb drag: before('+before+') raw('+val+') factored('+this._convertOffsetToValue(val)+')', 'info','slider');

        val = this._convertOffsetToValue(val);

        if (before !== val) {
            this.set(VALUE, val, { ddEvent: e.ddEvent });
        }
    },

    /**
     * Fires the slideEnd event.
     *
     * @method _onDDEndDrag
     * @param e {Event} the DD instance's drag:end custom event
     * @protected
     */
    _onDDEndDrag : function (e) {
        Y.log('slide end','info','slider');
        this.fire(SLIDE_END,{ ddEvent: e });
    },




    /**
     * Calls _uiPositionThumb with the value of the custom event's
     * &quot;offset&quot; property.
     *
     * @method _defPositionThumbFn
     * @param e {Event} the positionThumb custom event
     * @protected
     */
    _defPositionThumbFn : function (e) {
        Y.log('setting thumb offset ('+e.offset+') from value attribute update ('+e.value+')', 'info', 'slider');

        this._uiPositionThumb(e.offset);
    },

    /**
     * Places the thumb at a particular X or Y location based on the configured
     * axis.
     *
     * @method _uiPositionThumb
     * @param xy {Number} the desired left or top pixel position of the thumb
     *           in relation to the rail Node.
     * @protected
     */
    _uiPositionThumb : function (xy) {
        var dd     = this._dd,
            thumb  = dd.get('dragNode'),
            hidden = thumb.ancestor(this._isDisplayNone);

        if (!hidden) {
            dd._setStartPosition(dd.get('dragNode').getXY());

            // stickX/stickY config on DD instance will negate off-axis move
            dd._alignNode([xy,xy],true);
        }
    },

    /**
     * Helper function to search up the ancestor axis looking for a node with
     * style display: none.  This is passed as a function to node.ancestor(..)
     * to test if a given node is in the displayed DOM and can get accurate
     * positioning information.
     *
     * @method _isDisplayNone
     * @param el {Node} ancestor node as the function walks up the parent axis
     * @return {Boolean} true if the node is styled with display: none
     * @protected
     */
    _isDisplayNone : function (node) {
        return node.getComputedStyle('display') === 'none';
    },

    /**
     * Fires the internal positionThumb event in response to a change in the
     * value attribute.
     *
     * @method _afterValueChange
     * @param e {Event} valueChange custom event
     * @protected
     */
    _afterValueChange : function (e) {
        if (!e.ddEvent) {
            var xy = this._convertValueToOffset(e.newVal);

            Y.log('firing positionThumb to position thumb', 'info', 'slider');

            this.fire(POSITION_THUMB,{ value: e.newVal, offset: xy });
        }
    },

    /**
     * Converts a value to a pixel offset for the thumb position on the rail.
     *
     * @method _convertValueToOffset
     * @param v {Number} value between the Slider's min and max
     * @protected
     */
    _convertValueToOffset : function (v) {
        return round((v - this.get(MIN)) / this._factor) + this._offsetXY;
    },

    /**
     * Converts a pixel offset of the thumb on the rail to a value.
     *
     * @method _convertOffsetToValue
     * @param v {Number} pixel offset of the thumb on the rail
     * @protected
     */
    _convertOffsetToValue : function (v) {
        return round(this.get(MIN) + (v * this._factor));
    },

    /**
     * Replaces the thumb Node in response to a change in the thumb attribute.
     * This only has effect after the Slider is rendered.
     *
     * @method _afterThumbChange
     * @param e {Event} thumbChange custom event
     * @protected
     */
    _afterThumbChange : function (e) {
        var thumb;

        if (this.get(RENDERED)) {
            if (e.prevValue) {
                e.prevValue.get('parentNode').removeChild(e.prevValue);
            }

            this._initThumb();
            
            thumb = this.get(THUMB);
            this._dd.set('node',thumb);
            this._dd.set('dragNode',thumb);

            this.syncUI();
        }
    },

    /**
     * Sets or replaces the thumb's contained <code>img</code> Node with the
     * new Node in response to a change in the thumbImage attribute.  This only
     * has effect after the Slider is rendered.
     *
     * @method _afterThumbImageChange
     * @param e {Event} thumbImageChange custom event
     * @protected
     */
    _afterThumbImageChange : function (e) {
        if (this.get(RENDERED)) {
            if (e.prevValue) {
                e.prevValue.get('parentNode').removeChild(e.prevValue);
            }

            this._initThumbImage();
            
            this.syncUI();
        }
    },

    /**
     * Updates the Slider UI in response to change in the min attribute.
     *
     * @method _afterMinChange
     * @param e {Event} minChange custom event
     * @protected
     */
    _afterMinChange : function (e) {
        this._refresh(e);
    },

    /**
     * Updates the Slider UI in response to change in the max attribute.
     *
     * @method _afterMaxChange
     * @param e {Event} maxChange custom event
     * @protected
     */
    _afterMaxChange : function (e) {
        this._refresh(e);
    },

    /**
     * Updates the Slider UI in response to change in the railSize attribute.
     *
     * @method _afterRailSizeChange
     * @param e {Event} railSizeChange custom event
     * @protected
     */
    _afterRailSizeChange : function (e) {
        this._refresh(e);
    },

    /**
     * Locks or unlocks the DD instance in response to a change in the disabled
     * attribute.
     *
     * @method _afterDisabledChange
     * @param e {Event} disabledChange custom event
     * @protected
     */
    _afterDisabledChange : function (e) {
        if (this._dd) {
            this._dd.set('lock',e.newVal);
        }
    },

    /**
     * Common handler to call syncUI in response to change events that occurred
     * after the Slider is rendered.
     *
     * @method _refresh
     * @param e {Event} An attribute change event
     * @protected
     */
    _refresh : function (e) {
        if (e.newVal !== e.prevVal && this.get(RENDERED)) {
            this.syncUI();
        }
    },

    /**
     * Used to determine if there is a current or pending request for the
     * thumbImage resource.
     *
     * @method _isImageLoading
     * @param img {Node} <code>img</code> Node
     * @return Boolean
     * @protected
     */
    _isImageLoading : function (img) {
        return img && !img.get(COMPLETE);
    },

    /**
     * Used to determine if the image resource loaded successfully or there was
     * an error.
     *
     * NOTES:
     * <ul>
     *    <li>img load error fired xbrowser for image resources not yet resolved</li>
     *    <li>img.complete reports false in IE for images not yet loaded as well as images that failed to load</li>
     *    <li>img.complete true && img.naturalWidth == 0 in FF and Safari indicate image failed to load</li>
     *    <li>img.complete && img.width == 0 in Opera indicates image failed to load</li>
     * </ul>
     *
     * @method _isImageLoaded
     * @param img {Node} <code>img</code> Node
     * @return Boolean
     * @protected
     */
    _isImageLoaded : function (img) {
        if (img) {
            var w = img.get('naturalWidth');
            return img.get(COMPLETE) && (!isNumber(w) ? img.get(WIDTH) : w);
        }

        return true;
    }

});

Y.Slider = Slider;

Copyright © 2009 Yahoo! Inc. All rights reserved.