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

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * Copyright (c) 2014, 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
 */

/**
 * @ojcomponent oj.ojTrain
 * @augments oj.baseComponent
 * @classdesc
 * <h3 id="trainOverview-section">
 *   JET Train Component
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#trainOverview-section"></a>
 * </h3>
 *
 * <p>The ojTrain component is a navigation visual that allows a user to go between different "steps".
 * Each step can display information about the state of the step("visited", "unvisited", "disabled")
 * and display a message type("error", "confirmation", "warning", "info", "fatal")</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="markup-section">
 *   HTML Markup and Style Classes
 *   <a class="bookmarkable-link" title="Bookmarkable Link" href="#markup-section"></a>
 * </h3>
 *
 * <table class="generic-table styling-table">
 *   <thead>
 *     <tr>
 *       <th>Class(es)</th>
 *       <th>Description</th>
 *     </tr>
 *   </thead>
 *   <tbody>
 *     <tr>
 *       <td> oj-train-stretch</td>
 *       <td> Optional class that may be added to the train div which will stretch the train to cover the full width of the container specified. </td>
 *     </tr>
 *   </tbody>
 * </table>
 *
 *
 *
 * @desc Creates a JET Train. 
 * @example <caption>Initialize component using widget API</caption>
 * <div id="train"/><br/>
 * $("#train").ojTrain({'selected': "stp1", 'steps': [{label:'Step One', id:'stp1'},
 * {label:'Step Two', id:'stp2'},{label:'Step Three', id:'stp3'}]});
 * @example <caption>Using knockout, selected bind to observables - selectedStep</caption>
 * <div id="train" data-bind="ojComponent:{component: 'ojTrain', selected: selectedStep, steps:[{label:'Step One', id:'stp1'},
 * {label:'Step Two', id:'stp2'},{label:'Step Three', id:'stp3'}]}"/>gt;
 */
