Source: src/main/javascript/oracle/oj/ojslider/ojslider.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * Copyright (c) 2015, Oracle and/or its affiliates.
 * All rights reserved.
 */
/**
 * @preserve Copyright 2013 jQuery Foundation and other contributors
 * Released under the MIT license.
 * http://jquery.org/license
 */

(function() {

/*!
 * JET Slider @VERSION
 *
 *
 * Depends:
 *  jquery.ui.widget.js
 */

/**
 * @ojcomponent oj.ojSlider
 * @augments oj.editableValue
 * 
 * @classdesc
 * <h3 id="sliderOverview-section">
 *   JET Slider Component
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#sliderOverview-section"></a>
 * </h3>
 * <p>Description: The ojSlider component enhances an HTML <code class="prettyprint">input</code> element into an interactive slider.
 * </p>
 * The <code class="prettyprint">type</code> option is used to set the slider to either a single-thumb or a range slider.
 * Single thumb sliders are defined by setting the <code class="prettyprint">type</code> option to either <code class="prettyprint">"single"</code>, <code class="prettyprint">"fromMin"</code>, or <code class="prettyprint">"fromMax"</code>.
 * The <code class="prettyprint">type</code> option defaults to <code class="prettyprint">"fromMin"</code>, which will style the value bar from the minimum value to the slider thumb.
 * A range slider (a slider with two thumbs) is defined by setting <code class="prettyprint">type</code> to <code class="prettyprint">range"</code>. 
 * The value bar is styled between the thumbs for a range slider.
 * <ul>
 * <li> To create a single thumb slider:</li>
 * Either use the default <code class="prettyprint">type</code>, or set the <code class="prettyprint">type</code> option to either "single", "fromMin", or "fromMax".
 * Set the <code class="prettyprint">value</code> option to a number. The value should be between the <code class="prettyprint">min</code> and <code class="prettyprint">max</code> option values. 
 * <li> To create a range slider (slider with two thumbs):</li>
 * set the <code class="prettyprint">type</code> option to "range", 
 * and set the <code class="prettyprint">value</code> option to an array of two numbers.
 * The first array value must be less than or equal to the second array value, and both values must be between the <code class="prettyprint">min</code> and <code class="prettyprint">max</code> option values. 
 * </ul>
 * </p>
 * The step option of the slider specifies the interval between thumb stops. For example, if <code class="prettyprint">min</code>  is set to 0 and <code class="prettyprint">max</code>
 * is set to 10, a <code class="prettyprint">step</code> value of 2 would allow the thumb to be positioned at 0, 2, 4, 6, 8, and 10.
 * </p>
 * The <code class="prettyprint">orientation</code> option defaults to <code class="prettyprint">"horizontal"</code>.
 * Set <code class="prettyprint">orientation</code> to <code class="prettyprint">"vertical"</code> for a vertical slider (one where the thumb travels along the vertical axis).
 * </p>
 * Set the <code class="prettyprint">disabled</code> option <code class="prettyprint">true</code> to display a slider that displays a value but does not allow interaction.
 * </p>
 * For horizontal sliders, use the slider component's <code class="prettyprint">rootAttributes </code> 
 * to set the width. For vertical sliders, use <code class="prettyprint">style </code> attributes on the 
 * <code class="prettyprint">input </code> tag to set the slider height. These conventions are recommended in order
 * to achieve the best messaging format.
 * </p>
 * <h3 id="touch-section">
 *   Touch End User Information
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#touch-section"></a>
 * </h3>
 *
 * {@ojinclude "name":"touchDoc"}
 *
 * <h3 id="keyboard-section">
 *   Keyboard End User Information
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#keyboard-section"></a>
 * </h3>
 *
 * {@ojinclude "name":"keyboardDoc"}
 *
 * <h3 id="accessibility-section">
 *   Accessibility
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#accessibility-section"></a>
 * </h3>
 * <p>
 * The slider component is accessible - it sets and maintains the appropriate aria- attributes, 
 * including <code class="prettyprint">aria-valuenow</code>, <code class="prettyprint">aria-valuemax</code>,
 * <code class="prettyprint">aria-valuemin</code> and <code class="prettyprint">aria-orientation</code>.
 * <p>
 * It is up to the application developer to associate the label with the slider input component.
 * There are three ways to do this:
 * <ul>
 * <li>
 * Set an <code class="prettyprint">aria-labelledby</code> on the slider input to reference the label
 * <code class="prettyprint">id</code>.
 * </li>
 * <li>
 * Set an <code class="prettyprint">id</code> on the slider input, and also set 
 * the <code class="prettyprint">for</code> attribute on the label to be the input's id.
 * </li>
 * <li>
 * Set an <code class="prettyprint">aria-label</code> on the slider input. 
 * </li>
 * </ul>
 *
 * The slider component will then use this information in the input tag to create the appropriate aria attribute on the slider thumb(s).
</p>
In addition, the slider thumb element can be accessed using <code class="prettyprint">getNodeBySubId()</code> method.
This allows the user to (a) override any of the above aria attributes that were set automically on the thumb,
 * or (b) define additional aria attributes, such as <code class="prettyprint">aria-controls</code> 
 * or <code class="prettyprint">aria-valueText</code> .
 * <p> 
 * If the slider controls another element that is in a remote area of the page,
 * then the <code class="prettyprint">aria-controls</code> attribute for the slider thumb should be set.
 * This can also be accomplished by accessing the slider thumb element using <code class="prettyprint">getNodeBySubID()</code>.
 * 
 * <p> 
 * For example, suppose there is another component, that is in a remote area of the page that controlled by the slider.
 * Assume that the <code class="prettyprint">id</code>  of the remote element is "idOfRemoteElement".
 * Below we use the <code class="prettyprint">getNodeBySubId()</code> method to access the thumb element in order to set the 
 * <code class="prettyprint">aria-controls</code> attribute of the thumb to point to the the id ("idOfRemoteElement") of the remote html element:
 * 
 * <pre class="prettyprint">
 * <code>
 *  $(document).ready(function() {
 *     ko.applyBindings(...)
 *     var thumb0 = $('#inputslider-id').ojSlider("getNodeBySubId", {subId:"oj-slider-thumb-0"});
 *     $(thumb0).attr("aria-controls", "idOfRemoteElement");
 *  });
 * </code></pre>
 * 
 * <!-- - - - - Above this point, the tags are for the class.
 *              Below this point, the tags are for the constructor (initializer). - - - - - - -->
 * @desc Creates an ojSlider component
 * 
 * @param {Object=} options a map of option-value pairs to set on the component
 * 
 * @example <caption>Initialize component using widget API</caption>
 * <input id="spin" type="text"/><br/>
 * $("#slider").ojSlider({'value': 10, 'max':100, 'min':0, 'step':2});
 * $("#slider").ojSlider({'option', 'value', 10});
 * @example <caption>Using knockout, value, min, max bind to observables - salary, salaryMax, salaryMin</caption> 
 * <input id="foo" data-bind="ojComponent: 
 *   {component: 'ojSlider', value: salary, min:salaryMin, max:salaryMax, step:5}"/>
 */
    oj.__registerWidget("oj.ojSlider", $['oj']['editableValue'], {
        defaultElement: "<input>",
        version: "1.1.1",
        widgetEventPrefix : "oj", 

        options: {

            /** @private 
              * @type {?number}
            */
            distance: 0,
            /** 
              * The maximum value of the slider. 
              * The <code class="prettyprint">max</code> must not be less than the 
              * <code class="prettyprint">min</code>, or else an Error is thrown during initialization.
              * @expose 
              * @memberof oj.ojSlider
              * @instance
              * @type {?number}
              * @default <code class="prettyprint">null</code>
              * @example <caption>Initialize the slider with the 
              * <code class="prettyprint">max</code> option specified:</caption>
              * $( ".selector" ).ojSlider( { "max": 100 } );
              * @example <caption>Set the <code class="prettyprint">max</code> to a number inside 
              * of quotes; this is valid and will be coerced to a number,
              * -100 in this example:</caption>
              * $( ".selector" ).ojSlider( { "max": "-100" } );
              */
            max: 100,
            /** 
              * The minimum value of the slider. 
              * The <code class="prettyprint">min</code> must not be greater than the 
              * <code class="prettyprint">max</code>, or else an Error is thrown during initialization.
              * @expose 
              * @memberof oj.ojSlider
              * @instance
              * @type {?number}
              * @default <code class="prettyprint">null</code>
              * @example <caption>Initialize the slider with the 
              * <code class="prettyprint">max</code> option specified:</caption>
              * $( ".selector" ).ojSlider( { "max": 100 } );
              * @example <caption>Set the <code class="prettyprint">max</code> to a number inside 
              * of quotes; this is valid and will be coerced to a number,
              * -100 in this example:</caption>
              * $( ".selector" ).ojSlider( { "max": "-100" } );
              */
            min: 0,
            /** 
              * Specify the orientation of the slider.
              *
              * @expose 
              * @memberof! oj.ojSlider
              * @instance
              * @type {string}
              * @ojvalue {string} "horizontal" Orient the slider horizontally.
              * @ojvalue {string} "vertical" Orient the slider vertically.
              * @default <code class="prettyprint">"horizontal"</code>
              *
              * @example <caption>Initialize the slider with the 
              * <code class="prettyprint">orientation</code> option specified:</caption>
              * $( ".selector" ).ojSlider( { "orientation": "vertical" } );
              * 
              * @example <caption>Get or set the <code class="prettyprint">orientation</code> 
              * option after initialization:</caption>
              * // getter
              * var orientation = $( ".selector" ).ojSlider( "option", "orientation" );
              * 
              * // setter
              * $( ".selector" ).ojSlider( "option", "orientation", "vertical" );
              */
            orientation: "horizontal",
            
            /**
              * readOnly is private - more UX design is necessary to support readonly across components.
              * Whether the component is readOnly. The element's <code class="prettyprint">readOnly</code>
              *  property is used as its initial value if it exists, when the option is not explicitly set. When neither is set, 
              * <code class="prettyprint">readOnly </code>
              * defaults to false.
              * 
              * @example <caption>Initialize component with <code class="prettyprint">readOnly</code> option:</caption>
              * $(".selector").ojSlider({"readOnly": true});
              * 
              * @private
              * @type {?boolean}
              * @default <code class="prettyprint">false</code>
              * @instance
              * @memberof oj.ojSlider
              */
            readOnly: false,

            
            /** 
             * Whether the component is disabled. The element's <code class="prettyprint">disabled</code>
             *  property is used as its initial 
             * value if it exists, when the option is not explicitly set. When neither is set, 
             * <code class="prettyprint">disabled </code>
             * defaults to false.
             * 
             * @example <caption>Initialize component with <code class="prettyprint">disabled</code> option:</caption>
             * $(".selector").ojSlider({"disabled": true});
             * 
             * @expose 
             * @type {?boolean}
             * @default <code class="prettyprint">false</code>
             * @instance
             * @memberof oj.ojSlider
             */
            disabled: false,

            /** 
             * Determines the size or amount of each interval or step the slider takes 
             * between min and max.
             * The full specified value of the range (max - min) should be divisible by step.
             *
             * @expose 
             * @instance
             * @type {?number}
             * @default <code class="prettyprint">1</code>
             * @memberof oj.ojSlider 
             * @example <caption>Initialize the slider with the 
             * <code class="prettyprint">step</code> option specified:</caption>
             * $( ".selector" ).ojSlider( { "step": 10 } );
             * @example <caption>Set the <code class="prettyprint">step</code> to a number inside 
             * of quotes; this is valid and will be coerced to a number,
             * 10 in this example:</caption>
             * $( ".selector" ).ojSlider( { "step": "10" } );
             **/

            step: 1,

            /**
              * The slider type determines whether the slider has one thumb or two thumbs, and how the value is represented in the UI.
              * <p> Note that for the use case of dynamically switching between a range slider and a single-thumb slider,
              * it is best to set the <code class="prettyprint">type</code> option before setting the <code class="prettyprint">value</code> option. 
              * This avoids the loss of the second thumb's value (during value coersion) when switching from a single-thumb slider to a range slider (when the value would be set as an array on a single-thumb slider).
              *
              * @expose
              * @type {?string}
              * @ojvalue {string} "fromMin" A single-thumb slider where the value bar goes from the slider min to the the slider thumb.
              * @ojvalue {string} "fromMax" A single-thumb slider where the value bar goes from the slider thumb to the the slider max.
              * @ojvalue {string} "range" A slider with two thumbs, where the value bar goes between the slider thumbs.
              * @ojvalue {string} "single" A single-thumb slider where the value bar has no additional styling.
              * @default <code class="prettyprint">"fromMin"</code>
              * @instance
              * @memberof oj.ojSlider
              * 
              * @example <caption>Initialize component with <code class="prettyprint">type</code> option set to "range":</caption>
              * $(".selector").ojSlider({"type": "range"});
              * 
              */

            type: "fromMin",
            
            /** 
             * The value of the component. Value can be a number or an array. 
             * When <code class="prettyprint">type</code> is equal to "range" the <code class="prettyprint">value</code> should be an array of two numbers,
             * while any other <code class="prettyprint">type</code> should have <code class="prettyprint">value</code> defined as a number.
             * 
             * <p> Note that the <code class="prettyprint">value</code> option should
             * be compatible with the <code class="prettyprint">type</code> option, as described above.
             * A value that is not compatible with the type will be coerced into a compatible value. 
             * For example, it <code class="prettyprint">type</code> is set to "range" 
             * and the value is not an array of two numbers, then the value will be
             * automatically coerced into an array of two numbers.
             *
             * <p> Also note that for the use case of dynamically switching between a single-thumb slider and a range slider,
             * you should set the <code class="prettyprint">type</code> option before setting the <code class="prettyprint">value</code> option.
             * This avoids the loss of the second thumb's value (during value coersion) when switching from a single-thumb slider to a range slider (when the value would be set as an array on a single-thumb slider).
             *
             * @example <caption>Initialize the component with the <code class="prettyprint">value</code> option specified:</caption>
             * $(".selector").ojSlider({'value': 55});<br/>
             * @example <caption>Get or set <code class="prettyprint">value</code> option, after initialization:</caption>
             * // Getter: returns 55
             * $(".selector").ojSlider("option", "value");
             * // Setter: sets 20
             * $(".selector").ojSlider("option", "value", 20);
             * @example <caption>Set the <code class="prettyprint">value</code> to a number inside 
             * of quotes; this is valid and will be coerced to a number,
             * 10 in this example:</caption>
             * $( ".selector" ).ojSlider( { "value": "10" } );
             * @expose 
             * @access public
             * @instance
             * @default <code class="prettyprint">0</code>
             * @memberof oj.ojSlider
             * @type {?number|Array.<Number>}
             */
            value: 0
        },

        //
        // SLIDER_MIN and SLIDER_MAX are used as default aria-labelledby values on range slider thumbs.
        //
        /*
          _BUNDLE_KEY:
          {
              _SLIDER_MIN_:  'minimum',
              _SLIDER_MAX:   'maxiumum'    
          },
        */
        // 
        // Later on, we can do the following:
        // 
        // sliderMinimum = this.getTranslatedString(this._BUNDLE_KEY._SLIDER_MIN); 
        // sliderMaximum = this.getTranslatedString(this._BUNDLE_KEY._SLIDER_MAX); 
        //

        // number of pages in a slider
        // (how many times can you page up/down to go through the whole range)
        _numPages: 5,
        _isRTL: function() {
            return oj.DomUtils.getReadingDirection() === "rtl";
        },

        /*
        _getRootAttributesLocator: function() {
            return this.sliderWrapper;
        },            
*/

        _ComponentCreate : function () {

            this._super();

            this._newMultiValue = new Array();
            var node = this.element;

            // input type=number does not support the 'pattern' attribute, so
            // neither should ojInputNumber.
            // remove this before EditableValue grabs it and uses it.
            // node.removeAttr("pattern");
            
            node.removeAttr("pattern");

            this._keySliding = false;
            this._dragInProgress = false;

            this._thumbIndex = null;

            // ojSlider could support both <div> and <input> tags
            this._inputtag = false;
            if (this.element.is("INPUT")) 
            {
                this._inputtag = true;

                // Save the input tag style, apply to sliderWrapper later in the code.
                if (this.element[0].style) 
                    this._styleFromInputTag = this.element[0].style.cssText;

                // todo - test to ensure that this is put back on destory.
                this._inputElementOriginalDisplay = this.element.css("display");
                this.element.css("display", "none");

                // We will need the inputDiv later in order to destroy 
                var inputDiv = $(this.element).wrap('<div> </div>'); // @HTMLUpdateOK
                
                // _elementWrapped is the new wrapped input element.
                this._elementWrapped = inputDiv.parent();

            }
            else this._elementWrapped = this.element;

            var classes = "oj-slider ";

            if (this._isVertical()) 
                classes += "oj-slider-vertical";
            else
                classes += "oj-slider-horizontal";
        
            classes += " ui-widget ui-widget-content";

            this._elementWrapped.addClass(classes);

            // if (Array.isArray(this.options.value))
            if (this.options.type === "range")
                this._multipleThumbs = true;
            else this._multipleThumbs = false;

            this._calculateNewMax();

            this._setup();
	    this._setupEvents();

        },

        _AfterCreate : function ()
        {
            this._super();

	    this._makeDraggable();

            var label = this._GetLabelElementLocal();

            // this.$label = this._GetLabelElementLocal();

            //
            // Copy any labelled-by on the <input labelled-by="id"> to the slider thumb.
            //
            if (label) {

                // this id shold be on the thumb: aria-labelledby =

                // 
                // todo: verify that this works properly for range slider
                // 
                var thumb = this._elementWrapped.find('.oj-slider-thumb');
                // Set the aria-labelledby attribute of the thumb to the returned id.
                var labelId = label.attr("id");
                if (!labelId)
                    labelId = label.attr("for");

                thumb.attr('aria-labelledby', labelId);

                if (label.length > 1 && thumb.length > 1)  {

                    /*
                    var label2 = label[1];
                    var secondLabelId = $(label2).attr("id");
                    */

                    var thumb2 = thumb[1];
                    thumb2.attr('aria-labelledby',  String(labelId));
                }

            }
            else {

                //
                // Check if the <input> has aria-label=""
                //

                var ariaLabelString = this.element.attr("aria-label");
                if (ariaLabelString) {
                    // console.log("aria-label " + ariaLabelString);

                    var thumb = this._elementWrapped.find('.oj-slider-thumb');

                    // Set the aria-labelledby attribute of the thumb to the returned id
                    thumb.attr('aria-label',  ariaLabelString);

                }
            }
        },

        _GetLabelElementLocal : function ()
        {
            // If <input> has aria-labelledby set, then look for label it is referring to.
            var queryResult = this._getAriaLabelledByElementLocal();
            if (queryResult !== null && queryResult.length !== 0)
            {
                // console.log("found labelledby");
                // console.log("label id is " + queryResult.attr("id"));
                return queryResult;
            }
            
            queryResult = this._getAriaLabelForElementLocal();
            if (queryResult !== null && queryResult.length !== 0)
                return queryResult;

        },

        _getAriaLabelForElementLocal: function()
        {

            var id = this.element.prop("id");
            if (id !== undefined)
            {

                var labelQuery = "label[for='" + id + "']";

                // console.log("label id is " + $(labelQuery).attr("id"));

                var jqLabelQuery = $(labelQuery);
                if (jqLabelQuery.length > 0) return jqLabelQuery;

                var spanQuery = "span[for='" + id + "']";

                // console.log("span id is " + $(spanQuery).attr("id"));

                if ($(spanQuery).length !== 0)
                    return $(spanQuery);
            }
            return null;

        },

        _getAriaLabelledByElementLocal: function()
        {
            // look for a label with an id equal to the value of aria-labelledby. 
            // .prop does not work for aria-labelledby. Need to use .attr to find
            // aria-labelledby.
            var ariaId = this.element.attr("aria-labelledby");

            if (ariaId !== undefined )
            {

                var labelQuery = "label[id='" + ariaId + "']"; 
                var jqLabelQuery = $(labelQuery);
                if (jqLabelQuery.length > 0) return jqLabelQuery;

                var spanQuery = "span[id='" + ariaId + "']"; 
                var jqSpanQuery = $(spanQuery);
                if (jqSpanQuery.length > 0) return jqSpanQuery;

            }    
            else
                return null;
        },

        widget: function() {
            return this._elementWrapped;
        },

        _setup: function() {
            this._createSliderContainer();
            this._createBarBackground();
            this._createRange();
            this._createThumbs();
            this._refreshValue();
        },

        _getElementId: function() {
            return (this.element[0].id);
        },

        //
        // Return the id of the slider thumb at index.
        //
        _getThumbId: function(index) {

            var elementId = this._getElementId();
            var thumbId = elementId + "-thumb" + index;
            return thumbId;
        },

        // 
        // Return the id of the slider bar value.
        // 
        _getBarValueId: function(index) {

            var elementId = this._getElementId();
            var thumbId = elementId + "-barValue";
            return thumbId;
        },

        // 
        // Return the id of the slider bar.
        // 
        _getBarBackgroundId: function(index) {

            var elementId = this._getElementId();
            var thumbId = elementId + "-barBack";
            return thumbId;
        },

        // 
        // Return the id of the slider bar.
        // 
        _getSliderWrapperId: function(index) {

            var elementId = this._getElementId();
            var sliderWrapperId = elementId + "-sliderWrapper";
            return sliderWrapperId;
        },

        _createThumbs: function() {

            var i, thumbCount,
            ariaNow = "aria-valuenow = '" + this._valueMin() + "' ",
            ariaMin = "aria-valuemin = '" + this._valueMin() + "' ",
            ariaMax = "aria-valuemax = '" + this._valueMax() + "' ",
            options = this.options,
            orientationClass,
            thumb = "",
            thumbSpanStart = "<span ", 
            thumbClasses = "class='oj-slider-thumb ui-state-default' tabindex='0' role='slider'" + ariaMin + ariaMax + "></span>",
            thumbs = [];

            if (this._multipleThumbs) 
                thumbCount = this.options.value.length;
            else thumbCount = 1;

            // 
            // Assign each thumb a unique id based on the elementId and the thumb number.
            // 
            for (i=0; i < thumbCount; i++) {
                var thumbId = "id='" + this._getThumbId(i) + "' ";
                thumb = thumbSpanStart + thumbId + thumbClasses;
                thumbs.push( thumb );
            }

            this._thumbs = $(thumbs.join( "" )).appendTo(this._sliderContainer);  // @HTMLUpdateOK
            this._thumb = this._thumbs.eq(0);

            var that = this;
            this._thumbs.each(function( i ) {
                $( this ).data( "oj-slider-thumb-index", i );

                if (that._isVertical() ) 
                    $(this).attr('aria-orientation', "vertical");

                if (that.options.disabled) {
                    $(this).attr('aria-disabled', "true");
                    $(this).removeAttr("tabindex")
                } else {
                    $(this).removeAttr('aria-disabled');
                }
                
                // To support read only, we place set title = "read only" on the thumb.
                if (that.options.readOnly) {
                    $(this).attr('title', "read only");
                    // $(this).removeAttr("tabindex")
                } else 
                    $(this).removeAttr('title');
                
            });
        },

        // 
        // Create a containing div to group all component generated content.
        // This is used in messaging, so that we can apply margins/padding 
        // between the inline message div and the pixels that make up the slider.
        // 
        _createSliderContainer: function() {

            var sliderWrapperId = this._getSliderWrapperId();
            var existingSliderWrapper = this._elementWrapped.find('#' + sliderWrapperId);

            if (existingSliderWrapper.length) existingSliderWrapper.remove();

            this._sliderContainer = $("<div></div>");
            $(this._sliderContainer).attr('id', sliderWrapperId);
            this._sliderContainer.addClass("oj-slider-container");

            this.element.after(this._sliderContainer); // @HTMLUpdateOK

            // copy all style properties from the input tag to the enclosing slider div (sliderWrapper) 
            this._sliderContainer[0].style.cssText = this._styleFromInputTag;
            // 
            // If the height is not set on a vertical slider,
            // Apply a 150px default height.
            // 
            if (this._isVertical() && this._sliderContainer[0].style.height == "") {
                this._sliderContainer[0].style.height = "150px";
            }

        },

        _createBarBackground: function() {

            var barId = this._getBarBackgroundId();

            var existingBarBack = this._elementWrapped.find('#' + barId);

            if (existingBarBack.length) existingBarBack.remove();

            this._barback = $("<div></div>");

            var classes = "oj-slider-bar";
            
            if (this._isVertical()) 
                classes += " oj-slider-vertical";
            else
                classes += " oj-slider-horizontal";
        
            $(this._barback).attr('id', barId);
            this._barback.addClass(classes)

            // Place the background bar element immediately after the hidden input tab.
            this._sliderContainer.append(this._barback);  // @HTMLUpdateOK

            //
            // Clicking on the bar repositions the thumb.
            //
            var that = this;
            this._barback.on("mousedown" + that.eventNamespace, function( event ) {
                that._initDragging(event, false);
                that._mouseStop(event);
                that._refreshContainment();

                // that._closestThumb.addClass("ui-state-active").focus();
                // that._closestThumb.focus();

                var thumb = that._getActiveThumb();
                thumb.addClass("ui-state-active").focus();

            });

        },

        _createRange: function() {
            var options = this.options,
            classes = "";

            if ( options.type ) {

                if (!this.options.value) {

                    if (this._multipleThumbs) 
                        this.options.value = [this._valueMin(), this._valueMax()];
                    else 
                        this.options.value = this._valueMin();

                    this.option('value',
                                this.options.value,
                                {'_context': {writeback: true, internalSet: true}});

                } else if (this._multipleThumbs) {

                    // This is the expected, common case
                    if (this.options.value.length === 2) 
                        this.options.value = this.options.value.slice(0);
                    else {
                        // Coerse to an array here.
                        this.options.value = [this.options.value[0], this._valueMax()];
                        this.option('value',
                                    this.options.value,
                                    {'_context': {writeback: true, internalSet: true}});
                    }

                }

                if ( !this._range || !this._range.length ) {
                    this._range = $("<div></div>");
                    // Give the bar an id.
                    $(this._range).attr('id', this._getBarValueId());

                    // this._range = $("<div></div>").appendTo(this._sliderContainer);
                    this._sliderContainer.append(this._range);  // @HTMLUpdateOK
                    classes = "oj-slider-range oj-slider-bar-value";

                    //
                    // Like the bar background, clicking on the bar value also repositions the thumb.
                    //
                    var that = this;
                    this._range.on("mousedown" + that.eventNamespace, function( event ) {
                        that._initDragging(event, false);
                        that._mouseStop(event);
                        that._refreshContainment();
                        
                        var thumb = that._getActiveThumb();
                        thumb.addClass("ui-state-active").focus();

                    });

                } else {
                    this._range.removeClass( "oj-slider-range-min oj-slider-range-max" )
                    // Thumb range switching from true to min/max
                        .css({
                            "left": "",
                            "top": ""
                        });
                }

                this._range = this._sliderContainer.find('#' + this._getBarValueId());

                var newClass = "";
                if (options.type === "fromMin") newClass = " oj-slider-range-min";
                else if (options.type === "fromMax") newClass = " oj-slider-range-max";

                this._range.addClass( classes + newClass);
            } else {
                if ( this._range ) {
                    this._range.remove();
                }
                this._range = null;
            }
        },

        _setupTouch : function (e)
        {
            this._touchProxy = oj._TouchProxy.addTouchListeners(e);
        },

        _tearDownTouch : function (e)
        {
            oj._TouchProxy.removeTouchListeners(e);
        },

	/**
	 * Setup events for slider.
	 * 
	 * @protected
	 * @memberof oj.ojSlider
	 * @instance
	 */
        _setupEvents: function() {

            this._thumbs.toArray().forEach (

                function(current, i) {
                    var thumb = $(current);

                    // setup keyboard events on each thumb.
                    this._UnregisterChildNode(thumb);
                    this._on(thumb, this._thumbEvents);

                    // setup touch events on each thumb
                    this._setupTouch(thumb);

                    this._hoverable(thumb);
                    this._focusable(thumb);
                },
                this
            );
        },

        /**
          * Override of protected base class method.  
          * Method name needn't be quoted since is in externs.js.
          * @protected
          * @memberof oj.ojSlider
          * @instance
          */
        _destroy: function() {

            // Tear down touch events for each thumb.
            this._thumbs.toArray().forEach (
                function(current, i) {
                    var thumb = $(current);
                    this._tearDownTouch(thumb);
                },
                this
            );

            this._sliderContainer.remove();
            oj.DomUtils.unwrap(this.element, this._elementWrapped); // @HTMLUpdateOK
            this.element.css("display", this._inputElementOriginalDisplay);

	    this._RestoreAttributes(this.element);		
	    return this._super();

        },

        // 
        // Called on drag start.
        // Setup initial positions, distance.
        // Set the mouseSliding flag to true.
        // Define closestThumb, used later in drag handler.
        // 
        // Do not process mouse events if the slider is disabled (or readOnly).
        // 
        // The parameter fromThumb, if true, uses the thumb position,
        // otherwise we use the mouse position.
        // The mouse position is used for bar clicks,
        // while the thumb position is used when dragging the thumb.
        // 
        _initDragging: function(event, fromThumb) {

            var position, normValue, distance, index, allowed, offset, mouseOverThumb,
            that = this,
            o = this.options;

            this._closestThumb = this._thumb;

            if ( o.disabled ) return false;
            if ( o.readOnly ) return false;

            if (!fromThumb || this._multipleThumbs) {
                position = { x: event.pageX, y: event.pageY };
                normValue = this._getNormValueFromMouse(position);
            }
            else {
                normValue = this._getNormValueFromThumb();
            }

            distance = this._valueMax() - this._valueMin() + 1;

            if (this._multipleThumbs) {
                this._thumbs.each(function( i ) {
                    var thisDistance = Math.abs( normValue - that._getMultiValues(i) );
                    if (( distance > thisDistance ) ||
                        ( distance === thisDistance &&
                          (i === that._lastChangedValueIndex || that._getMultiValues(i) === o.min ))) {
                        distance = thisDistance;
                        this._closestThumb = $( this );
                        index = i;
                    } 
                });
            }

            this._dragInProgress = true;
            this._thumbIndex = index;
            if (!this._closestThumb) return;

            if ( !this._thumbs.hasClass( "ui-state-hover" ) ) {
                // console.log("call slide");
                this._slide( event, index, normValue );
            }

            var thumb = this._getActiveThumb();
            thumb.addClass("ui-state-active").focus();

            return true;
        },

        _mouseStart: function() {

            return true;
        },

        _mouseDrag: function( event ) {

            // console.log("mouseDrag " + normValue + " " + position.x + " " + position.y);
            // Mirror the mouse drag with a pct change.

            var pct = this._getFracFromThumb() * 100;
            // console.log("pct " + pct);

            if (this._multipleThumbs) this._setRangeMultiThumb(pct, this._thumbIndex);
            else this._setRange(pct);

            return false;
        },

        _mouseStop: function( event ) {

            this._thumbs.removeClass( "ui-state-active" );
            this._dragInProgress = false;

            var normValue = this._getNormValueFromThumb();
            this._slide( event, this._thumbIndex, normValue );
            // _change is needed for click positioning
            this._change( event, this._thumbIndex );

            this._thumbIndex = null;

            return false;
        },

        _isVertical: function() {
            return (this.options.orientation === "vertical");
        },
        // 
        // Adjust the fraction for bounds limits and orientation.
        // 
        _getOrientationAdjustedFrac: function(frac) {

            if ( frac > 1 ) {
                frac = 1;
            }
            if ( frac < 0 ) {
                frac = 0;
            }
            if ( this._isVertical() ) {
                frac = 1 - frac;
            }

            return frac;
        },

        // 
        // Return a normalized value (trimmed to step increments)
        // based on the passed mouse coordinates.
        //
        _getNormValueFromMouse: function(position) {

            var pixelTotal,
            pixelMouse,
            valueTotal,
            valueMouse;

            var fracMouse = this._getFracFromMouse(position);

            valueTotal = this._valueMax() - this._valueMin();

            if (this._isRTL() && !this._isVertical())
                fracMouse = 1 - fracMouse;

            valueMouse = this._valueMin() + fracMouse * valueTotal;

            return this._trimAlignValue( valueMouse );
        },

        // 
        // Return the fraction (between 0 and 1)
        // that represents the bar value.
        // This is based on the mouse position parameter.
        // 
        _getFracFromMouse: function(position) {

            var pixelTotal, pixelMouse, fracMouse;

            if (!this._isVertical()) {
                pixelTotal = this._barback.width();
                pixelMouse = position.x - this._barback.offset().left;
            } else {
                pixelTotal = this._barback.height();
                pixelMouse = position.y - this._barback.offset().top;
            }

            if (pixelTotal == 0) return 1;

            fracMouse = ( pixelMouse / pixelTotal );
            fracMouse = this._getOrientationAdjustedFrac(fracMouse);

            return fracMouse;

        },


        // Return the active thumb
        _getActiveThumb: function() {
            if (this._multipleThumbs) 
                return($(this._thumbs[this._thumbIndex]));
            else 
                return(this._thumb);
        },

        // 
        // Return the fraction (between 0 and 1)
        // that represents the bar value.
        // This is based on the current position of the thumb.
        // 
        _getFracFromThumb: function() {

            var pixelTotal, pixelMouse, fracThumb;

            var thumb = this._getActiveThumb();

            var pos;

            if (!this._isVertical()) {
                var halfThumbWidth = thumb.outerWidth()/2;
                pos = thumb.offset().left + halfThumbWidth;
                pixelTotal = this._barback.width();
                pixelMouse = pos - this._barback.offset().left;
            } else {
                var halfThumbHeight = thumb.outerHeight()/2;
                pos = thumb.offset().top + halfThumbHeight;
                pixelTotal = this._barback.height();
                pixelMouse = pos - this._barback.offset().top;
            }

            if (pixelTotal == 0) return 1;

            fracThumb = ( pixelMouse / pixelTotal );

            // console.log("pos " + pos + " pctMouse " + fracThumb);

            fracThumb = this._getOrientationAdjustedFrac(fracThumb);

            return fracThumb;

        },


        _getNormValueFromThumb: function() {
            var fracThumb,
            valueTotal,
            valueMouse;

            fracThumb = this._getFracFromThumb();

            // console.log("_getNormValueFromThumb percent: " + fracThumb);

            valueTotal = this._valueMax() - this._valueMin();

            if (this._isRTL() && !this._isVertical())
                fracThumb = 1 - fracThumb;

            valueMouse = this._valueMin() + fracThumb * valueTotal;

            var trimmedValue =  this._trimAlignValue( valueMouse );
            // console.log("_getNormValueFromThumb valueMouse: " + valueMouse + " " + trimmedValue);

            return trimmedValue;
        },

        _slide: function( event, index, newVal ) {
            
            // console.log("_slide: options.value " + this.options.value + " newVal " + newVal);

            var otherVal,
            allowed;

            if (this._multipleThumbs) {

                otherVal = this._getMultiValues( index ? 0 : 1 );

                if ( ( this.options.value.length === 2 ) &&
                     ( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) )
                   ) {
                    newVal = otherVal;
                }

                if ( newVal !== this._getMultiValues(index) ) {
                    // console.log("slide " + index + " " + newVal);
                    this._setMultiValue(event, index, newVal);
                }
            } else {
                // This case handles a single value
                // console.log("_slide: single value ");
                if ( newVal !== this._getSingleValue() ) {

                    //console.log("_slide: setSingleValue ");

                    // sets slider thumb value
                    this._setSingleValue(event, newVal);

                    if (this._inputtag) {
                        this.element.val(newVal);
                    }
                }
            }
        },

        _change: function( event, index ) {

            // console.log("_change from " + this.options.value + " to newValue " + this._newValue);

            // Plan for 1.2: (once _SetRawValue() is implemented) 
            //    call _SetRawValue when we are sliding.
            //    (comment out the outside condition, conditionalize the SetValue)
            //

            if ( !this._keySliding && !this._dragInProgress ) 
            {
                if (this._multipleThumbs) {
                    // store the last change values for creating draggable containment
                    this._lastChangedValues = this._getNewValues(index, this._newMultiValue[index]);
                    this._SetValue(this._lastChangedValues, event); 
                }
                else {
                    this._SetValue(this._newValue, event); 
                }

                //store the last changed value index for reference when thumbs overlap
                this._lastChangedValueIndex = index;
                
            }
        },
        
        // 
        // Return the value of the slider.
        // For a single thumbbed slider, return a number.
        // For a two thumbbed slider, return an array.
        // 
        _getSingleValue: function() {

            if ( !this._keySliding && !this._dragInProgress ) {
                return this._getValueAligned();
            } else {
                if (!isNaN(this._newValue)) return this._newValue;
                return this._getValueAligned();

            }
        },

        // 
        // Set the slider value.
        // Pass on the event to _change.
        // 
        _setSingleValue: function(event, newValue ) {

            this._newValue = this._trimAlignValue( newValue );
            // console.log("setSingleValue " + newValue + " -> " + this._newValue);

            this._change(event, 0 );
            // console.log("setSingleValue options.value = " + this.options.value)
            this._refreshValue();

            return;

        },

        // 
        // Return the value for the specified thumb.
        // 
        _getMultiValues: function(index) {

            // 
            // Parameter index is supplied.
            // Return the value for the specified thumb.
            // 
            if (this._multipleThumbs) {

                if (!this._keySliding && !this._dragInProgress ) {
                    return this._getValuesAligned(index);
                } else {
                    // Return the raw value if we are sliding the thumb.
                    // This is needed to have the thumb track the mouse properly.
                    if (index === this._thumbIndex) {
                        if (!isNaN(this._newMultiValue[index]))
                            return this._newMultiValue[index];
                    }
                    return this._getValuesAligned(index);
                }
            } else {
                return this._getSingleValue();   
            }

        },

        //
        // Internal setter for slider values.
        // Sets the value for the specifed thumb.
        // (index 0 is used for a single thumb, 
        // for a range slider index 0 and index 1 are used).
        //
        _setMultiValue: function(event, index, newValue ) {

            this._newMultiValue[index] = this._trimAlignValue( newValue );
            // console.log(index + " _setMultiValue " + this._newValue);
            this._change(event, index );
            this._refreshValue();
            return;

        },

        _setOption: function( key, value, flags ) {

            var i;
            var coercedValue;

            if (key === "value")
            {
                if (Array.isArray(value)) {
                    this._multipleThumbs = true;
                    coercedValue = value;
                }
                else {
                    this._multipleThumbs = false;
                    coercedValue = this._parse(key, value);
                }
            }

            if (key === "max" || key === "min")
            {
                coercedValue = this._parse(key, value);
            }
            else if (key === "step")
            {
                coercedValue = this._parseStep(value);    
            }
            else
                coercedValue = value;
            
            // Note - this sets aria-disabled="false" on the top level element.
            // For sliders, we do not set disabled on the top level div.
            if ( key != "disabled" ) 
                this._super( key, coercedValue, flags );

            // when a dom element supports readonly, use that, and not aria-readonly.
            // having both is an error

            // Note - for now, readOnly is private.
            if (key === "readOnly")
            {
                this._elementWrapped.toggleClass( "oj-read-only", !!coercedValue );
                this._setup();
            }
            if (key === "disabled") {
	        this._disableDraggable();
                this._elementWrapped.toggleClass( "oj-disabled", !!coercedValue );
                this.options.disabled = coercedValue;
                this._setup();
            }

            switch ( key ) {
            case "orientation":

                this._elementWrapped.removeClass("oj-slider-horizontal oj-slider-vertical");
                this._barback.removeClass("oj-slider-horizontal oj-slider-vertical");

                if (this._isVertical()) {
                    this._elementWrapped.addClass( "oj-slider-vertical");
                    this._barback.addClass("oj-slider-vertical");
                } else {
                    this._elementWrapped.addClass( "oj-slider-horizontal");
                    this._barback.addClass("oj-slider-horizontal");
                }

                this._refreshValue();

                // Reset positioning from previous orientation
                this._thumbs.css( coercedValue === "horizontal" ? "top" : "left", "" );

	        this._makeDraggable();

                break;
            case "value":
                this._refreshValue();
	        this._makeDraggable();
                break;

            case "step":
            case "min":
            case "max":

                this._calculateNewMax();
                this._refreshValue();
	        this._makeDraggable();
                break;
            case "range":

                this._setup();
                break;
            }
        },

        // internal value getter
        // _getValueAligned() returns value trimmed by min and max, aligned by step
        _getValueAligned: function() {
            var val = this.options.value;
            val = this._trimAlignValue( val );

            return val;
        },

        // 
        // Internal values getter
        //
        // _getValuesAligned() returns array of values trimmed by min and max, aligned by step
        // _getValuesAligned( index ) returns single value trimmed by min and max, aligned by step
        // 
        _getValuesAligned: function( index ) {
            var val = this._trimAlignValue(this.options.value[index]);
            return val;
        },

        // Return options.values with the new value copied in the array
        _getNewValues: function(index, newValue) {

            var vals, i;

            vals = this.options.value.slice();

            for ( i = 0; i < vals.length; i += 1) {
                // console.log("**********  " + i + " " + vals[i]);
                vals[ i ] = this._trimAlignValue( vals[ i ] );
            }

            // assume newValue is trim aligned
            // Assign only if it is the thumb that is actually sliding
            if (index === this._thumbIndex)
                vals[index] = newValue;

            return vals;

        },

        // returns the step-aligned value that val is closest to, between (inclusive) min and max
        _trimAlignValue: function( val ) {
            if ( val <= this._valueMin() ) {
                return this._valueMin();
            }
            if ( val >= this._valueMax() ) {
                return this._valueMax();
            }
            var step = ( this.options.step > 0 ) ? this.options.step : 1,
            valModStep = (val - this._valueMin()) % step,
            alignValue = val - valModStep;

            if ( Math.abs(valModStep) * 2 >= step ) {
                alignValue += ( valModStep > 0 ) ? step : ( -step );
            }

            // Since JavaScript has problems with large floats, round
            // the final value to 5 digits after the decimal point (see #4124)
            return parseFloat( alignValue.toFixed(5) );
        },

        _calculateNewMax: function() {
            // Check if we need to do this--don't just use straight remainder because of JS math issues
            var min = this._valueMin();
            if ((((this.options.max - min) / this.options.step) % 1) !== 0) {
                var remainder = ( this.options.max - min ) % this.options.step;
                this.max = this.options.max - remainder;
            }
            else {
                this.max = this.options.max;
            }
        },

        _valueMin: function() {
            return this.options.min;
        },

        _valueMax: function() {
            return this.max;
        },

        //
        // Calculate the grid size passed to draggable.
        // This implements the drag to step increments.
        // For a horizontal grid, we would return an array of [pixelInterval, 1],
        // while a vertical grid returns [1, pixelInterval].
        //
        _getGrid: function () {

            var numIntervals;
            if (this.options.step > 0)
                numIntervals = (this._valueMax() - this._valueMin()) / this.options.step;
            else
                numIntervals = 100; // this case should not occur.

            var pixelTotal;

            if (!this._isVertical()) {
                pixelTotal = this._barback.width();
            } else {
                pixelTotal = this._barback.height();
            }
            
            var pixelInterval = pixelTotal / numIntervals;

            if (pixelInterval < 1) pixelInterval = 1;

            if (!this._isVertical()) {
                return new Array(pixelInterval, 1);
            } else {
                return new Array(1, pixelInterval);
            }
            
        },

        _getThumbsValueFrac: function (index) {

            return ((this._getMultiValues(index) - this._valueMin()) / (this._valueMax() - this._valueMin()));
            // Note - (max - min) is checked in options to make sure that it is non-zero and positive 
            // Note - we always use aligned values.
        },

        _refreshValue: function() {

            var lastValPercent, valPercent, value, valueMin, valueMax, valPercentCss;

            // 
            // Multiple thumbs case.
            // 
            if (this._multipleThumbs) {
                this._thumbs.toArray().forEach (

                    function(current, i) {
                        var thumb = $(current);
                        valPercent = this._getThumbsValueFrac(i) * 100;

                        // valPercentCss = valPercent; // use this for the css width/left/etc.
                        if (this._isRTL() && !this._isVertical()) valPercent = 100 - valPercent;
                        // console.log(i + " Refresh value " + valPercent);
                        
                        if (!this._isVertical()) {
                            $(thumb)["css"]({left: valPercent + "%"});
                        } else { 
                            $(thumb)["css"]({top: (100 - valPercent) + "%"});
                        }

                        $(thumb).attr('aria-valuenow', this._getMultiValues(i));
                        this._setRangeMultiThumb(valPercent, i);

                    },
                    this
                );

            } else {

                // 
                // Scalar value (single thumb)
                //

                // 
                // We always want an aligned value here.
                // 
                value =  this._getValueAligned();

                // console.log("refreshValue: single value " + value)

                valueMin = this._valueMin();
                valueMax = this._valueMax();
                valPercent = ( valueMax !== valueMin ) ?
                    ( value - valueMin ) / ( valueMax - valueMin ) * 100 : 0;

                // valPercentCss = valPercent; // use this for the css width/left/etc.
                if (this._isRTL() && !this._isVertical()) valPercent = 100 - valPercent;

                if (!this._isVertical()) {
                    this._thumb["css"]({left: valPercent + "%"});
                } else {
                    this._thumb["css"]({top: (100 - valPercent) + "%"});
                }

                $(this._thumb).attr('aria-valuenow', value);

                // this._setRange(valPercentCss);
                this._setRange(valPercent);
            }
        },

        // Set the range (bar value)
        _setRange: function(val) {

            var oRange = this.options.type;

            if (!this._isVertical()) {

                if (!this._isRTL()) {
                    if (oRange === "fromMin") {
                        this._range["css"]( { width: val + "%" });
                    }
                    if (oRange === "fromMax") {
                        this._range["css" ]( { width: ( 100 - val ) + "%" });
                    }
                }
                else {
                    if (oRange === "fromMin") {
                        this._range["css" ]( { width: ( 100 - val ) + "%" });
                    }
                    if (oRange === "fromMax") {
                        this._range["css"]( { width: val + "%" });
                    }
                }
            }
            else {

                if ( oRange === "fromMin") {
                    this._range["css"]( { height: val + "%" });
                }
                if ( oRange === "fromMax") {
                    this._range["css"]( { height: ( 100 - val ) + "%" });
                }
            }
        },

        // 
        // set the range for a multi-thumb (range) slider
        // 
        _setRangeMultiThumb: function(val, index) {

            // console.log("");
            // console.log(index + " setRangeMultiThumb " + val);

            var id = this._range.attr('id');

            if (index == 0) {

                var thumb1Pct = this._getThumbsValueFrac(1) * 100;
                // console.log("thumb1Pct " + thumb1Pct);

                switch (this.options.type) {

                case "fromMin":

                    if (!this._isVertical()) {
                        this._range["css"]( { width: val + "%" });
                    } 
                    else {
                        this._range["css"]( { height: val + "%" });
                    }
                    break;

                case "range":

                    if (!this._isVertical() ) {
                        if (!this._isRTL()) {
                            this._range["css"]( { left: val + "%" });
                            this._range["css"]( { width: (thumb1Pct - val) + "%" });
                        }
                        else {
                            this._range["css"]( { left: (100 - thumb1Pct) + "%" });
                            this._range["css"]( { width: (thumb1Pct - (100 - val)) + "%" });
                        }

                    } else {
                        this._range["css"]( { top: (100 - thumb1Pct) + "%" });
                        this._range["css"]( { height: (thumb1Pct - val) + "%" });
                    }

                    break;
                }

            } else {

                var thumb0Pct = this._getThumbsValueFrac(0) * 100;
                // console.log("thumb0Pct " + thumb0Pct);

                switch (this.options.type) {

                case "fromMax":
                    if (!this._isVertical()) {
                        this._range["css" ]( { width: ( 100 - val ) + "%" });
                    } 
                    else {
                        this._range["css" ]( { height: ( 100 - val ) + "%" });
                    }
                    break;

                case "range":

                    if (!this._isVertical() ) {
                        if (!this._isRTL()) {
                            if (document.getElementById(id)) {
                                var barLeft = parseInt(document.getElementById(id).style.left, 10);
                                this._range["css"]( { width: (val - barLeft ) + "%" });
                            }
                        }
                        else {
                            if (document.getElementById(id)) {
                                this._range["css"]( { left: val + "%" });
                                this._range["css"]( { width: (- val + 100 - thumb0Pct) + "%" });
                            }
                        }

                    } else {

                        if (document.getElementById(id)) {
                            this._range["css"]( { top: (100 - val) + "%" });
                            this._range["css"]( { height: (val - thumb0Pct) + "%" });
                        }
                    }

                    break;
                }
            }
        },

        _thumbEvents: {
            keydown: function( event ) {
                var allowed, curVal, newVal, step, tempVal,
                
                index = $( event.target ).data( "oj-slider-thumb-index" );
                this._thumbIndex = index;

                switch (event.keyCode) {
                case $.ui.keyCode.HOME:
                case $.ui.keyCode.END:
                case $.ui.keyCode.PAGE_UP:
                case $.ui.keyCode.PAGE_DOWN:
                case $.ui.keyCode.UP:
                case $.ui.keyCode.RIGHT:
                case $.ui.keyCode.DOWN:
                case $.ui.keyCode.LEFT:
                    event.preventDefault();
                    if ( !this._keySliding ) {
                        this._keySliding = true;
                        $( event.target ).addClass( "ui-state-active" );
                    }
                    break;
                }

                step = this.options.step;

                if (this._multipleThumbs) {
                    curVal = newVal = this._getMultiValues( index );
                } else {
                    curVal = newVal = this._getSingleValue();
                }

                switch (event.keyCode) {
                case $.ui.keyCode.HOME:
                    newVal = this._valueMin();
                    break;
                case $.ui.keyCode.END:
                    newVal = this._valueMax();
                    break;
                case $.ui.keyCode.PAGE_UP:
                    newVal = this._trimAlignValue(
                        curVal + ( ( this._valueMax() - this._valueMin() ) / this._numPages )
                    );
                    break;
                case $.ui.keyCode.PAGE_DOWN:
                    newVal = this._trimAlignValue(
                        curVal - ( (this._valueMax() - this._valueMin()) / this._numPages ) );
                    break;
                case $.ui.keyCode.UP:
                case $.ui.keyCode.RIGHT:

                    if (!this._isRTL() || this._isVertical()) {
                        if (curVal === this._valueMax()) return;
                        tempVal = curVal + step;
                    }
                    else {
                        if (curVal === this._valueMin()) return;
                        tempVal = curVal - step;
                    }

                    newVal = this._trimAlignValue(tempVal);
                    break;
                case $.ui.keyCode.DOWN:
                case $.ui.keyCode.LEFT:

                    if (!this._isRTL() || this._isVertical()) {
                        if (curVal === this._valueMin()) return;
                        tempVal = curVal - step;
                    }
                    else {
                        if (curVal === this._valueMax()) return;
                        tempVal = curVal + step;
                        // console.log("horz key " + curVal + " -> " + tempVal + " " + step);
                    }

                    newVal = this._trimAlignValue(tempVal);
                    break;
                }

                this._slide( event, index, newVal );
            },
            keyup: function(event) {
                var index = $( event.target ).data( "oj-slider-thumb-index" );
                this._thumbIndex = index;

                if ( this._keySliding ) {
                    this._keySliding = false;

                    this._change( event, index );
                    
                    $( event.target ).removeClass( "ui-state-active" );
                }
                this._thumbIndex = null;
                this._refreshContainment();

            }
        },

        // P R O T E C T E D    C O N S T A N T S   A N D   M E T H O D S

        // *********** START WIDGET FACTORY METHODS (they retain _camelcase naming convention) **********
        
        
        /**
          * Called at component create time primarily to initialize options, often using DOM values. This 
          * method is called before _ComponentCreate is called, so components that override this method 
          * should be aware that the component has not been rendered yet. The element DOM is available and 
          * can be relied on to retrieve any default values. <p> 
          * @param {!Object} originalDefaults - original default options defined on the widget and its ancestors
          * @param {?Object} constructorOptions - options passed into the widget constructor
          * 
          * @memberof oj.ojSlider
          * @instance
          * @protected
          */
        _InitOptions: function(originalDefaults, constructorOptions)
        {

            var opts = this.options;
            var self = this;

            this._superApply(arguments);

            // props changed for 1.1.1 backport.
            var props = 
                [{attribute: "disabled", defaultOptionValue: false, validateOption: true},
                 {attribute: "placeholder", defaultOptionValue: ""},
                 {attribute: "value", defaultOptionValue: null}, // don't coerce here. I do it myself
                 {attribute: "readonly", option: "readOnly", defaultOptionValue: false, validateOption: true},
                 {attribute: "required", defaultOptionValue: false,
                  coerceDomValue: true, validateOption: true},
                 {attribute: "title", defaultOptionValue: ""},
                 {attribute: "min", defaultOptionValue: null},
                 {attribute: "max", defaultOptionValue: null},
                 {attribute: "step", defaultOptionValue: 1}];


            oj.EditableValueUtils.initializeOptionsFromDom(props, constructorOptions, this,
                // post-process callback
                function (initializedOptions)
                { 
                    // coerce regardless of where the option value came from - dom/constructor
                    var toParse = ['value', 'step', 'min', 'max'];
                    
                    for (var i=0; i<toParse.length; i++)
                    {
                        var opt = toParse[i];
                        var value = (opt in initializedOptions) ? initializedOptions[opt] : opts[opt];
                        if (value != null)
                        {
                            if (opt === 'step')
                                initializedOptions[opt] =  self._parseStep(value);
                            else if (opt === 'min' || opt === 'max')
                                initializedOptions[opt] =  self._parse(opt, value);
                            else if (opt === 'value') {
                                if (Array.isArray(value))
                                    // todo - create a version of _parse to accept array values.
                                    // initializedOptions[opt] =  self._parse(opt, value);
                                    initializedOptions[opt] = value;
                                else 
                                    initializedOptions[opt] =  self._parse(opt, value);
                            }
                        } 
                    }
                }
            );
            
            if (opts['value'] === undefined)
            {
                throw new Error(this.getTranslatedString("noValue"));
            }
            
            // now make sure min < max, else throw an Error
            if (opts['min'] != null && opts['max'] != null)
            {
                if (opts['max'] < opts['min'])
                {
                    throw new Error(this.getTranslatedString("maxMin"));
                }
                
                // Make sure value is within min and max
                if (opts['value'] < opts['min'] || opts['value'] > opts['max']) {
                    throw new Error(this.getTranslatedString("valueRange"));
                }
            }
        },
        
        /**
         * Return the subcomponent node represented by the documented locator 
         * attribute values.
         * Test authors should target sub elements using the following names:
         * <ul>
         * <li><b>oj-slider-bar</b>: the slider bar</li>
         * <li><b>oj-slider-bar-value</b>: The value portion of the slider bar. </li>
         * <li><b>oj-slider-thumb-0</b>: The thumb for a single thumb slider, the minimum thumb for a range slider.</li>
         * <li><b>oj-slider-thumb-1</b>: The second thumb of a range slider. (Do not use in a single thumb slider) </li>
         * </ul>
         * @expose
         * @memberof! oj.ojSlider
         * @instance
         * @override
         * @param {Object} locator An Object containing at minimum a subId property 
         *        whose value is a string, documented by the component, that allows 
         *        the component to look up the subcomponent associated with that 
         *        string.  It contains:<p>
         *        component: optional - in the future there may be more than one 
         *        component contained within a page element<p>
         *        subId: the string, documented by the component, that the component 
         *        expects in getNodeBySubId to locate a particular subcomponent
         * @returns {Element|null} the subcomponent located by the subId string passed
         *          in locator, if found.<p>
         */
        getNodeBySubId: function(locator)
        {
            if (locator == null)
            {
                return this.element ? this.element[0] : null;
            }
            
            var subId = locator['subId'];
            
            var thumbId;
            if (subId === "oj-slider-thumb-0") {
                thumbId = "#" + this._getThumbId(0);
                return this.widget().find(thumbId)[0];
            }
            else if (subId === "oj-slider-thumb-1") {
                thumbId = "#" + this._getThumbId(1);
                return this.widget().find(thumbId)[0];
            }
            else if (subId === "oj-slider-bar") {
                var barId = "#" + this._getBarBackgroundId();
                return this.widget().find(barId)[0];
            }
            else if (subId === "oj-slider-bar-value") {
                var barValueId = "#" + this._getBarValueId();
                return this.widget().find(barValueId)[0];
            }
            
            // Non-null locators have to be handled by the component subclasses
            return null;
        },
        
        /**
         * Returns the subId locator for the given child DOM node. 
         * Invoked by widget
         * @param {!Element} node - child DOM node
         * @return {Object|null} The subId for the DOM node, or null when none is found.
         */
        getSubIdByNode: function(node)
        {
            var item, key;

            if (node != null) {
                if (node.id === this._getThumbId(0) && $(node).hasClass("oj-slider-thumb"))
                    return {'subId': 'oj-slider-thumb-0'};
                else if (node.id === this._getThumbId(1) && $(node).hasClass("oj-slider-thumb"))
                    return {'subId': 'oj-slider-thumb-1'};
                else if (node.id === this._getBarBackgroundId() && $(node).hasClass("oj-slider-bar"))
                    return {'subId': 'oj-slider-bar'};
                else if (node.id === this._getBarValueId() && $(node).hasClass("oj-slider-bar-value"))
                    return {'subId': 'oj-slider-bar-value'};
            }

            return null;
        },

        // *********** END WIDGET FACTORY METHODS **********

        /**
         * Returns the default styleclass for the component.
         * 
         * @return {string}
         * @memberof oj.ojSlider
         * @override
         * @protected
         */
        _GetDefaultStyleClass: function()
        {
            return "oj-slider";
        },

        // The user can clear out min/max by setting the option to null, so we
        // do not coerce null.
        /**
         * @param {string} option name of the option. this will show up in the error if thrown
         * @param val value to parse
         * @throws {Error} if option value is invalid
         * @private
         */
        _parse: function(option, val)
        {
            var returnValue;
            if (val !== null)
                returnValue = +val;
            else
                returnValue = val;

            if (isNaN(returnValue))
                throw new Error(this.getTranslatedString("optionNum", {'option':option}));
            
            return returnValue;
        },
        /**
         * parse the step's value
         * We are following the behavior of HTML-5 the best we can. According
         * to the spec, it says step must be a number greater than 0. 
         * Chrome defaults it to 1 if it is not.
         * @throws {Error} if option value is invalid 
         * @private
         */
        _parseStep: function(val)
        {
            var defaultStep = 1, parsedStep;
            if (val === null)
                return defaultStep;
            parsedStep = this._parse("step", val);
            if (parsedStep <= 0)
            {
                // throw an exception
                throw new Error(this.getTranslatedString("invalidStep"));
            }
            // DEFAULT to 1 if it isn't > 0
            if (parsedStep === null || parsedStep <= 0)
                parsedStep = defaultStep;
            return parsedStep;
        },

        /////////////////////////////////////////////////////////////////////////////////////////
        //
        // Draggable - used to implement slider thumb dragging
        //
        // JQueryUI draggable is used to implement the dragging of slider thumbs.
        // Several draggable options are used:
        //
        // - Axis constaint
        //   The thumb is constrained to movement along the x-axis (for horizontal sliders)
        //   or movement along the y-axis (for vertical siders)
        //
        // - Range constraints
        //   The thumb is constrained to ranges along the axis using the draggable constraint 
        //   option. 
        //
        // - Step
        //   The granularity of movement is constrained to the step size using the grid 
        //   option.
        //
        /////////////////////////////////////////////////////////////////////////////////////////

        // return the endpoint of the bar
        _getEndInterval: function() {
            return (this._barback.offset().left + this._barback.width());
        },
            
        // return the startpoint of the bar
        _getStartInterval: function() {
            return (this._barback.offset().left);
        },
            

        // 
        // Return the containment array for a range slider.
        // 
	_getContainmentArrayRange: function(index) {

            var valFrac;

            // Initialize contaiment array to that of the single thumb case.
            var c = this._getContainmentArray();
            var x1 = c[0], y1 = c[1], x2 = c[2], y2 = c[3];

            // console.log(index + " pre containment " + x1 + " " + y1 + " " + x2 + " " + y2);

            // 
            // Limit the maximum slide of the first thumb to the current value of the second thumb.
            // Limit the minimum slide of the second thumb to the current value of the first thumb.
            // 

            var thumbHalfHeight = this._thumb.outerHeight()/2;

            switch (index) {

            case 0:
                valFrac = this._getThumbsValueFrac(1);
                if (!this._isVertical()) {
                    if (!this._isRTL()) {
                        x2 = x1 + (valFrac * this._barback.width());
                    }
                    else {
                        x2 = this._getEndInterval();
                        x1 = x2 - (valFrac * this._barback.width());
                    }
                }
                else {
                    y2 = this._barback.offset().top + this._barback.height();
                    y1 = this._barback.offset().top + ((1 - valFrac) * this._barback.height());
                }

                break;

            case 1:
                valFrac = this._getThumbsValueFrac(0);
                if (!this._isVertical()) {
                    if (!this._isRTL()) {
                        x1 = this._barback.offset().left + (valFrac * this._barback.width());
                    }
                    else {
                        x1 = this._getStartInterval();
                        x2 = x1 + ((1 - valFrac) * this._barback.width());
                    }
                }
                else {
                    y1 = this._barback.offset().top;
                    y2 = y1 + ((1 - valFrac) * this._barback.height());
                }
                break;
            }

            // console.log(index + " valFrac " + valFrac);
            // console.log(index + " containment " + x1 + " " + y1 + " " + x2 + " " + y2);

            return ([x1, y1, x2, y2]);
        },


        // 
        // Return the containment boundbox of the bar.
        // Used to constrain the slider thumb to the bar.
        // 
	_getContainmentArray: function() {

            var x1 = this._barback.offset().left;
            var y1;

            if (!this._isVertical()) {
                y1 = this._barback.offset().top;
            } else {
                // y1 = this._barback.offset().top - this._thumb.outerHeight()/2;
                y1 = this._barback.offset().top;
            }
            
            var x2 = x1 + this._barback.width();
            var y2 = y1 + this._barback.height();

            // console.log(" containment array " + x1 + " " + y1 + " " + x2 + " " + y2);
            return ([x1, y1, x2, y2]);
        },

        // 
        // Set up the draggable with the context, thumb, and containment parameters.
        // Use the axis method to ensure only horizontal or vertical movement.
        // 
	_callDraggable: function(thumbParam, containmentArray) {

            // in progress
           var g = this._getGrid();

            var axisValue;
            if (!this._isVertical()) axisValue = "x";
            else axisValue = "y";

            // var that = this ? sub for containment
            var that = this;

            thumbParam.draggable({
                axis: axisValue,
		containment: containmentArray,
                // grid: [8.8,1],
                grid: g,
                disabled: false,
		start: function( event, ui ) {
                    // console.log("drag start");
                    that._initDragging(event, true);
		},
		'drag': function( event, ui ) {
                    // console.log("drag");
                    that._mouseDrag(event);
		},
		stop: function( event, ui ) {
                    // console.log("drag stop");
                    that._mouseStop(event);
                    that._refreshContainment();
		}
	    });
        },

        //
        // Refresh the thumb slider containment (for range sliders)
        // After an interaction (slide or key) is complete, this method is used to redefine the 
        // containment of the slider thumb.
        //
	_refreshContainment: function() {

	    var that = this,
	    options = this.options;
            var containmentArray;

            if (this._multipleThumbs) {
                this._thumbs.toArray().forEach (

                    function(current, i) {
                        var thumb = $(current);
                        containmentArray = this._getContainmentArrayRange(i);
                        thumb['draggable']("option", "containment", containmentArray);
                    },
                    this
                );
            }
        },

        //
        // Setup the draggable for each of the thumbs.
        //
	_makeDraggable: function() {

            // Do not allow dragging on a disabled thumb.
            if (this.options.disabled) return;

            var containmentArray;

            // console.log("_makeDraggable");

            if (this._multipleThumbs) {
                this._thumbs.toArray().forEach (

                    function(current, i) {
                        var thumb = $(current);
                        containmentArray = this._getContainmentArrayRange(i);
	                this._callDraggable(thumb, containmentArray);
                    },
                    this
                );

            } else {
                containmentArray = this._getContainmentArray();
	        this._callDraggable(this._thumb, containmentArray);
                // needed ?
	        // this._thumb.addClass("oj-draggable")
            }
	},

        // 
        // Call this if we change option to disabled.
        // 
	_disableDraggable: function() {

            if (this._multipleThumbs) {
                this._thumbs.toArray().forEach (

                    function(current, i) {
                        var thumb = $(current);
                        if (thumb.is('.ui-draggable'))
                            thumb['draggable']("disable");
                    },
                    this
                );

            } else {
                if (this._thumb.is('.ui-draggable'))
                    this._thumb.draggable("disable");
            }
	}

        /////////////////////////////////////////////////////////////////////////////////////////
        // Draggable - end
        /////////////////////////////////////////////////////////////////////////////////////////


/**
 * <table class="keyboard-table">
 *   <thead>
 *     <tr>
 *       <th>Target</th>
 *       <th>Gesture</th>
 *       <th>Action</th>
 *     </tr>
 *   </thead>
 *   <tbody>
 *     <tr>
 *       <td>Slider Bar</td>
 *       <td><kbd>Tap</kbd></td>
 *       <td>Reposition the thumb.</td>
 *     </tr>
 *     <tr>
 *       <td>Slider Thumb</td>
 *       <td><kbd>Swipe</kbd></td>
 *       <td>Reposition the thumb.</td>
 *     </tr>
 *   </tbody>
 * </table>
 *
 * @ojfragment touchDoc - Used in touch gesture section of classdesc, and standalone gesture doc
 * @memberof oj.ojSlider
 */

/**
 * The JET slider supports keyboard actions for thumb movement:
 *
 * <p>
 * <table class="keyboard-table">
 *   <thead>
 *     <tr>
 *       <th>Key</th>
 *       <th>Use</th>
 *     </tr>
 *   </thead>
 *   <tbody>
 *     <tr>
 *       <td><kbd>Tab</kbd></td>
 *       <td> Places focus on the slider component.
 *     </tr>
 *     <tr>
 *       <td><kbd>RightArrow</kbd></td>
 *       <td>Scrolls right on a horizontal slider, scrolls up on a vertical slider.
 *       </td>
 *     </tr>
 *     <tr>
 *       <td><kbd>LeftArrow</kbd></td>
 *       <td>Scrolls left on a horizontal slider, scrolls down on a vertical slider.
 *       </td>
 *     </tr>
 *     <tr>
 *       <td><kbd>UpArrow</kbd></td>
 *       <td>Scrolls right on a horizontal slider, scrolls up on a vertical slider.
 *       </td>
 *     </tr>
 *     <tr>
 *       <td><kbd>DownArrow</kbd></td>
 *       <td>Scrolls left on a horizontal slider, scrolls down on a vertical slider.
 *       </td>
 *     </tr>
 *     <tr>
 *       <td><kbd>PageUp</kbd></td>
 *       <td>Scrolls one page right on a horizontal slider, scrolls one page up on a vertical slider. <br>
 *       A page is defined as 20% of the range of the slider.
 *     </td>
 *     </tr>
 *     <tr>
 *       <td><kbd>PageDown</kbd></td>
 *       <td>Scrolls one page left on a horizontal slider, scrolls one page down on a vertical slider.
 *       </td>
 *     </tr>
 *     <tr>
 *       <td><kbd>End</kbd></td>
 *       <td>Scrolls to the right end on a horizontal slider, scrolls to the bottom on a vertical slider.
 *       </td>
 *     </tr>
 *     <tr>
 *       <td><kbd>Home</kbd></td>
 *       <td>Scrolls to the left end on a horizontal slider, scrolls to the top on a vertical slider.
 *       </td>
 *     </tr>
 * </tbody>
 * </table>
 *
 * @ojfragment keyboardDoc - Used in keyboard section of classdesc, and standalone gesture doc
 * @memberof oj.ojSlider
 *
 */

    });

}() );