(function()
{
  oj.__registerWidget("oj.ojTrain", $['oj']['baseComponent'],
          {
            version: "1.0.0",
            defaultElement: "<div>",
            widgetEventPrefix: "oj",
            options: {
			
              /**
               * The array of step objects. Each step must have an 'id' and 'label' variable. 
               * <ul>
               *   <li><code class="prettyprint">id</code> - The id for the step</li>
               *   <li><code class="prettyprint">label</code> - The label for the step </li>
               *   <li><code class="prettyprint">disabled</code> - Indicates if the step is selectable. 'disabled' is a boolean type with a default value of false. </li>
               *   <li><code class="prettyprint">visited</code> - Indicates if the step has been visited. 'visited' is a boolean type with a default value of false. </li>
               *   <li><code class="prettyprint">messageType</code> - The messageType icon to display on the step. Possible options are 'confirmation', 'info', 'error', 'fatal', or 'warning'. Default value is null. </li>
               * </ul>
               * @expose
               * @public
               * @instance
               * @memberof! oj.ojTrain
               */
              steps: [],
			  
              /**
               * The selected variable indicates the id of the current selected step.
               * @expose
               * @public
               * @instance
               * @memberof! oj.ojTrain
               */
              selected: "",
			  
              // Events

              /**
               * Fired whenever a supported component option changes, whether due to user interaction or programmatic
               * intervention.  If the new value is the same as the previous value, no event will be fired.
               *
               * Currently there is one supported option, <code class="prettyprint">"selected"</code>.  Additional
               * options may be supported in the future, so listeners should verify which option is changing
               * before taking any action.
               *
               * @expose
               * @event
               * @memberof! oj.ojTrain
               * @instance
               * @property {Event} event <code class="prettyprint">jQuery</code> event object
               * @property {Object} ui Parameters
               * @property {string} ui.option the name of the option that is changing
               * @property {string} ui.previousValue the previous value of the option
               * @property {string} ui.value the current value of the option
               * @property {Object} ui.optionMetadata information about the option that is changing
               * @property {string} ui.optionMetadata.writeback <code class="prettyprint">"shouldWrite"</code> or
               *           <code class="prettyprint">"shouldNotWrite"</code>.  For use by the JET writeback mechanism.
               *
               */
              optionChange: null,
			  
              /**
               * Triggered immediately before a step is deselected.
               * The beforeDeselect can be cancelled by calling <code class="prettyprint">event.
               
               Default()</code>.
               *
               * @expose 
               * @event 
               * @memberof! oj.ojTrain
               * @instance
               * @property {Event} event <code class="prettyprint">jQuery</code> event object
               * @property {Object} ui Parameters
               * @property {jQuery} ui.toStep The step that is about to be deselected.
               * @property {jQuery} ui.fromStep The step that is about to be selected.
               * 
               * @example <caption>Initialize the train with the <code class="prettyprint">beforeDeselect</code> callback specified:</caption>
               * $( ".deselector" ).ojTrain({
               *     "beforeDeselect": function( event, ui ) {}
               * });
               *
               * @example <caption>Bind an event listener to the <code class="prettyprint">ojbeforedeselect</code> event:</caption>
               * $( ".deselector" ).on( "ojbeforedeselect", function( event, ui ) {} );
               */
              beforeDeselect: null,
			  
              /**
               * Triggered after a step has been deselected.
               * The deselect can be cancelled by calling <code class="prettyprint">event.preventDefault()</code>.
               *
               * @expose 
               * @event 
               * @memberof! oj.ojTrain
               * @instance
               * @property {Event} event <code class="prettyprint">jQuery</code> event object
               * @property {Object} ui Parameters
               * @property {jQuery} ui.toStep The step that is about to be deselected.
               * @property {jQuery} ui.fromStep The step that is about to be selected.
               * 
               * @example <caption>Initialize the train with the <code class="prettyprint">deselect</code> callback specified:</caption>
               * $( ".deselector" ).ojTrain({
               *     "deselect": function( event, ui ) {}
               * });
               *
               * @example <caption>Bind an event listener to the <code class="prettyprint">ojdeselect</code> event:</caption>
               * $( ".deselector" ).on( "ojdeselect", function( event, ui ) {} );
               */
              deselect: null,
			  
              /**
               * Triggered immediately before a Step is selected.
               * The beforeSelect can be cancelled by calling <code class="prettyprint">event.preventDefault()</code>.
               *
               * @expose 
               * @event 
               * @memberof! oj.ojTrain
               * @instance
               * @property {Event} event <code class="prettyprint">jQuery</code> event object
               * @property {Object} ui Parameters
               * @property {jQuery} ui.toStep The step that is about to be deselected.
               * @property {jQuery} ui.fromStep The step that is about to be selected.
               * 
               * @example <caption>Initialize the train with the <code class="prettyprint">beforeSelect</code> callback specified:</caption>
               * $( ".selector" ).ojTrain({
               *     "beforeSelect": function( event, ui ) {}
               * });
               *
               * @example <caption>Bind an event listener to the <code class="prettyprint">ojbeforeselect</code> event:</caption>
               * $( ".selector" ).on( "ojbeforeselect", function( event, ui ) {} );
               */
              beforeSelect: null,
			  
              /**
               * Triggered after a step has been selected.
               *
               * @expose 
               * @event 
               * @memberof! oj.ojTrain
               * @instance
               * @property {Event} event <code class="prettyprint">jQuery</code> event object
               * @property {Object} ui Parameters
               * @property {jQuery} ui.toStep The step that is about to be deselected.
               * @property {jQuery} ui.fromStep The step that is about to be selected.
               * 
               * @example <caption>Initialize the train with the <code class="prettyprint">select</code> callback specified:</caption>
               * $( ".selector" ).ojTrain({
               *     "select": function( event, ui ) {}
               * });
               *
               * @example <caption>Bind an event listener to the <code class="prettyprint">ojselect</code> event:</caption>
               * $( ".selector" ).on( "ojselect", function( event, ui ) {} );
               */
              select: null
            },
			
            /**
             * Variable for storing the number of steps in the train
             *
             * @private
             */
            _stepNum: 0,
			
            /**
             * Variable for storing the train information about each step in 2D Array form
             *
             * @private
             */
            _stepArray: null,
			
            _ComponentCreate: function() {
              this._super();
              this._setupTrain();
            },
			
            /**
             * Set up the train component.
             *
             * @private
             */
            _setupTrain: function() {
              // Constrain initial value
              var options = this.options;
              var steps = options.steps;
              this._stepNum = steps.length;

              // Train Wrapper
              this._wrapper = $("<div class='oj-train-wrapper'></div>");

              // @HTMLUpdateOK
              this._wrapper.appendTo(this.element);

              this._connectorWrapper = $("<div class='oj-train-connector-wrapper'></div>");

              // @HTMLUpdateOK
              this._connectorWrapper.appendTo(this._wrapper);

              var styleClass = this.element.attr('class');
              this._stretch = (styleClass != null && styleClass.indexOf("oj-train-stretch") >= 0);
              if (this._stretch)
                this._connectorWrapper.css("padding", "0 " + 100 / (this._stepNum * 2) + "%");

              // Draw the connector bar  for the train
              this._connector = $("<div class='oj-train-connector'></div>");

              // @HTMLUpdateOK
              this._connector.appendTo(this._connectorWrapper);

              this._stepList = $("<ul>");
              this._stepList.addClass('oj-train-step-list');


              // Initialize the background progressbar object that will be updated to have the correct width based on the current step.
              this._connectorFill = $("<div class='oj-train-connector-fill'></div>");

              // @HTMLUpdateOK
              this._connectorFill.appendTo(this._connectorWrapper);

              // Setup array that stores train information for each step.
              this._setupArray();
              this._selectedIndex = this._getStepIndex(options.selected);
              if (this._selectedIndex === -1 && steps[0] && steps[0]['id'])
              {
                this._selectedIndex = 0;
                options.selected = steps[0]['id'];
              }

              // Draw each step. Visually each step consists of a background circle, a button, an icon, and a label.
              for (var i = 0; i < this._stepNum; i++)
              {
                // Create a list item to store each step.
                var stepTag = $("<li>").addClass("oj-train-step-list-item").attr({'id': this._stepArray[i][1]})

                // @HTMLUpdateOK			  
                stepTag.appendTo(this._stepList);

                this._drawLabel(i);
                this._drawStepFill(i);
                this._drawButton(i);
                this._drawMessageType(i);
                if (this._stretch)
                  stepTag.css("width", 100 / (this._stepNum) + "%");
              }

              // Update background progressbar width to show the progress.
              var connectorFillWidth = this._stepNum - 1 === this._selectedIndex ? 100 : (100 / (2 * (this._stepNum - 1)) + this._selectedIndex / (this._stepNum - 1) * 100)
              this._connectorFill.css({'width': connectorFillWidth + "%"});

              // @HTMLUpdateOK
              this._stepList.appendTo(this._wrapper);

              this.element.addClass("oj-train");
            },
			
            /**
             * Set up the _stepArray(). The first dimension indicates the step index and the second dimension indicates the step variables.
             * The order of the step variables are: label, id, selection, visited, messageType
             *
             * @private
             */
            _setupArray: function() {
              var options = this.options;
              this._stepArray = [];
              for (var i = 0; i < this._stepNum; i++) {
                var step = options.steps[i];
                this._stepArray[i] = new Array(5);
                this._stepArray[i][0] = step['label'] ? step['label'] : null;
                this._stepArray[i][1] = step['id'] ? step['id'] : null;
                this._stepArray[i][2] = step['disabled'] ? true : false;
                this._stepArray[i][3] = step['visited'] ? true : false;
                this._stepArray[i][4] = step['messageType'] ? step['messageType'] : null;
              }
            },
			
            /**
             * Draw the button object for the step based on the index. If a button for that step alread exists remove it and draw the new one.
             * @param {number} index - The index of the step for which the button is being drawn.
             *
             * @private
             */
            _drawButton: function(index)
            {
              var button = $("<div/>")
                      .addClass("oj-train-button");
              var scrnRead = $("<span/>");
              var self = this;
              var desc = "";
              if (this._stepArray[index])
              {
                var visited = this._stepArray[index][3];
                var disabled = this._stepArray[index][2];
                if (this._selectedIndex === index) {
                  button.addClass("oj-selected");
                  desc = " current ";
                }
                else
                {
                  if (visited && !disabled) {
                    button.addClass("oj-visited");
                    desc = " visited ";
                  }
                  else if (!visited && !disabled) {
                    button.addClass("oj-default");
                    desc = " not visited ";
                  }
                  else
                    button.addClass("oj-disabled");
                }

                if (!this._stepArray[index][2] && this._selectedIndex !== index) {
                  this._hoverable(button);
                  button.on("click" + this.eventNamespace, function() {
                    var oldStepId = self.options.selected;
                    self._fireOptionChange("selected", oldStepId, this.parentNode.parentNode.id, true);
                    self.refresh();
                  });
                }
                ;
                var stepBackground = this._stepList.children().eq(index).find('.oj-train-button-connector');

                // Check that there are at least 3 items in the list item indicating that a button has already been created for this step.
                // If there is remove it and insert the new button in the same position. If there isn't a preexsisting button, simply add one.
                if (stepBackground.length >= 1) {
                  stepBackground.children().remove();

                  // @HTMLUpdateOK
                  stepBackground.append(button);
                }
                // @HTMLUpdateOK
                else
                  stepBackground.append(button);
                scrnRead.text(desc);
                scrnRead.addClass("oj-helper-hidden-accessible");

                // @HTMLUpdateOK
                this._stepList.children().eq(index).find('a').append(scrnRead);
              }
            },
			
            /**
             * Draw the icon that displays the messageType for the step based on the index. If there already is an icon remove it and draw the new one.
             * @param {number} index - The index of the step for which the icon is being drawn.
             *
             * @private
             */
            _drawMessageType: function(index)
            {
              var icon = $("<div/>")
                      .addClass("oj-train-icon");
              var scrnRead = $("<span/>");
              var desc = "";
              var self = this;
              if (this._stepArray[index])
              {
                var messageType = this._stepArray[index][4];
                if (messageType === "confirmation") {
                  icon.addClass("oj-confirmation");
                  desc = " Confirmation ";
                }
                else if (messageType === "info") {
                  icon.addClass("oj-info");
                  desc = " Info ";
                }
                else if (messageType === "error") {
                  icon.addClass("oj-error");
                  desc = " Error ";
                }
                else if (messageType === "fatal") {
                  icon.addClass("oj-error");
                  desc = " Error ";
                }
                else if (messageType === "warning") {
                  icon.addClass("oj-warning");
                  desc = " Warning ";
                }

                // Remove previous messageType
                var button = this._stepList.children().eq(index).find('.oj-train-button');
                if (button.children().length >= 1) {
                  button.children().remove();
                }
                // Make icon clickable
                if (!this._stepArray[index][2] && this._selectedIndex !== index) {
                  this._hoverable(icon);
                  icon.on("click" + this.eventNamespace, function() {
                    var oldStepId = self.options.selected;
                    self._fireOptionChange("selected", oldStepId, this.parentNode.parentNode.parentNode.id, true);
                    self.refresh();
                  });
                }
                // Add new message
                if (messageType != null) {
                  // If there is remove it and insert the new icon in the same position.
                  // Add description to span
                  scrnRead.text(desc);
                  scrnRead.addClass("oj-helper-hidden-accessible");

                  /*@HTMLUpdateOK*/
                  this._stepList.children().eq(index).find('a').append(scrnRead);
                  button.append(icon);

                }
              }
            },
			
            /**
             * Fire optionChange event 
             * @param {String} key - 'selected'
             * @param {String} previousValue 
             * @param {String} value
             * @param {Boolean} originalEvent  
             *
             * @private
             */
            _fireOptionChange: function(key, previousValue, value, originalEvent)
            {
              var eventData = {
                "option": key,
                "fromStep": this.getStep(previousValue),
                "toStep": this.getStep(value),
                // (originalEvent is non-null) iff (option change is due to user interaction) iff (binding should write back the value)
                "optionMetadata": {'writeback': originalEvent ? "shouldWrite" : "shouldNotWrite"}
              };
              if (this._trigger("beforeDeselect", null, eventData) === false || this._trigger("beforeSelect", null, eventData) === false)
                return;

              // Set the previous step to visited
              var stepIndex = this._getStepIndex(previousValue);
              if (stepIndex !== -1) {
                var oldStepProperties = this.options.steps[stepIndex];
                oldStepProperties['visited'] = true;
              }

              this._trigger("deselect", null, eventData);

              this.option('selected', value, {'_context': {originalEvent: originalEvent, internalSet: true}});

              this._trigger("select", null, eventData);

            },
			
            /**
             * Draw the background circle for the step which is either light or dark base on if the step is before or after the selected step.
             * @param {number} index - The index of the step for which the icon is being drawn.
             *
             * @private
             */
            _drawStepFill: function(index)
            {
              var stepFill = $("<div/>");
              stepFill.addClass("oj-train-button-connector");
              if (this._stepArray[index])
              {
                if (index <= this._selectedIndex)
                  stepFill.addClass("oj-train-fill");
                var stepLi = this._stepList.children().eq(index).children();
                stepFill.insertBefore(stepLi);
              }
            },
			
            /**
             * Draw the label for the step.
             * @param {number} index - The index of the step for which the icon is being drawn.
             *
             * @private
             */
            _drawLabel: function(index)
            {
              var self = this;
              if (this._stepArray[index])
              {
                var labelWrapper = $("<div/>")
                        .addClass("oj-train-label-wrapper");
                var label = $("<a>");
				label.text(this._stepArray[index][0]);
                var disabled = this._stepArray[index][2];
				labelWrapper.append(label);// @HTMLUpdateReview
                label.addClass("oj-train-label");
                if (index === this._selectedIndex)
                  label.addClass("oj-selected");
                else if (this._stepArray[index][3] && !disabled)
                  label.addClass("oj-visited");
                else if (disabled)
                  label.addClass("oj-disabled");
                if (!disabled) {
                  label.attr("href", "#");
                  this._hoverable(label);
                  label.on("click keydown" + this.eventNamespace,
                          /**
                           * @suppress {missingProperties}
                           */
                                  function(event) {
                                    if (event.keyCode === $.ui.keyCode.ENTER || event.type === "click") {
                                      event.preventDefault();
                                      var oldStepId = self.options.selected;
                                      self._fireOptionChange("selected", oldStepId, this.parentNode.parentNode.id, true);
                                      self.refresh();
                                      if (event.keyCode === $.ui.keyCode.ENTER)
                                        self._setFocus(this.parentNode.parentNode.id);
                                    }
                                  });
                        }
                var stepLi = this._stepList.children().eq(index).children();
                if (stepLi.length >= 1)
                  stepLi[0].remove();
                // @HTMLUpdateOK
                this._stepList.children().eq(index).append(labelWrapper);
              }
            },
			
            /**
             * Return the index of the step
             * @param {String} id - The index of the id whose id is being passed in.
             * @return {number} index of step. -1 for not valid ids.
             * @private
             */
            _getStepIndex: function(id)
            {
              for (var i = 0; i < this._stepNum; i++) {
                if (this._stepArray[i] && this._stepArray[i][1] === id)
                  return i;
              }
              return -1;
            },
			
            /**
             * <p>Returns the step based on the id passed in. If the step doesn't exist, return null;</p>
             * @public
             * @param {String} id - The id of the step.
             * @return {Object} step object.
             * @expose
             * @instance
             * @memberof! oj.ojTrain
             */
            getStep: function(id)
            {
              for (var i = 0; i < this._stepNum; i++) {
                if (this._stepArray[i] && this._stepArray[i][1] === id)
                  return jQuery.extend({}, this.options.steps[i]);
              }
              return null;
            },
			
            /**
             * <p>Returns the id of the next selectable step based on the selected id. If the current step is the last selectable step, returns null</p>
             * @public
             * @return {String} next selectable Id
             * @expose
             * @instance
             * @memberof! oj.ojTrain
             */
            nextSelectableStep: function()
            {
              var selectedIndex = this._getStepIndex(this.options.selected);
              for (var i = selectedIndex; i < this._stepNum; i++) {
                if (i + 1 < this._stepNum && this._stepArray[i + 1] && !this._stepArray[i + 1][2])
                {
                  return this._stepArray[i + 1][1];
                }
              }
              return null;

            },
			
            /**
             * <p>Returns the id of the previous selectable step based on the selected id. If the current step is the first selectable step, returns null</p>
             * @public
             * @return {String} previous selectable Id
             * @expose
             * @instance
             * @memberof! oj.ojTrain
             */
            previousSelectableStep: function()
            {
              var selectedIndex = this._getStepIndex(this.options.selected);
              for (var i = selectedIndex; i >= 0; i--) {
                if (this._stepArray[i - 1] && !this._stepArray[i - 1][2])
                {
                  return this._stepArray[i - 1][1];
                }
              }
              return null;
            },
			
            /**
             * <p>Sets the properties for the step. Takes in the object conatining the properties for the step.</p>
             * @public
             * @param {Object} stepProperties - The property bag to overwrite properties on the step.
             * @expose
             * @instance
             * @memberof! oj.ojTrain
             */
            setStep: function(stepProperties)
            {

              if (stepProperties['id'])
              {
                var stepInfo = this.getStep(stepProperties['id']);
                var stepIndex = this._getStepIndex(stepProperties['id']);
                if (stepIndex !== -1)
                {
                  var stepObj = this.options.steps[stepIndex];
                  if (stepProperties['label'])
                  {
                    stepInfo[0] = stepProperties['label'];
                    stepObj['label'] = stepProperties['label'];
                  }
                  if (typeof (stepProperties['disabled']) === 'boolean')
                  {
                    stepInfo[2] = stepProperties['disabled'];
                    stepObj['disabled'] = stepProperties['disabled'];
                  }
                  if (typeof (stepProperties['visited']) === 'boolean')
                  {
                    stepInfo[3] = stepProperties['visited'];
                    stepObj['visited'] = stepProperties['visited'];
                  }
                  if (stepProperties['messageType'])
                  {
                    stepInfo[4] = stepProperties['messageType'];
                    stepObj['messageType'] = stepProperties['messageType'];
                  }
                }
                this.refresh();
              }
            },
			
            _setOptions: function(options) {
              this._super(options);
              this.refresh();
            },
			
            /**
             * Refreshes the train.
             *
             * <p>This method does not accept any arguments.</p>
             * 
             * @expose 
             * @memberof! oj.ojTrain
             * @instance
             */
            refresh: function()
            {
              // TODO: Refresh should not call destroy.
              this._super();
              this._destroy();
              this._setupTrain();
            },
			
            /**
             * @override
             * @private
             */
            _destroy: function()
            {
              this._stepList.children().each(function() {
                $(this).remove();
              });
              this.element.removeClass("oj-train");
              this.element.find(".oj-train-wrapper").remove();
              this.element.find(".oj-train-connector-wrapper").remove();
              this.element.find(".oj-train-step-list").remove();
              this.element.find(".oj-train-step-list").remove();

              // Call super at the end for destroy.
              this._super();
            },
			
            /**
             * Set the focus on the step following the step specified. Focus first step if last step is specified.
             * @param {string} id - The id of the step to focus
             *
             * @private
             */
            _setFocus: function(id)
            {
              var index = this._getStepIndex(id)
              for (var i = 0; i < this._stepNum; i++) {
                if (this._stepArray[(index + i + 1) % this._stepNum] && !this._stepArray[(index + i + 1) % this._stepNum][2]) {
                  this._stepList.children().eq((index + i + 1) % this._stepNum).find('.oj-train-label').focus();
                  break;
                }
              }
            },
			
            /**
             * 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-train-button</b>: train step button </li>
             * <li><b>oj-train-button-connector</b>: train step button connector background </li>
             * <li><b>oj-train-connector</b>: train connector bar </li>
             * <li><b>oj-train-connector-fill</b>: train connector fill indicating progress </li>
             * <li><b>oj-train-icon</b>: train step message icon </li>
             * <li><b>oj-train-label</b>: train step label </li>
             * </ul>
             *
             * @expose
             * @override
             * @memberof! oj.ojTrain
             * @instance
             * @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:
             * <ul>
             * <li>
             index: a number, the index of train.
             * </li>
             * <li>
             * subId: the string, documented by the component, that the component expects 
             * in getNodeBySubId to locate a particular subcomponent 
             * </li>
             * </ul>  
             * @returns {Element|null} the subcomponent located by the subId string 
             * passed in locator, if found.
             */
            getNodeBySubId: function(locator)
            {

              if (locator === null)
              {
                return this.element ? this.element[0] : null;
              }

              var subId = locator['subId'],
                      index = locator['index'];

              if ((typeof index !== 'number') ||
                      index < 0 || index >= this._stepNum)
                return null;

              switch (subId)
              {
                case 'oj-train-button':
                  return this._stepList.children().eq(index).find('.oj-train-button')[0];

                case 'oj-train-button-connector':
                  return this._stepList.children().eq(index).find('.oj-train-button-connector')[0];

                case 'oj-train-connector':
                  return this._connector;

                case 'oj-train-connector-fill':
                  return this._connectorFill;

                case 'oj-train-icon':
                  return this._stepList.children().eq(index).find('.oj-train-icon')[0];

                case 'oj-train-label':
                  //return this._stepList.children().eq(index).find('.oj-train-button');

                  // Bug 19518205 - OJ-TRAIN-LABEL DOES NOT RETURN THE RIGHT NODE 
                  return this._stepList.children().eq(index).find('.oj-train-label')[0];
              }

              // Non-null locators have to be handled by the component subclasses
              return null;
            }
			 // Fragments:
    
			/**
			 * <table class="keyboard-table">
			 *   <thead>
			 *     <tr>
			 *       <th>Target</th>
			 *       <th>Gesture</th>
			 *       <th>Action</th>
			 *     </tr>
			 *   </thead>
			 *   <tbody>
			 *    <tr>
			 *       <td>Step</td>
			 *       <td><kbd>Tap</kbd></td>
			 *       <td>Move to the targeted step</td>
			 *     </tr>
			 *     <tr>
			 *       <td>Step Label</td>
			 *       <td><kbd>Tap</kbd></td>
			 *       <td>Move to the targeted step</td>
			 *    </tr>
			 *   </tbody>
			 *  </table>
			 *
			 *
			 * @ojfragment touchDoc - Used in touch gesture section of classdesc, and standalone gesture doc
			 * @memberof oj.ojTrain
			 */

			/**
			 * <p>
			 * <h5>  When the focus is on the train</h5>
			 * </p>
			 * 
			 * <table class="keyboard-table">
			 *   <thead>
			 *     <tr>
			 *       <th>Key</th>
			 *       <th>Use</th>
			 *     </tr>
			 *   </thead>
			 *   <tbody>
			 *     <tr>
			 *       <td><kbd>Tab</kbd></td>
			 *       <td> Move focus to the next selectable step. </tr>
			 *     <tr>
		 	 *       <td><kbd>Shift+Tab</kbd></td>
			 *       <td> Move focus to the previous selectable step.</tr>
			 *     <tr>
			 *       <td><kbd>Enter</kbd></td>
			 *       <td> Select the focused step.</tr>
			 *   </tbody>
			 *  </table>
			 *
			 *
			 * @ojfragment keyboardDoc - Used in keyboard section of classdesc, and standalone gesture doc
			 * @memberof oj.ojTrain
			 */

          });
        }
        ());